diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aaa4b61c3..6eccd64b8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ name: CI env: DISPLAY: ':99' + ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch @@ -14,12 +15,17 @@ on: pull_request: branches: [ master ] schedule: - - cron: "0 */12 * * *" + - cron: "0 0 * * *" # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: window-build: runs-on: windows-latest + strategy: + matrix: + config: + - {python: true} + - {python: false} steps: - uses: actions/checkout@v2 @@ -36,7 +42,7 @@ jobs: with: repository: cagnulein/qmdnsengine path: "src/qmdnsengine/" - ref: "zwift" + ref: "zwift" - uses: actions/checkout@v2 - name: Checkout submodule repo @@ -54,9 +60,30 @@ jobs: path: "src/MSIX-Toolkit/" ref: b82af826d29e93e4c85d34fad8a405b6c49905e7 + - uses: actions/checkout@v2 + - name: Checkout qHttpServer + uses: actions/checkout@v2 + with: + repository: qt-labs/qthttpserver + path: "src/qthttpserver" + + + - uses: actions/setup-python@v4 + with: + python-version: 3.10.11 + - name: download python and paddleocr + run: | + python -VV + python -m pip install opencv-python + python -m pip install pywin32 + python -m pip install paddlepaddle-gpu==2.4.2.post117 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html + python -m pip install https://files.pythonhosted.org/packages/03/ac/13fbe0ebf110d57a89f055a292d4fe430fee3fb22c56f8c077e63e0c5a4e/paddlepaddle-2.4.2-cp310-cp310-win_amd64.whl + python -m pip install paddleocr>=2.0.1 + if: matrix.config.python + - uses: msys2/setup-msys2@v2 with: - install: mingw-w64-x86_64-toolchain + install: mingw-w64-x86_64-toolchain mingw-w64-x86_64-qt5-webview msystem: mingw64 release: false @@ -66,7 +93,7 @@ jobs: cmake-version: '3.20.x' - name: Install Qt - uses: jurplel/install-qt-action@v2 + uses: jurplel/install-qt-action@v3 with: version: '5.15.2' host: 'windows' @@ -75,6 +102,20 @@ jobs: arch: win64_mingw81 dir: "${{github.workspace}}/qt/" install-deps: "true" + cache: 'true' + cache-key-prefix: 'install-qt-action-windows' + + - 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: Build run: | @@ -84,6 +125,40 @@ jobs: 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 .. + make -j8 + cd src/debug + mkdir output + mkdir appx + 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 ../../../icons/iOS/iTunesArtwork@2x.png . + cp ../../AppxManifest.xml . + cp ../../windows/*.py . + cp ../../windows/*.bat . + mkdir adb + mkdir python + Copy-Item -Path C:\hostedtoolcache\windows\Python\3.10.11\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 + run: | + qmake + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_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 .. make -j8 cd src/debug @@ -92,22 +167,90 @@ jobs: cp qdomyos-zwift.exe output/ cd output windeployqt --qmldir ../../ qdomyos-zwift.exe - cp "${{github.workspace}}/qt/Qt/5.15.2/mingw81_64/bin/libwinpthread-1.dll" . - cp "${{github.workspace}}/qt/Qt/5.15.2/mingw81_64/bin/libgcc_s_seh-1.dll" . - cp "${{github.workspace}}/qt/Qt/5.15.2/mingw81_64/bin/libstdc++-6.dll" . + 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 . mkdir adb cp ../../adb/* adb/ cd .. cd appx - #../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz + #../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz + if: matrix.config.python == false + + - name: patching qt for bluetooth + run: cp qt-patches/windows/5.15.2/binary/mingw64/*.* ${{ github.workspace }}/src/debug/output/ + + - name: Zip artifact for deployment + run: Compress-Archive src/debug/output windows-binary.zip + if: matrix.config.python + + - name: Zip artifact for deployment + run: Compress-Archive src/debug/output windows-binary-no-python.zip + if: ${{ ! matrix.config.python }} - name: Archive windows binary uses: actions/upload-artifact@v2 with: name: windows-binary - path: src/debug/output + path: windows-binary.zip + if: matrix.config.python + + - name: Archive windows binary + 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/main' + # run: exit 1 + + # - uses: actions/checkout@v3 + # with: + # fetch-depth: 0 # Required due to the way Git works, without it this action won't be able to find any or the correct tags + + # - name: Get previous tag + # id: previoustag + # uses: 'WyriHaximus/github-action-get-previous-tag@v1' + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Create Release + # if: ${{ ! matrix.config.python }} + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # tag_name: ${{ steps.previoustag.outputs.tag }} + # release_name: Release ${{ steps.previoustag.outputs.tag }} + # draft: false + # prerelease: false + + # - name: upload windows artifact + # uses: actions/upload-release-asset@v1 + # if: ${{ ! matrix.config.python }} + # env: + # GITHUB_TOKEN: ${{ github.token }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_path: release.zip + # asset_name: windows-binary-no-python.zip + # asset_content_type: application/zip + + # - name: upload windows artifact + # uses: actions/upload-release-asset@v1 + # if: ${{ matrix.config.python }} + # env: + # GITHUB_TOKEN: ${{ github.token }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_path: release.zip + # asset_name: windows-binary.zip + # asset_content_type: application/zip # window-steam-build: # runs-on: windows-latest @@ -159,6 +302,7 @@ jobs: # 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 STEAM_STORE" >> secret.h # cd .. # make -j8 @@ -193,6 +337,19 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: + - name: release + uses: actions/create-release@v1 + if: startsWith(github.ref, 'refs/tags/') + id: create_release + with: + draft: false + prerelease: false + release_name: ${{ steps.version.outputs.version }} + tag_name: ${{ github.ref }} + body_path: CHANGELOG.md + env: + GITHUB_TOKEN: ${{ github.token }} + # - name: Cache Qt Linux Desktop # id: cache-qt-linux-desktop # uses: actions/cache@v1 @@ -237,15 +394,36 @@ jobs: path: "tst/googletest/" ref: "release-1.12.1" + - uses: actions/checkout@v2 + - name: Checkout qHttpServer + uses: actions/checkout@v2 + with: + repository: qt-labs/qthttpserver + path: "src/qthttpserver" + - name: Install packages required to run QZ inside workflow run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev - name: Install Qt - uses: jurplel/install-qt-action@v2 + uses: jurplel/install-qt-action@v3 with: version: '5.15.2' host: 'linux' modules: 'qtnetworkauth qtcharts' + cache: 'true' + cache-key-prefix: 'install-qt-action-linux' + + - 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: Compile Linux Desktop run: qmake; make -j8 @@ -373,6 +551,13 @@ jobs: path: "tst/googletest/" ref: "release-1.12.1" + - uses: actions/checkout@v2 + - name: Checkout qHttpServer + uses: actions/checkout@v2 + with: + repository: qt-labs/qthttpserver + path: "src/qthttpserver" + - name: Install packages required to run QZ inside workflow run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev @@ -406,20 +591,36 @@ jobs: # waiting github.com/jurplel/install-qt-action/issues/63 - name: Install Qt Android - uses: jurplel/install-qt-action@v2 + uses: jurplel/install-qt-action@v3 with: - version: '5.15.2' + version: '5.15.0' host: 'linux' target: 'android' arch: 'android' modules: 'qtcharts qtnetworkauth' dir: '${{ github.workspace }}/output/android/' + cache: 'true' + cache-key-prefix: 'install-qt-action-android' - name: Install Java uses: actions/setup-java@v3 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '11' + + - name: patching qt for bluetooth + run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/ + + - name: download 3rd party files for qthttpserver + run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/ + + - name: Build qthttpserver + run: | + cd src/qthttpserver + qmake + make -j8 + make install + cd ../.. - name: Set Android NDK 21 && build run: | @@ -431,6 +632,14 @@ jobs: echo "y" | $SDKMANAGER "ndk;21.4.7075529" export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle" export ANDROID_NDK_ROOT="${ANDROID_NDK}" + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h + echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h + echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h + echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h + echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js + echo "#define LICENSE" >> secret.h + cd .. ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393 @@ -438,3 +647,299 @@ jobs: - name: Build APK (not usable for production due to unpatched QT library) run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab + + - name: Archive apk binary + uses: actions/upload-artifact@v2 + with: + name: fdroid-android-trial + path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/ + + # - name: Exit if not on master branch + # if: github.ref == 'refs/heads/main' + # run: exit 1 + + # - 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 + runs-on: macos-latest + permissions: + contents: write + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: bluetiger9/SmtpClient-for-Qt + path: "src/smtpclient/" + ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c + + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: cagnulein/qmdnsengine + path: "src/qmdnsengine/" + ref: "zwift" + + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: google/googletest + path: "tst/googletest/" + ref: "release-1.12.1" + + - name: Install Qt iOS + uses: jurplel/install-qt-action@v3 + with: + version: '5.15.2' + host: 'mac' + target: 'ios' + modules: 'qtcharts qtnetworkauth' + dir: '${{ github.workspace }}/output/ios/' + cache: 'true' + cache-key-prefix: 'install-qt-action-ios' + + - name: fix qt + run: find ${{ github.workspace }}/output/ios/ -name 'ios.conf' -exec sed -i '' 's/ios-simulator/iphonesimulator/g' {} \; + + - name: fix qt + run: find ${{ github.workspace }}/output/ios/ -name 'devices.py' -exec sed -i '' 's/\/usr\/bin\/python/\/usr\/bin\/python3/g' {} \; + + - name: fix qt + run: find ./ -name 'qdomyos-zwift-lib.pro' -exec sed -i '' 's/TARGET = qdomyos-zwift/TARGET = qdomyoszwift/g' {} \; + + - name: patching qt for bluetooth + run: cp qt-patches/ios/5.15.2/binary/*.* ${{ github.workspace }}/output/ios/Qt/5.15.2/ios/lib/ + + - name: Build + run: | + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_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 .. + qmake CONFIG+=debug && make -j4 + + # causes iOS build on Mac to fail + # - name: Commit moc files + # uses: EndBug/add-and-commit@v9 + # with: + # message: 'moc files added' + # add: 'src/moc_*.cpp --force' + # if: github.ref == 'refs/heads/master' + + window-msvc2019-build: + runs-on: windows-latest + strategy: + matrix: + config: + - {python: true} + - {python: false} + + steps: + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: bluetiger9/SmtpClient-for-Qt + path: "src/smtpclient/" + ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c + + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: cagnulein/qmdnsengine + path: "src/qmdnsengine/" + ref: "zwift" + + - uses: actions/checkout@v2 + - name: Checkout submodule repo + uses: actions/checkout@v2 + with: + repository: google/googletest + path: "tst/googletest/" + ref: "release-1.12.1" + + - uses: actions/checkout@v2 + - name: Checkout qHttpServer + uses: actions/checkout@v2 + with: + repository: qt-labs/qthttpserver + path: "src/qthttpserver" + + - uses: actions/setup-python@v4 + with: + python-version: 3.10.11 + - name: download python and paddleocr + run: | + python -VV + python -m pip install opencv-python + python -m pip install pywin32 + python -m pip install paddlepaddle-gpu==2.4.2.post117 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html + python -m pip install https://files.pythonhosted.org/packages/03/ac/13fbe0ebf110d57a89f055a292d4fe430fee3fb22c56f8c077e63e0c5a4e/paddlepaddle-2.4.2-cp310-cp310-win_amd64.whl + python -m pip install paddleocr>=2.0.1 + if: matrix.config.python + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: '5.15.2' + host: 'windows' + modules: 'qtnetworkauth qtcharts' + target: "desktop" + arch: win64_msvc2019_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: Build + run: | + qmake + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_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 .. + nmake + cd src/debug + mkdir output + mkdir appx + cp qdomyos-zwift.exe output/ + cd output + windeployqt --qmldir ../../ qdomyos-zwift.exe + cp ../../../icons/iOS/iTunesArtwork@2x.png . + cp ../../AppxManifest.xml . + cp ../../windows/*.py . + cp ../../windows/*.bat . + mkdir adb + mkdir python + Copy-Item -Path C:\hostedtoolcache\windows\Python\3.10.11\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 + run: | + qmake + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_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 .. + nmake + cd src/debug + mkdir output + mkdir appx + 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 ../../../icons/iOS/iTunesArtwork@2x.png . + cp ../../AppxManifest.xml . + 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-msvc2019-binary.zip + if: matrix.config.python + + - name: Zip artifact for deployment + run: Compress-Archive src/debug/output windows-msvc2019-binary-no-python.zip + if: ${{ ! matrix.config.python }} + + - name: Archive windows binary + 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@v2 + with: + name: windows-msvc2019-binary-no-python + path: windows-msvc2019-binary-no-python.zip + if: ${{ ! matrix.config.python }} + + upload_to_release: + permissions: write-all + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + needs: [linux-x86-build, window-msvc2019-build, ios-build, window-build, android-build] # Specify the job dependencies + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + - name: Update nightly release + uses: andelf/nightly-release@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: nightly + prerelease: false + name: 'QZ nightly build $$' + body: | + This is a nightly build of QZ. + + You can use this if you want to try new features without waiting for releases. + From time to time, in development builds, old difficult-to-reproduce bugs are + fixed, but it is also true that in the development process with the introduction + of new complex code, the stability of the program may suffer compared to + official releases, so **use it with caution**! + + __Please help us improve QZ by reporting any issues you encounter!__ :wink: + files: | + windows-msvc2019-binary-no-python/* + windows-msvc2019-binary/* + windows-binary-no-python/* + windows-binary/* + fdroid-android-trial/* diff --git a/.gitignore b/.gitignore index 1e8f45a5b..f77b22e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ src/secret.h build-qdomyos-zwift-Android_Qt_5_15_2_Clang_Multi_Abi-Debug/* **/node_modules/* -*.pro.user template-examples/youtube-viewer/node_modules/* template-examples/youtube-viewer/*.json @@ -48,3 +47,6 @@ google_test/* !build-qdomyos-zwift-Qt_*_for_iOS-Debug # Needed for Apple Watch src/inner_templates/googlemaps/cesium-key.js *.autosave +.vscode/settings.json +/tst/Devices/.vs +src/inner_templates/googlemaps/cesium-key.js diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..bb9099b48 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +roberto.viola83@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/QZ_ESP32/QZ_ESP32.ino b/QZ_ESP32/QZ_ESP32.ino new file mode 100644 index 000000000..396f29197 --- /dev/null +++ b/QZ_ESP32/QZ_ESP32.ino @@ -0,0 +1,437 @@ +/** NimBLE_Server Demo: + * +This is working to broadcast Power and Cadence under the Cycling Power Service Profile +Data tested against Edge and Phone + * +*/ +#include +#include + +short powerInstantaneous = 0; +short cadenceInstantaneous = 0; +short speedInstantaneous = 0; +float powerScale = 1.28; // incoming power is multiplied by this value for correction +short resistance = 0; //Not currently doing anything with this value after receiving it +bool notify = false; + +// Define stuff for the Client that will receive data from Fitness Machine +// The remote service we wish to connect to. +static BLEUUID serviceUUID("1826"); // Fitness Machine +// The characteristic of the remote service we are interested in. +static BLEUUID charUUID("2ad2"); // Indoor Bike (Fitness Machine) + + +static BLEUUID HRserviceUUID("180D"); // HR Service +static BLEUUID HRcharUUID("2a37"); // HR Measuremente + +static boolean doConnect = false; +static boolean connected = false; +static boolean doScan = false; +static BLERemoteCharacteristic *pRemoteCharacteristic; +static BLEAdvertisedDevice *myDevice; +/* + * Server Stuff + */ +static NimBLEServer *pServer; +/** None of these are required as they will be handled by the library with defaults. ** + ** Remove as you see fit for your needs */ +class ServerCallbacks : public NimBLEServerCallbacks +{ + void onConnect(NimBLEServer *pServer) + { + Serial.println("Client connected"); + Serial.println("Multi-connect support: start advertising"); + NimBLEDevice::startAdvertising(); + }; + /** Alternative onConnect() method to extract details of the connection. + * See: src/ble_gap.h for the details of the ble_gap_conn_desc struct. + */ + void onConnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) + { + Serial.print("Client address: "); + Serial.println(NimBLEAddress(desc->peer_ota_addr).toString().c_str()); + /** We can use the connection handle here to ask for different connection parameters. + * Args: connection handle, min connection interval, max connection interval + * latency, supervision timeout. + * Units; Min/Max Intervals: 1.25 millisecond increments. + * Latency: number of intervals allowed to skip. + * Timeout: 10 millisecond increments, try for 5x interval time for best results. + */ + pServer->updateConnParams(desc->conn_handle, 24, 48, 0, 60); + }; + void onDisconnect(NimBLEServer *pServer) + { + Serial.println("Client disconnected - start advertising"); + NimBLEDevice::startAdvertising(); + }; + void onMTUChange(uint16_t MTU, ble_gap_conn_desc *desc) + { + Serial.printf("MTU updated: %u for connection ID: %u\n", MTU, desc->conn_handle); + }; +}; + +/** Handler class for characteristic actions */ +class CharacteristicCallbacks : public NimBLECharacteristicCallbacks +{ + void onRead(NimBLECharacteristic *pCharacteristic) + { + Serial.print(pCharacteristic->getUUID().toString().c_str()); + Serial.print(": onRead(), value: "); + Serial.println(pCharacteristic->getValue().c_str()); + }; + + void onWrite(NimBLECharacteristic *pCharacteristic) + { + Serial.print(pCharacteristic->getUUID().toString().c_str()); + Serial.print(": onWrite(), value: "); + Serial.println(pCharacteristic->getValue().c_str()); + }; + /** Called before notification or indication is sent, + * the value can be changed here before sending if desired. + */ + void onNotify(NimBLECharacteristic *pCharacteristic) + { + Serial.println("Sending notification to clients"); + }; + + /** The status returned in status is defined in NimBLECharacteristic.h. + * The value returned in code is the NimBLE host return code. + */ + void onStatus(NimBLECharacteristic *pCharacteristic, Status status, int code) + { + String str = ("Notification/Indication status code: "); + str += status; + str += ", return code: "; + str += code; + str += ", "; + str += NimBLEUtils::returnCodeToString(code); + Serial.println(str); + }; + + void onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue) + { + String str = "Client ID: "; + str += desc->conn_handle; + str += " Address: "; + str += std::string(NimBLEAddress(desc->peer_ota_addr)).c_str(); + if (subValue == 0) + { + str += " Unsubscribed to "; + } + else if (subValue == 1) + { + str += " Subscribed to notifications for "; + } + else if (subValue == 2) + { + str += " Subscribed to indications for "; + } + else if (subValue == 3) + { + str += " Subscribed to notifications and indications for "; + } + str += std::string(pCharacteristic->getUUID()).c_str(); + + Serial.println(str); + }; +}; + +/** Handler class for descriptor actions */ +class DescriptorCallbacks : public NimBLEDescriptorCallbacks +{ + void onWrite(NimBLEDescriptor *pDescriptor) + { + std::string dscVal((char *)pDescriptor->getValue(), pDescriptor->getLength()); + Serial.print("Descriptor witten value:"); + Serial.println(dscVal.c_str()); + }; + + void onRead(NimBLEDescriptor *pDescriptor) + { + Serial.print(pDescriptor->getUUID().toString().c_str()); + Serial.println(" Descriptor read"); + }; +}; +/* + * Client Stuff + */ +// This callback is for when data is received from Server +static void notifyCallback( + BLERemoteCharacteristic *pBLERemoteCharacteristic, + uint8_t *pData, + size_t length, + bool isNotify) +{ + powerInstantaneous = pData[8] | pData[9] << 8; // 2 bytes of power + cadenceInstantaneous = 60; //(pData[4] | pData[5] << 8) / 2; // 2 bytes of power in 0.5 resolution RPM, convert to RPM + resistance = pData[6]; // 1 byte of resistance + Serial.printf("Power = %d | Cadence = %d | Resistance = %d\n", powerInstantaneous, cadenceInstantaneous, resistance); +} + +/** None of these are required as they will be handled by the library with defaults. ** + ** Remove as you see fit for your needs */ +class MyClientCallback : public BLEClientCallbacks +{ + void onConnect(BLEClient *pclient) + { + } + + void onDisconnect(BLEClient *pclient) + { + connected = false; + Serial.println("onDisconnect"); + } +}; + +bool connectToServer() +{ + Serial.print("Forming a connection to "); + Serial.println(myDevice->getAddress().toString().c_str()); + + BLEClient *pClient = BLEDevice::createClient(); + Serial.println(" - Created client"); + + pClient->setClientCallbacks(new MyClientCallback()); + + // Connect to the remove BLE Server. + pClient->connect(myDevice); // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private) + Serial.println(" - Connected to server"); + + // Obtain a reference to the service we are after in the remote BLE server. + BLERemoteService *pRemoteService = pClient->getService(serviceUUID); + if (pRemoteService == nullptr) + { + Serial.print("Failed to find our service UUID: "); + Serial.println(serviceUUID.toString().c_str()); + pClient->disconnect(); + return false; + } + Serial.println(" - Found our service"); + + // Obtain a reference to the characteristic in the service of the remote BLE server. + pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); + if (pRemoteCharacteristic == nullptr) + { + Serial.print("Failed to find our characteristic UUID: "); + Serial.println(charUUID.toString().c_str()); + pClient->disconnect(); + return false; + } + Serial.println(" - Found our characteristic"); + + // Read the value of the characteristic. + if (pRemoteCharacteristic->canRead()) + { + std::string value = pRemoteCharacteristic->readValue(); + Serial.print("The characteristic value was: "); + Serial.println(value.c_str()); + } + + if (pRemoteCharacteristic->canNotify()) + pRemoteCharacteristic->registerForNotify(notifyCallback); + + connected = true; + return true; +} + +/** + * Scan for BLE servers and find the first one that advertises the service we are looking for. + */ +class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks +{ + /** + * Called for each advertising BLE server. + */ + + /*** Only a reference to the advertised device is passed now + void onResult(BLEAdvertisedDevice advertisedDevice) { **/ + void onResult(BLEAdvertisedDevice *advertisedDevice) + { + Serial.print("BLE Advertised Device found: "); + Serial.println(advertisedDevice->toString().c_str()); + + // We have found a device, let us now see if it contains the service we are looking for. + /******************************************************************************** + if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) { +********************************************************************************/ + if (advertisedDevice->haveServiceUUID() && advertisedDevice->isAdvertisingService(serviceUUID)) + { + + BLEDevice::getScan()->stop(); + /******************************************************************* + myDevice = new BLEAdvertisedDevice(advertisedDevice); +*******************************************************************/ + myDevice = advertisedDevice; /** Just save the reference now, no need to copy the object */ + doConnect = true; + doScan = true; + + } // Found our server + } // onResult +}; // MyAdvertisedDeviceCallbacks + +//delays for X ms, should not block execution +void softDelay(unsigned long delayTime) +{ + unsigned long startTime = millis(); + while ((millis() - startTime) < delayTime) + { + //wait + } +} + +/** Define callback instances globally to use for multiple Characteristics \ Descriptors */ +// This section is for the Server that will broadcast the data as Cycling Power +static DescriptorCallbacks dscCallbacks; +static CharacteristicCallbacks chrCallbacks; +NimBLECharacteristic *CyclingPowerFeature = NULL; +NimBLECharacteristic *CyclingPowerMeasurement = NULL; +NimBLECharacteristic *CyclingPowerSensorLocation = NULL; +NimBLECharacteristic *HRMeasurement = NULL; +unsigned char bleBuffer[8]; +unsigned char slBuffer[1]; +unsigned char fBuffer[4]; +unsigned short revolutions = 0; +unsigned short timestamp = 0; +unsigned short flags = 0x20; +byte sensorlocation = 0x0D; +long lastNotify = 0; +long lastRevolution = 0; + +void setup() +{ + Serial.begin(115200); + Serial.println("Starting NimBLE Server"); + + /** sets device name */ + NimBLEDevice::init("QZESP"); + /** Optional: set the transmit power, default is 3db */ + NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */ + + pServer = NimBLEDevice::createServer(); + pServer->setCallbacks(new ServerCallbacks()); + + fBuffer[0] = 0x00; + fBuffer[1] = 0x00; + fBuffer[2] = 0x00; + fBuffer[3] = 0x08; + + slBuffer[0] = sensorlocation & 0xff; + + NimBLEService *pDeadService = pServer->createService("1818"); + CyclingPowerFeature = pDeadService->createCharacteristic( + "2A65", + NIMBLE_PROPERTY::READ); + CyclingPowerSensorLocation = pDeadService->createCharacteristic( + "2A5D", + NIMBLE_PROPERTY::READ); + CyclingPowerMeasurement = pDeadService->createCharacteristic( + "2A63", + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); + + CyclingPowerFeature->setValue(fBuffer, 4); + CyclingPowerSensorLocation->setValue(slBuffer, 1); + CyclingPowerMeasurement->setValue(slBuffer, 1); + + /** Start the services when finished creating all Characteristics and Descriptors */ + pDeadService->start(); + +#if 0 + // HR service + NimBLEService *pHRService = pServer->createService("180D"); + HRMeasurement = pHRService->createCharacteristic( + "2A37", + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); + + HRMeasurement->setValue(fBuffer, 2); + + /** Start the services when finished creating all Characteristics and Descriptors */ + pHRService->start(); +#endif + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + /** Add the services to the advertisement data **/ +// pAdvertising->addServiceUUID(pHRService->getUUID()); + pAdvertising->addServiceUUID(pDeadService->getUUID()); + pAdvertising->setScanResponse(true); + pAdvertising->start(); + + Serial.println("Advertising Started"); + + Serial.println("Starting Arduino BLE Client application..."); + BLEDevice::init(""); + + // Retrieve a Scanner and set the callback we want to use to be informed when we + // have detected a new device. Specify that we want active scanning and start the + // scan to run for 5 seconds. + BLEScan *pBLEScan = BLEDevice::getScan(); + pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); + pBLEScan->setInterval(1349); + pBLEScan->setWindow(449); + pBLEScan->setActiveScan(true); + pBLEScan->start(5, false); +} + +void loop() +{ + // If the flag "doConnect" is true then we have scanned for and found the desired + // BLE Server with which we wish to connect. Now we connect to it. Once we are + // connected we set the connected flag to be true. + if (doConnect == true) + { + if (connectToServer()) + { + Serial.println("We are now connected to the BLE Server."); + } + else + { + Serial.println("We have failed to connect to the server; there is nothing more we will do."); + } + doConnect = false; + } + // If we are connected to a peer BLE Server, update the characteristic each time we are reached + // with the current time since boot. + if (connected) + { + //Stuff to do when connected to Client + } + else if (doScan) + { + BLEDevice::getScan()->start(0); // this is just sample to start scan after disconnect, most likely there is better way to do it in arduino + } + + // convert RPM to timestamp + if (cadenceInstantaneous != 0 && (millis()) >= (lastRevolution + (60000 / cadenceInstantaneous))) + { + revolutions++; // One crank revolution should have passed, add one revolution + timestamp = (unsigned short)(((millis() * 1024) / 1000) % 65536); // create timestamp and format + lastRevolution = millis(); + } + + if (millis() - lastNotify >= 1000) // do this every second + { + //if (pServer->getConnectedCount() > 0) + { + bleBuffer[0] = flags & 0xff; + bleBuffer[1] = (flags >> 8) & 0xff; + bleBuffer[2] = powerInstantaneous & 0xff; + bleBuffer[3] = (powerInstantaneous >> 8) & 0xff; + bleBuffer[4] = revolutions & 0xff; + bleBuffer[5] = (revolutions >> 8) & 0xff; + bleBuffer[6] = timestamp & 0xff; + bleBuffer[7] = (timestamp >> 8) & 0xff; + CyclingPowerMeasurement->setValue(bleBuffer, 8); + CyclingPowerMeasurement->notify(); + + /*bleBuffer[0] = 0; + bleBuffer[1] = powerInstantaneous; + + HRMeasurement->setValue(bleBuffer, 2); + HRMeasurement->notify();*/ + lastNotify = millis(); + } + } + /*if (pServer->getConnectedCount() == 0) + { + powerInstantaneous = 0; + }*/ +} \ No newline at end of file diff --git a/README.md b/README.md index eb522dc83..fb41e661d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ UI on MacOS ### Installation -You can install on multiple platforms. +You can install it on multiple platforms. Read the [installation procedure](docs/10_Installation.md) @@ -45,7 +45,7 @@ You can run the app on [Macintosh or Linux devices](docs/10_Installation.md). IO 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 +### No GUI version run as @@ -57,7 +57,7 @@ https://github.com/ProH4Ck/treadmill-bridge https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/ -Icons used in this documentation comes from [flaticon.com](https://www.flaticon.com) +Icons used in this documentation come from [flaticon.com](https://www.flaticon.com) ### Blog diff --git a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj index b96fe9eb0..51d88c0af 100644 --- a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj +++ b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -143,6 +143,9 @@ 87083D9626678EFA0072410D /* zwiftworkout.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87083D9526678EFA0072410D /* zwiftworkout.cpp */; }; 87097D2F275EA9A30020EE6F /* sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D2D275EA9A20020EE6F /* sportsplusbike.cpp */; }; 87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */; }; + 8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706B29C48AEA0094D0F3 /* handleurl.cpp */; }; + 8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */; }; + 8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710707229C4A5E70094D0F3 /* GarminConnect.swift */; }; 871189132893C930006A04D1 /* libQt5Multimedia.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 871189122893C92F006A04D1 /* libQt5Multimedia.a */; }; 871189152893CB52006A04D1 /* libdeclarative_multimedia.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 871189142893CB51006A04D1 /* libdeclarative_multimedia.a */; }; 871189172893CC45006A04D1 /* libQt5MultimediaQuick.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 871189162893CC44006A04D1 /* libQt5MultimediaQuick.a */; }; @@ -171,6 +174,8 @@ 872A20DC28C5F5CE0037774D /* moc_faketreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872A20DB28C5F5CE0037774D /* moc_faketreadmill.cpp */; }; 872BAB4E261750EE006A59AB /* libQt5Charts.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 872BAB4D261750EE006A59AB /* libQt5Charts.a */; }; 872BAB50261751FB006A59AB /* libqtchartsqml2.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 872BAB4F261751FB006A59AB /* libqtchartsqml2.a */; }; + 872DCC392A18D4A800EC9F68 /* virtualdevice.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872DCC382A18D4A800EC9F68 /* virtualdevice.cpp */; }; + 872DCC3B2A18D4C000EC9F68 /* moc_virtualdevice.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872DCC3A2A18D4C000EC9F68 /* moc_virtualdevice.cpp */; }; 873063BE259DF20000DA0F44 /* heartratebelt.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873063BC259DF20000DA0F44 /* heartratebelt.cpp */; }; 873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873063BF259DF2C500DA0F44 /* moc_heartratebelt.cpp */; }; 87310B1E266FBB59008BA0D6 /* smartrowrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87310B1B266FBB54008BA0D6 /* smartrowrower.cpp */; }; @@ -238,6 +243,8 @@ 873CD22D27EF8E4B000131BC /* iosinapppurchaseproduct.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873CD22927EF8E4B000131BC /* iosinapppurchaseproduct.mm */; }; 873CD22F27EF8EC1000131BC /* StoreKit.framework in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 873CD22E27EF8EC1000131BC /* StoreKit.framework */; }; 873CD23027EF8EF5000131BC /* iosinapppurchasetransaction.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873CD22727EF8E4B000131BC /* iosinapppurchasetransaction.mm */; }; + 873D388B29B0D745006A2611 /* ConnectIQ.xcframework in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 873D388A29B0D744006A2611 /* ConnectIQ.xcframework */; }; + 873D388C29B0D745006A2611 /* ConnectIQ.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 873D388A29B0D744006A2611 /* ConnectIQ.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 873F022F274BE471002D0349 /* mcfbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873F022D274BE471002D0349 /* mcfbike.cpp */; }; 873F0231274BE47D002D0349 /* moc_mcfbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 873F0230274BE47D002D0349 /* moc_mcfbike.cpp */; }; 87420DF6269D770F000C5EC6 /* libQt5WebView.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87420DF5269D770F000C5EC6 /* libQt5WebView.a */; }; @@ -295,6 +302,8 @@ 876F9B61275385D8006AE6FA /* moc_fitmetria_fanfit.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876F9B60275385D8006AE6FA /* moc_fitmetria_fanfit.cpp */; }; 8772A0E625E43ADB0080718C /* trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */; }; 8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */; }; + 8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */; }; + 8775008529E87713008E48B7 /* moc_iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */; }; 877A080D2893DC4300C0F0AB /* CoreVideo.framework in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 879F74122893D705009A64C8 /* CoreVideo.framework */; }; 877A7609269D8E9F0024DD2C /* WebKit.framework in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 877A7608269D8E9F0024DD2C /* WebKit.framework */; }; 877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877FBA27276E684400F6C0C9 /* bowflextreadmill.cpp */; }; @@ -315,6 +324,8 @@ 878A331D25AB50C300BD13E1 /* moc_yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */; }; 878C9E6928B77E7C00669129 /* nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */; }; 878C9E6B28B77E9800669129 /* moc_nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */; }; + 878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878D83732A1F33C600D7F004 /* bkoolbike.cpp */; }; + 878D83762A1F33D900D7F004 /* moc_bkoolbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878D83752A1F33D900D7F004 /* moc_bkoolbike.cpp */; }; 87900DC6268B672E000CB351 /* renphobike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87900DC5268B672E000CB351 /* renphobike.cpp */; }; 87900DC8268B673C000CB351 /* moc_renphobike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87900DC7268B673C000CB351 /* moc_renphobike.cpp */; }; 8790FDDF277B0ABA00247550 /* nautilustreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8790FDDD277B0ABA00247550 /* nautilustreadmill.cpp */; }; @@ -325,6 +336,7 @@ 87917A7728E768D200F8D9AC /* Client.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87917A7228E768D200F8D9AC /* Client.swift */; }; 8791A8AA25C8603F003B50B2 /* moc_inspirebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8791A8A925C8603F003B50B2 /* moc_inspirebike.cpp */; }; 8791A8AB25C861BD003B50B2 /* inspirebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8791A8A825C8602A003B50B2 /* inspirebike.cpp */; }; + 87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87943AB229E0215D007575F2 /* localipaddress.cpp */; }; 87958F1927628D4500124B24 /* elitesterzosmart.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87958F1827628D4500124B24 /* elitesterzosmart.cpp */; }; 87958F1B27628D5400124B24 /* moc_elitesterzosmart.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87958F1A27628D5400124B24 /* moc_elitesterzosmart.cpp */; }; 8798C8872733E103003148B3 /* strydrunpowersensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8798C8862733E103003148B3 /* strydrunpowersensor.cpp */; }; @@ -340,10 +352,14 @@ 879F740F2893D592009A64C8 /* libqtmedia_audioengine.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 879F740E2893D591009A64C8 /* libqtmedia_audioengine.a */; }; 879F74112893D5B8009A64C8 /* libqavfcamera.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 879F74102893D5B7009A64C8 /* libqavfcamera.a */; }; 879F74152893D732009A64C8 /* CoreMedia.framework in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 879F74142893D732009A64C8 /* CoreMedia.framework */; }; + 87A0771029B641D600A368BF /* wahookickrheadwind.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0770F29B641D500A368BF /* wahookickrheadwind.cpp */; }; + 87A0771229B6420200A368BF /* moc_wahookickrheadwind.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0771129B6420200A368BF /* moc_wahookickrheadwind.cpp */; }; 87A0C4BB262329A600121A76 /* npecablebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0C4B7262329A600121A76 /* npecablebike.cpp */; }; 87A0C4BC262329A600121A76 /* cscbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0C4B9262329A600121A76 /* cscbike.cpp */; }; 87A0C4BF262329B500121A76 /* moc_cscbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0C4BD262329B500121A76 /* moc_cscbike.cpp */; }; 87A0C4C0262329B500121A76 /* moc_npecablebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0C4BE262329B500121A76 /* moc_npecablebike.cpp */; }; + 87A0D7522A3A4518005147F2 /* fakerower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0D7502A3A4517005147F2 /* fakerower.cpp */; }; + 87A0D7542A3A4547005147F2 /* moc_fakerower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A0D7532A3A4547005147F2 /* moc_fakerower.cpp */; }; 87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A18F052660D5C0002D7C96 /* ftmsrower.cpp */; }; 87A18F092660D5D9002D7C96 /* moc_ftmsrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A18F082660D5D9002D7C96 /* moc_ftmsrower.cpp */; }; 87A3BC222656429600D302E3 /* rower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3BC1F2656429400D302E3 /* rower.cpp */; }; @@ -354,6 +370,8 @@ 87ADD2BB27634C1500B7A0AB /* technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */; }; 87ADD2BD27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */; }; 87AE0CB227760DCB00E547E9 /* virtualtreadmill_zwift.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87AE0CB127760DCB00E547E9 /* virtualtreadmill_zwift.swift */; }; + 87B187BB29B8C552007EEF9D /* ziprotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87B187B929B8C552007EEF9D /* ziprotreadmill.cpp */; }; + 87B187BD29B8C577007EEF9D /* moc_ziprotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87B187BC29B8C577007EEF9D /* moc_ziprotreadmill.cpp */; }; 87B617EC25F25FED0094A1CB /* screencapture.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87B617E725F25FEC0094A1CB /* screencapture.cpp */; }; 87B617ED25F25FED0094A1CB /* fitshowtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87B617EA25F25FED0094A1CB /* fitshowtreadmill.cpp */; }; 87B617EE25F25FED0094A1CB /* snodebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87B617EB25F25FED0094A1CB /* snodebike.cpp */; }; @@ -362,6 +380,8 @@ 87B617F425F260150094A1CB /* moc_screencapture.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87B617F125F260150094A1CB /* moc_screencapture.cpp */; }; 87BB1774269E983200F46A1C /* moc_webserverinfosender.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BB1773269E983200F46A1C /* moc_webserverinfosender.cpp */; }; 87BB1776269E987100F46A1C /* libQt5HttpServer.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87BB1775269E987000F46A1C /* libQt5HttpServer.a */; }; + 87BCE6BD29F28F72001F70EB /* ypooelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BCE6BC29F28F72001F70EB /* ypooelliptical.cpp */; }; + 87BCE6BF29F28F95001F70EB /* moc_ypooelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BCE6BE29F28F94001F70EB /* moc_ypooelliptical.cpp */; }; 87BE6FDC272D2A3100C35795 /* horizongr7bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BE6FDA272D2A3100C35795 /* horizongr7bike.cpp */; }; 87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BE6FDD272D2A3E00C35795 /* moc_horizongr7bike.cpp */; }; 87BF116D298E28CA00B5B6E7 /* pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */; }; @@ -409,6 +429,7 @@ 87D269A325F535340076AA48 /* moc_skandikawiribike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D269A125F535300076AA48 /* moc_skandikawiribike.cpp */; }; 87D269A425F535340076AA48 /* moc_m3ibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D269A225F535300076AA48 /* moc_m3ibike.cpp */; }; 87D44181269DE979003263D5 /* webserverinfosender.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D44180269DE979003263D5 /* webserverinfosender.cpp */; }; + 87D4693629B64D8100C9A382 /* ios_app_delegate.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D4693529B64D8100C9A382 /* ios_app_delegate.mm */; }; 87D5DC402823047D008CCDE7 /* truetreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D5DC3E2823047D008CCDE7 /* truetreadmill.cpp */; }; 87D5DC4228230496008CCDE7 /* moc_truetreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D5DC4128230496008CCDE7 /* moc_truetreadmill.cpp */; }; 87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F992800B9970026D43C /* proformwifibike.cpp */; }; @@ -453,6 +474,8 @@ 87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; }; 87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */; }; 87F1179E26A5FBDE00541B3A /* libqtwebview_darwin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87420DF7269D7CE1000C5EC6 /* libqtwebview_darwin.a */; }; + 87F4FB5A29D550C00061BB4A /* schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */; }; + 87F4FB5C29D550E00061BB4A /* moc_schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */; }; 87F527BE28EEB5AA00A9F8D5 /* qzsettings.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F527BC28EEB5AA00A9F8D5 /* qzsettings.cpp */; }; 87F93427278E0EC00088B596 /* domyosrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F93426278E0EC00088B596 /* domyosrower.cpp */; }; 87F93429278E0ECF0088B596 /* moc_domyosrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F93428278E0ECF0088B596 /* moc_domyosrower.cpp */; }; @@ -582,6 +605,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 873D388C29B0D745006A2611 /* ConnectIQ.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -818,6 +842,10 @@ 87097D2D275EA9A20020EE6F /* sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sportsplusbike.cpp; path = ../src/sportsplusbike.cpp; sourceTree = ""; }; 87097D2E275EA9A20020EE6F /* sportsplusbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = sportsplusbike.h; path = ../src/sportsplusbike.h; sourceTree = ""; }; 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusbike.cpp; sourceTree = ""; }; + 8710706A29C48AE90094D0F3 /* handleurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = handleurl.h; path = ../src/handleurl.h; sourceTree = ""; }; + 8710706B29C48AEA0094D0F3 /* handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = handleurl.cpp; path = ../src/handleurl.cpp; sourceTree = ""; }; + 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_handleurl.cpp; sourceTree = ""; }; + 8710707229C4A5E70094D0F3 /* GarminConnect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GarminConnect.swift; path = ../src/ios/GarminConnect.swift; sourceTree = ""; }; 871189122893C92F006A04D1 /* libQt5Multimedia.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5Multimedia.a; path = ../../Qt/5.15.2/ios/lib/libQt5Multimedia.a; sourceTree = ""; }; 871189142893CB51006A04D1 /* libdeclarative_multimedia.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libdeclarative_multimedia.a; path = ../../Qt/5.15.2/ios/qml/QtMultimedia/libdeclarative_multimedia.a; sourceTree = ""; }; 871189162893CC44006A04D1 /* libQt5MultimediaQuick.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5MultimediaQuick.a; path = ../../Qt/5.15.2/ios/lib/libQt5MultimediaQuick.a; sourceTree = ""; }; @@ -858,6 +886,9 @@ 872A20DB28C5F5CE0037774D /* moc_faketreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_faketreadmill.cpp; sourceTree = ""; }; 872BAB4D261750EE006A59AB /* libQt5Charts.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5Charts.a; path = ../../Qt/5.15.2/ios/lib/libQt5Charts.a; sourceTree = ""; }; 872BAB4F261751FB006A59AB /* libqtchartsqml2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtchartsqml2.a; path = ../../Qt/5.15.2/ios/qml/QtCharts/libqtchartsqml2.a; sourceTree = ""; }; + 872DCC372A18D4A800EC9F68 /* virtualdevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = virtualdevice.h; path = ../src/virtualdevice.h; sourceTree = ""; }; + 872DCC382A18D4A800EC9F68 /* virtualdevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = virtualdevice.cpp; path = ../src/virtualdevice.cpp; sourceTree = ""; }; + 872DCC3A2A18D4C000EC9F68 /* moc_virtualdevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_virtualdevice.cpp; sourceTree = ""; }; 873063BC259DF20000DA0F44 /* heartratebelt.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = heartratebelt.cpp; path = ../src/heartratebelt.cpp; sourceTree = ""; }; 873063BD259DF20000DA0F44 /* heartratebelt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = heartratebelt.h; path = ../src/heartratebelt.h; sourceTree = ""; }; 873063BF259DF2C500DA0F44 /* moc_heartratebelt.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_heartratebelt.cpp; sourceTree = ""; }; @@ -960,6 +991,7 @@ 873CD22927EF8E4B000131BC /* iosinapppurchaseproduct.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = iosinapppurchaseproduct.mm; path = ../src/purchasing/ios/iosinapppurchaseproduct.mm; sourceTree = ""; }; 873CD22A27EF8E4B000131BC /* iosinapppurchasetransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iosinapppurchasetransaction.h; path = ../src/purchasing/ios/iosinapppurchasetransaction.h; sourceTree = ""; }; 873CD22E27EF8EC1000131BC /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 873D388A29B0D744006A2611 /* ConnectIQ.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ConnectIQ.xcframework; path = ../src/ConnectIQ/iOS/ConnectIQ.xcframework; sourceTree = ""; }; 873F022D274BE471002D0349 /* mcfbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = mcfbike.cpp; path = ../src/mcfbike.cpp; sourceTree = ""; }; 873F022E274BE471002D0349 /* mcfbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = mcfbike.h; path = ../src/mcfbike.h; sourceTree = ""; }; 873F0230274BE47D002D0349 /* moc_mcfbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_mcfbike.cpp; sourceTree = ""; }; @@ -1050,6 +1082,9 @@ 8772A0E425E43AD90080718C /* trxappgateusbbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = trxappgateusbbike.h; path = ../src/trxappgateusbbike.h; sourceTree = ""; }; 8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = trxappgateusbbike.cpp; path = ../src/trxappgateusbbike.cpp; sourceTree = ""; }; 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_trxappgateusbbike.cpp; sourceTree = ""; }; + 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = iconceptelliptical.cpp; path = ../src/iconceptelliptical.cpp; sourceTree = ""; }; + 8775008229E876F7008E48B7 /* iconceptelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iconceptelliptical.h; path = ../src/iconceptelliptical.h; sourceTree = ""; }; + 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_iconceptelliptical.cpp; sourceTree = ""; }; 877A7606269D8E0F0024DD2C /* libqtwebview_darwin_debug.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtwebview_darwin_debug.a; path = ../../Qt/5.15.2/ios/plugins/webview/libqtwebview_darwin_debug.a; sourceTree = ""; }; 877A7608269D8E9F0024DD2C /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 877FBA27276E684400F6C0C9 /* bowflextreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bowflextreadmill.cpp; path = ../src/bowflextreadmill.cpp; sourceTree = ""; }; @@ -1080,6 +1115,9 @@ 878C9E6728B77E7B00669129 /* nordictrackifitadbbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbbike.h; path = ../src/nordictrackifitadbbike.h; sourceTree = ""; }; 878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbbike.cpp; path = ../src/nordictrackifitadbbike.cpp; sourceTree = ""; }; 878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbbike.cpp; sourceTree = ""; }; + 878D83722A1F33C600D7F004 /* bkoolbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bkoolbike.h; path = ../src/bkoolbike.h; sourceTree = ""; }; + 878D83732A1F33C600D7F004 /* bkoolbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bkoolbike.cpp; path = ../src/bkoolbike.cpp; sourceTree = ""; }; + 878D83752A1F33D900D7F004 /* moc_bkoolbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bkoolbike.cpp; sourceTree = ""; }; 87900DC4268B672E000CB351 /* renphobike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = renphobike.h; path = ../src/renphobike.h; sourceTree = ""; }; 87900DC5268B672E000CB351 /* renphobike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = renphobike.cpp; path = ../src/renphobike.cpp; sourceTree = ""; }; 87900DC7268B673C000CB351 /* moc_renphobike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_renphobike.cpp; sourceTree = ""; }; @@ -1093,6 +1131,8 @@ 8791A8A725C8602A003B50B2 /* inspirebike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = inspirebike.h; path = ../src/inspirebike.h; sourceTree = ""; }; 8791A8A825C8602A003B50B2 /* inspirebike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = inspirebike.cpp; path = ../src/inspirebike.cpp; sourceTree = ""; }; 8791A8A925C8603F003B50B2 /* moc_inspirebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inspirebike.cpp; sourceTree = ""; }; + 87943AB229E0215D007575F2 /* localipaddress.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = localipaddress.cpp; path = ../src/localipaddress.cpp; sourceTree = ""; }; + 87943AB329E0215D007575F2 /* localipaddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = localipaddress.h; path = ../src/localipaddress.h; sourceTree = ""; }; 87958F1727628D4500124B24 /* elitesterzosmart.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = elitesterzosmart.h; path = ../src/elitesterzosmart.h; sourceTree = ""; }; 87958F1827628D4500124B24 /* elitesterzosmart.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = elitesterzosmart.cpp; path = ../src/elitesterzosmart.cpp; sourceTree = ""; }; 87958F1A27628D5400124B24 /* moc_elitesterzosmart.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elitesterzosmart.cpp; sourceTree = ""; }; @@ -1113,12 +1153,18 @@ 879F74102893D5B7009A64C8 /* libqavfcamera.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqavfcamera.a; path = ../../Qt/5.15.2/ios/plugins/mediaservice/libqavfcamera.a; sourceTree = ""; }; 879F74122893D705009A64C8 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; 879F74142893D732009A64C8 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + 87A0770E29B641D500A368BF /* wahookickrheadwind.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = wahookickrheadwind.h; path = ../src/wahookickrheadwind.h; sourceTree = ""; }; + 87A0770F29B641D500A368BF /* wahookickrheadwind.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = wahookickrheadwind.cpp; path = ../src/wahookickrheadwind.cpp; sourceTree = ""; }; + 87A0771129B6420200A368BF /* moc_wahookickrheadwind.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_wahookickrheadwind.cpp; sourceTree = ""; }; 87A0C4B7262329A600121A76 /* npecablebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = npecablebike.cpp; path = ../src/npecablebike.cpp; sourceTree = ""; }; 87A0C4B8262329A600121A76 /* cscbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = cscbike.h; path = ../src/cscbike.h; sourceTree = ""; }; 87A0C4B9262329A600121A76 /* cscbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = cscbike.cpp; path = ../src/cscbike.cpp; sourceTree = ""; }; 87A0C4BA262329A600121A76 /* npecablebike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = npecablebike.h; path = ../src/npecablebike.h; sourceTree = ""; }; 87A0C4BD262329B500121A76 /* moc_cscbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_cscbike.cpp; sourceTree = ""; }; 87A0C4BE262329B500121A76 /* moc_npecablebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_npecablebike.cpp; sourceTree = ""; }; + 87A0D7502A3A4517005147F2 /* fakerower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakerower.cpp; path = ../src/fakerower.cpp; sourceTree = ""; }; + 87A0D7512A3A4517005147F2 /* fakerower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fakerower.h; path = ../src/fakerower.h; sourceTree = ""; }; + 87A0D7532A3A4547005147F2 /* moc_fakerower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fakerower.cpp; sourceTree = ""; }; 87A18F052660D5C0002D7C96 /* ftmsrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ftmsrower.cpp; path = ../src/ftmsrower.cpp; sourceTree = ""; }; 87A18F062660D5C1002D7C96 /* ftmsrower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ftmsrower.h; path = ../src/ftmsrower.h; sourceTree = ""; }; 87A18F082660D5D9002D7C96 /* moc_ftmsrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_ftmsrower.cpp; sourceTree = ""; }; @@ -1137,6 +1183,9 @@ 87ADD2BA27634C1400B7A0AB /* technogymmyruntreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = technogymmyruntreadmill.h; path = ../src/technogymmyruntreadmill.h; sourceTree = ""; }; 87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymmyruntreadmill.cpp; sourceTree = ""; }; 87AE0CB127760DCB00E547E9 /* virtualtreadmill_zwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = virtualtreadmill_zwift.swift; path = ../src/ios/virtualtreadmill_zwift.swift; sourceTree = ""; }; + 87B187B929B8C552007EEF9D /* ziprotreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ziprotreadmill.cpp; path = ../src/ziprotreadmill.cpp; sourceTree = ""; }; + 87B187BA29B8C552007EEF9D /* ziprotreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ziprotreadmill.h; path = ../src/ziprotreadmill.h; sourceTree = ""; }; + 87B187BC29B8C577007EEF9D /* moc_ziprotreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_ziprotreadmill.cpp; sourceTree = ""; }; 87B617E625F25FEC0094A1CB /* fitshowtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fitshowtreadmill.h; path = ../src/fitshowtreadmill.h; sourceTree = ""; }; 87B617E725F25FEC0094A1CB /* screencapture.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = screencapture.cpp; path = ../src/screencapture.cpp; sourceTree = ""; }; 87B617E825F25FEC0094A1CB /* screencapture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = screencapture.h; path = ../src/screencapture.h; sourceTree = ""; }; @@ -1148,6 +1197,9 @@ 87B617F125F260150094A1CB /* moc_screencapture.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_screencapture.cpp; sourceTree = ""; }; 87BB1773269E983200F46A1C /* moc_webserverinfosender.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_webserverinfosender.cpp; sourceTree = ""; }; 87BB1775269E987000F46A1C /* libQt5HttpServer.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5HttpServer.a; path = ../../Qt/5.15.2/ios/lib/libQt5HttpServer.a; sourceTree = ""; }; + 87BCE6BB29F28F72001F70EB /* ypooelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ypooelliptical.h; path = ../src/ypooelliptical.h; sourceTree = ""; }; + 87BCE6BC29F28F72001F70EB /* ypooelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ypooelliptical.cpp; path = ../src/ypooelliptical.cpp; sourceTree = ""; }; + 87BCE6BE29F28F94001F70EB /* moc_ypooelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_ypooelliptical.cpp; sourceTree = ""; }; 87BE6FDA272D2A3100C35795 /* horizongr7bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = horizongr7bike.cpp; path = ../src/horizongr7bike.cpp; sourceTree = ""; }; 87BE6FDB272D2A3100C35795 /* horizongr7bike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = horizongr7bike.h; path = ../src/horizongr7bike.h; sourceTree = ""; }; 87BE6FDD272D2A3E00C35795 /* moc_horizongr7bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_horizongr7bike.cpp; sourceTree = ""; }; @@ -1220,6 +1272,7 @@ 87D269A125F535300076AA48 /* moc_skandikawiribike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_skandikawiribike.cpp; sourceTree = ""; }; 87D269A225F535300076AA48 /* moc_m3ibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_m3ibike.cpp; sourceTree = ""; }; 87D44180269DE979003263D5 /* webserverinfosender.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = webserverinfosender.cpp; path = ../src/webserverinfosender.cpp; sourceTree = ""; }; + 87D4693529B64D8100C9A382 /* ios_app_delegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_app_delegate.mm; path = ../src/ios/ios_app_delegate.mm; sourceTree = ""; }; 87D5DC3E2823047D008CCDE7 /* truetreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = truetreadmill.cpp; path = ../src/truetreadmill.cpp; sourceTree = ""; }; 87D5DC3F2823047D008CCDE7 /* truetreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = truetreadmill.h; path = ../src/truetreadmill.h; sourceTree = ""; }; 87D5DC4128230496008CCDE7 /* moc_truetreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_truetreadmill.cpp; sourceTree = ""; }; @@ -1283,6 +1336,9 @@ 87F02E3E29178523000DB52C /* octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = octaneelliptical.cpp; path = ../src/octaneelliptical.cpp; sourceTree = ""; }; 87F02E3F29178524000DB52C /* octaneelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = octaneelliptical.h; path = ../src/octaneelliptical.h; sourceTree = ""; }; 87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_octaneelliptical.cpp; sourceTree = ""; }; + 87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = schwinn170bike.cpp; path = ../src/schwinn170bike.cpp; sourceTree = ""; }; + 87F4FB5929D550BF0061BB4A /* schwinn170bike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = schwinn170bike.h; path = ../src/schwinn170bike.h; sourceTree = ""; }; + 87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_schwinn170bike.cpp; sourceTree = ""; }; 87F527BC28EEB5AA00A9F8D5 /* qzsettings.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = qzsettings.cpp; path = ../src/qzsettings.cpp; sourceTree = ""; }; 87F527BD28EEB5AA00A9F8D5 /* qzsettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = qzsettings.h; path = ../src/qzsettings.h; sourceTree = ""; }; 87F93425278E0EC00088B596 /* domyosrower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = domyosrower.h; path = ../src/domyosrower.h; sourceTree = ""; }; @@ -1592,6 +1648,7 @@ 023642106C14651D2E1F4D5D /* dialogplugin in Link Binary With Libraries */, 133CA0345CD2BFB03079A655 /* qmlfolderlistmodelplugin in Link Binary With Libraries */, 4AAA7380490CBDBAA8587CFA /* qmlsettingsplugin in Link Binary With Libraries */, + 873D388B29B0D745006A2611 /* ConnectIQ.xcframework in Link Binary With Libraries */, 3BD5A5F95DF5239184791B58 /* dialogsprivateplugin in Link Binary With Libraries */, EF98F8C34BE322582E9B73D7 /* qtquickcontrolsplugin in Link Binary With Libraries */, 7C8D236C48F2964061C3457C /* widgetsplugin in Link Binary With Libraries */, @@ -1664,6 +1721,15 @@ 25B08E2869634E9BCBA333A2 /* Generated Sources */ = { isa = PBXGroup; children = ( + 87A0D7532A3A4547005147F2 /* moc_fakerower.cpp */, + 878D83752A1F33D900D7F004 /* moc_bkoolbike.cpp */, + 872DCC3A2A18D4C000EC9F68 /* moc_virtualdevice.cpp */, + 87BCE6BE29F28F94001F70EB /* moc_ypooelliptical.cpp */, + 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */, + 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */, + 87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */, + 87B187BC29B8C577007EEF9D /* moc_ziprotreadmill.cpp */, + 87A0771129B6420200A368BF /* moc_wahookickrheadwind.cpp */, 874D272129AFA13B0007C079 /* moc_apexbike.cpp */, 87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */, 87CF516A293C87AF00A7CABC /* moc_characteristicwriteprocessore005.cpp */, @@ -1823,6 +1889,27 @@ 2EB56BE3C2D93CDAB0C52E67 /* Sources */ = { isa = PBXGroup; children = ( + 87A0D7502A3A4517005147F2 /* fakerower.cpp */, + 87A0D7512A3A4517005147F2 /* fakerower.h */, + 878D83732A1F33C600D7F004 /* bkoolbike.cpp */, + 878D83722A1F33C600D7F004 /* bkoolbike.h */, + 872DCC382A18D4A800EC9F68 /* virtualdevice.cpp */, + 872DCC372A18D4A800EC9F68 /* virtualdevice.h */, + 87BCE6BC29F28F72001F70EB /* ypooelliptical.cpp */, + 87BCE6BB29F28F72001F70EB /* ypooelliptical.h */, + 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */, + 8775008229E876F7008E48B7 /* iconceptelliptical.h */, + 87943AB229E0215D007575F2 /* localipaddress.cpp */, + 87943AB329E0215D007575F2 /* localipaddress.h */, + 8710706B29C48AEA0094D0F3 /* handleurl.cpp */, + 8710706A29C48AE90094D0F3 /* handleurl.h */, + 87D4693529B64D8100C9A382 /* ios_app_delegate.mm */, + 87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */, + 87F4FB5929D550BF0061BB4A /* schwinn170bike.h */, + 87B187B929B8C552007EEF9D /* ziprotreadmill.cpp */, + 87B187BA29B8C552007EEF9D /* ziprotreadmill.h */, + 87A0770F29B641D500A368BF /* wahookickrheadwind.cpp */, + 87A0770E29B641D500A368BF /* wahookickrheadwind.h */, 874D271E29AFA11F0007C079 /* apexbike.cpp */, 874D271F29AFA11F0007C079 /* apexbike.h */, 87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */, @@ -2158,6 +2245,7 @@ 68418A2D51B69FC30BF5A41C /* virtualbike.h */, 35E903698E72424585D33829 /* virtualtreadmill.h */, C8CE72E7B224D8B886614E3F /* domyosbike.h */, + 8710707229C4A5E70094D0F3 /* GarminConnect.swift */, ); name = Sources; sourceTree = ""; @@ -2250,6 +2338,7 @@ AF39DD055C3EF8226FBE929D /* Frameworks */ = { isa = PBXGroup; children = ( + 873D388A29B0D744006A2611 /* ConnectIQ.xcframework */, 879F74142893D732009A64C8 /* CoreMedia.framework */, 879F74122893D705009A64C8 /* CoreVideo.framework */, 879F74102893D5B7009A64C8 /* libqavfcamera.a */, @@ -2860,6 +2949,7 @@ 87D269A425F535340076AA48 /* moc_m3ibike.cpp in Compile Sources */, EA780CE97E201242E33E6EEE /* bike.cpp in Compile Sources */, 8738249827E646E3004F1B46 /* dirconmanager.cpp in Compile Sources */, + 87D4693629B64D8100C9A382 /* ios_app_delegate.mm in Compile Sources */, 87C5F0D926285E7E0067A1B5 /* moc_mimeattachment.cpp in Compile Sources */, 87B617F225F260150094A1CB /* moc_fitshowtreadmill.cpp in Compile Sources */, 87473A9627ECA9EE00C203F5 /* proformrower.cpp in Compile Sources */, @@ -2914,12 +3004,14 @@ 873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */, DD5ED224478CB82859C61B9F /* fit_buffered_record_mesg_broadcaster.cpp in Compile Sources */, 87368825259C602800C71C7E /* watchAppStart.swift in Compile Sources */, + 87BCE6BF29F28F95001F70EB /* moc_ypooelliptical.cpp in Compile Sources */, 876ED21925C3E9000065F3DC /* moc_ftmsbike.cpp in Compile Sources */, 87C5F0BD26285E5F0067A1B5 /* chronobike.cpp in Compile Sources */, 872A20DA28C5EC380037774D /* faketreadmill.cpp in Compile Sources */, 87900DC8268B673C000CB351 /* moc_renphobike.cpp in Compile Sources */, 87CC3B9E25A08812001EC5A8 /* moc_elliptical.cpp in Compile Sources */, 876BFC9C27BE35C5001D7645 /* proformelliptical.cpp in Compile Sources */, + 878D83762A1F33D900D7F004 /* moc_bkoolbike.cpp in Compile Sources */, E7F190E59DC975BA4CA65F0C /* fit_crc.cpp in Compile Sources */, 87A18F092660D5D9002D7C96 /* moc_ftmsrower.cpp in Compile Sources */, DA1DC0B761BD7A3004BCF43D /* fit_date_time.cpp in Compile Sources */, @@ -2947,8 +3039,11 @@ 87646C2227B5065100F82131 /* moc_bhfitnesselliptical.cpp in Compile Sources */, 873824B127E64706004F1B46 /* moc_browser_p.cpp in Compile Sources */, 873824B627E64707004F1B46 /* moc_hostname_p.cpp in Compile Sources */, + 87A0D7542A3A4547005147F2 /* moc_fakerower.cpp in Compile Sources */, 873824EA27E647A8004F1B46 /* browser.cpp in Compile Sources */, 87E34C2B2886F95400CEDE4B /* octanetreadmill.cpp in Compile Sources */, + 87A0D7522A3A4518005147F2 /* fakerower.cpp in Compile Sources */, + 87B187BB29B8C552007EEF9D /* ziprotreadmill.cpp in Compile Sources */, 87BF116D298E28CA00B5B6E7 /* pelotonbike.cpp in Compile Sources */, 87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */, 87D91F9C2800B9B90026D43C /* moc_proformwifibike.cpp in Compile Sources */, @@ -2977,6 +3072,7 @@ 876ED21625C3E8DE0065F3DC /* schwinnic4bike.cpp in Compile Sources */, 879E5AAA289C05A500FEA38A /* moc_proformwifitreadmill.cpp in Compile Sources */, 87D269A325F535340076AA48 /* moc_skandikawiribike.cpp in Compile Sources */, + 87F4FB5C29D550E00061BB4A /* moc_schwinn170bike.cpp in Compile Sources */, 87C5F0C226285E5F0067A1B5 /* emailaddress.cpp in Compile Sources */, 873824AF27E64706004F1B46 /* moc_characteristicwriteprocessor2ad9.cpp in Compile Sources */, 25F2400F80DAFBD41FE5CC75 /* fit_field.cpp in Compile Sources */, @@ -3003,12 +3099,14 @@ FE77C778768741F1A161682E /* fit_mesg_definition.cpp in Compile Sources */, 875F69B926342E8D0009FD78 /* spirittreadmill.cpp in Compile Sources */, 87DF68B825E2673B00FCDA46 /* eslinkertreadmill.cpp in Compile Sources */, + 87BCE6BD29F28F72001F70EB /* ypooelliptical.cpp in Compile Sources */, 87C7074327E4CF5900E79C46 /* keepbike.cpp in Compile Sources */, 878C9E6928B77E7C00669129 /* nordictrackifitadbbike.cpp in Compile Sources */, 8798C8892733E10E003148B3 /* moc_strydrunpowersensor.cpp in Compile Sources */, 8781907E2615089D0085E656 /* peloton.cpp in Compile Sources */, 2B800DC34C91D8B080DEFBE8 /* fit_mesg_with_event_broadcaster.cpp in Compile Sources */, 6DC5D7C695B8763F9E2E029F /* fit_profile.cpp in Compile Sources */, + 8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */, 87C5F0B726285E5F0067A1B5 /* mimecontentformatter.cpp in Compile Sources */, 23191C28CB29474279752FD3 /* fit_protocol_validator.cpp in Compile Sources */, 275D55B5D956B2E5F1B7E46E /* fit_unicode.cpp in Compile Sources */, @@ -3017,6 +3115,7 @@ 87061399286D8D6500D2446E /* moc_wobjectdefs.cpp in Compile Sources */, 873824BA27E64707004F1B46 /* moc_server_p.cpp in Compile Sources */, 87958F1927628D4500124B24 /* elitesterzosmart.cpp in Compile Sources */, + 87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */, 87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */, ACB47DC464A2BC9D39C544AD /* gpx.cpp in Compile Sources */, 6361329E515248BB41640C07 /* homeform.cpp in Compile Sources */, @@ -3044,11 +3143,13 @@ E40895A73216AC52D35083D9 /* signalhandler.cpp in Compile Sources */, 873CD22427EF8E18000131BC /* inappproductqmltype.cpp in Compile Sources */, 87DF68BF25E2675100FCDA46 /* moc_schwinnic4bike.cpp in Compile Sources */, + 8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */, BE1D17BBF32F04829E1B3767 /* toorxtreadmill.cpp in Compile Sources */, 879A38C8281BD83300F78B2A /* characteristicnotifier2ad9.cpp in Compile Sources */, 4697729B15991E98D6A2533D /* treadmill.cpp in Compile Sources */, 87C481FC26DFA7D1006211AD /* moc_eliterizer.cpp in Compile Sources */, 20AA270C9F447F42F5DC2FF2 /* trainprogram.cpp in Compile Sources */, + 8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */, 87F93429278E0ECF0088B596 /* moc_domyosrower.cpp in Compile Sources */, 87C5F0C026285E5F0067A1B5 /* mimepart.cpp in Compile Sources */, 47E45EE0BB22C1E4332F1D1D /* trxappgateusbtreadmill.cpp in Compile Sources */, @@ -3066,6 +3167,7 @@ 87C5F0CF26285E7E0067A1B5 /* moc_mimepart.cpp in Compile Sources */, 87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */, 87182A0B276BBB1200141463 /* moc_virtualrower.cpp in Compile Sources */, + 872DCC3B2A18D4C000EC9F68 /* moc_virtualdevice.cpp in Compile Sources */, 0317752B0C295CAB82D37E45 /* virtualtreadmill.cpp in Compile Sources */, 8742C2B427C92C48007D3FA0 /* moc_wahookickrsnapbike.cpp in Compile Sources */, 878531692711A3EC004B153D /* moc_fakebike.cpp in Compile Sources */, @@ -3074,12 +3176,14 @@ 87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */, 87819080261508B10085E656 /* moc_peloton.cpp in Compile Sources */, 7EC1321DD83EAAFAA2B7109C /* domyosbike.cpp in Compile Sources */, + 8775008529E87713008E48B7 /* moc_iconceptelliptical.cpp in Compile Sources */, 614192CB787D12C3E98ADE55 /* lockscreen.mm in Compile Sources */, 87A0C4BB262329A600121A76 /* npecablebike.cpp in Compile Sources */, 873CD22D27EF8E4B000131BC /* iosinapppurchaseproduct.mm in Compile Sources */, 873CD20727EF8D8A000131BC /* inappproduct.cpp in Compile Sources */, 87A3BC26265642A300D302E3 /* moc_rower.cpp in Compile Sources */, 87EFE45B27A51901006EA1C3 /* moc_nautiluselliptical.cpp in Compile Sources */, + 872DCC392A18D4A800EC9F68 /* virtualdevice.cpp in Compile Sources */, 0F974CB18B3E792B42270F19 /* FitDecode.mm in Compile Sources */, 87440FBF2640292900E4DC0B /* moc_fitplusbike.cpp in Compile Sources */, 87B617EC25F25FED0094A1CB /* screencapture.cpp in Compile Sources */, @@ -3119,6 +3223,7 @@ 87D2699F25F535200076AA48 /* m3ibike.cpp in Compile Sources */, 87917A7528E768D200F8D9AC /* Connection.swift in Compile Sources */, 87FFA13727BBE3FF00924E4E /* solebike.cpp in Compile Sources */, + 87F4FB5A29D550C00061BB4A /* schwinn170bike.cpp in Compile Sources */, 7352E0F0EE5366AC809B9D64 /* qrc_qml.cpp in Compile Sources */, 873824E727E647A8004F1B46 /* record.cpp in Compile Sources */, B38F3288D4AE4025465C1953 /* moc_bike.cpp in Compile Sources */, @@ -3154,6 +3259,7 @@ E8B499F921FB0AB55C7A8A8B /* moc_gpx.cpp in Compile Sources */, 87E6A85825B5C88E00371D28 /* moc_flywheelbike.cpp in Compile Sources */, 8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */, + 878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */, 873824B727E64707004F1B46 /* moc_characteristicwriteprocessor.cpp in Compile Sources */, 87310B1F266FBB59008BA0D6 /* homefitnessbuddy.cpp in Compile Sources */, 140BAAA8823E05940EF35A38 /* moc_treadmill.cpp in Compile Sources */, @@ -3170,9 +3276,12 @@ 87D269A025F535200076AA48 /* skandikawiribike.cpp in Compile Sources */, 8738249427E646E3004F1B46 /* characteristicnotifier2a5b.cpp in Compile Sources */, 8768D1FB285081FE00F58E3A /* nordictrackifitadbtreadmill.cpp in Compile Sources */, + 8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */, + 87B187BD29B8C577007EEF9D /* moc_ziprotreadmill.cpp in Compile Sources */, 877FBA2B276E684E00F6C0C9 /* moc_bowflextreadmill.cpp in Compile Sources */, 874D272229AFA13B0007C079 /* moc_apexbike.cpp in Compile Sources */, 8738248127E646C1004F1B46 /* characteristicnotifier2ad2.cpp in Compile Sources */, + 87A0771029B641D600A368BF /* wahookickrheadwind.cpp in Compile Sources */, 8791A8AB25C861BD003B50B2 /* inspirebike.cpp in Compile Sources */, 876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */, 871235C126B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp in Compile Sources */, @@ -3200,6 +3309,7 @@ 74C43649C9C4E2E5F9378019 /* moc_domyosbike.cpp in Compile Sources */, 87E0761D277A081A00FDA0F9 /* technogymmyruntreadmillrfcomm.cpp in Compile Sources */, 873824B327E64707004F1B46 /* moc_dirconprocessor.cpp in Compile Sources */, + 87A0771229B6420200A368BF /* moc_wahookickrheadwind.cpp in Compile Sources */, 87EB918827EE5FE7002535E1 /* moc_inappstoreqmltype.cpp in Compile Sources */, 87083D9626678EFA0072410D /* zwiftworkout.cpp in Compile Sources */, 87C5F0B826285E5F0067A1B5 /* stagesbike.cpp in Compile Sources */, @@ -3552,11 +3662,11 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; "ARCHS[sdk=iphoneos*]" = arm64; - "ARCHS[sdk=iphonesimulator*]" = x86_64; + "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 508; + CURRENT_PROJECT_VERSION = 653; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -3631,7 +3741,7 @@ /Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats, /Users/cagnulein/Qt/5.15.2/ios/plugins/audio, ); - MARKETING_VERSION = 2.13; + MARKETING_VERSION = 2.16; OTHER_CFLAGS = ( "-pipe", "-g", @@ -3720,11 +3830,11 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; "ARCHS[sdk=iphoneos*]" = arm64; - "ARCHS[sdk=iphonesimulator*]" = x86_64; + "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 508; + CURRENT_PROJECT_VERSION = 653; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; @@ -3801,7 +3911,7 @@ /Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats, /Users/cagnulein/Qt/5.15.2/ios/plugins/audio, ); - MARKETING_VERSION = 2.13; + MARKETING_VERSION = 2.16; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-pipe", @@ -3928,7 +4038,7 @@ CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 508; + CURRENT_PROJECT_VERSION = 653; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3948,8 +4058,12 @@ GCC_WARN_UNUSED_VARIABLE = YES; IBSC_MODULE = watchkit_Extension; INFOPLIST_FILE = watchkit/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MARKETING_VERSION = 2.13; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MARKETING_VERSION = 2.16; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4020,7 +4134,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 508; + CURRENT_PROJECT_VERSION = 653; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; @@ -4036,8 +4150,12 @@ GCC_WARN_UNUSED_VARIABLE = YES; IBSC_MODULE = watchkit_Extension; INFOPLIST_FILE = watchkit/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MARKETING_VERSION = 2.13; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MARKETING_VERSION = 2.16; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4057,8 +4175,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "watchkit-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VALIDATE_PRODUCT = YES; @@ -4107,7 +4226,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 508; + CURRENT_PROJECT_VERSION = 653; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; ENABLE_PREVIEWS = YES; @@ -4147,8 +4266,12 @@ ../../Qt/5.15.2/ios/include/QtMultimedia, ); INFOPLIST_FILE = "watchkit Extension/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 2.13; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.16; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4217,7 +4340,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 508; + CURRENT_PROJECT_VERSION = 653; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; @@ -4253,8 +4376,12 @@ ../../Qt/5.15.2/ios/include/QtMultimedia, ); INFOPLIST_FILE = "watchkit Extension/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 2.13; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.16; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4274,7 +4401,8 @@ PRODUCT_NAME = "${TARGET_NAME}"; SDKROOT = watchos; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VALIDATE_PRODUCT = YES; diff --git a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/xcshareddata/xcschemes/watchkit.xcscheme b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/xcshareddata/xcschemes/watchkit.xcscheme index b366fd19f..3f5882349 100644 --- a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/xcshareddata/xcschemes/watchkit.xcscheme +++ b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj/xcshareddata/xcschemes/watchkit.xcscheme @@ -87,8 +87,6 @@ diff --git a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/MainController.swift b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/MainController.swift index 40442a0ac..118453ef0 100644 --- a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/MainController.swift +++ b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/MainController.swift @@ -104,6 +104,9 @@ extension MainController: WorkoutTrackingDelegate { "\(heartRate)" as AnyObject]) WorkoutTracking.distance = WatchKitConnection.distance WorkoutTracking.kcal = WatchKitConnection.kcal + WorkoutTracking.speed = WatchKitConnection.speed + WorkoutTracking.power = WatchKitConnection.power + WorkoutTracking.cadence = WatchKitConnection.cadence if Locale.current.measurementSystem != "Metric" { self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance))") diff --git a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchKitConnection.swift b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchKitConnection.swift index 6891f636a..d8b9e7958 100644 --- a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchKitConnection.swift +++ b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchKitConnection.swift @@ -24,6 +24,9 @@ class WatchKitConnection: NSObject { public static var distance = 0.0 public static var kcal = 0.0 public static var stepCadence = 0 + public static var speed = 0.0 + public static var cadence = 0.0 + public static var power = 0.0 weak var delegate: WatchKitConnectionDelegate? private override init() { @@ -66,6 +69,13 @@ extension WatchKitConnection: WatchKitConnectionProtocol { WatchKitConnection.distance = dDistance let dKcal = Double(result["kcal"] as! Double) WatchKitConnection.kcal = dKcal + + let dSpeed = Double(result["speed"] as! Double) + WatchKitConnection.speed = dSpeed + let dPower = Double(result["power"] as! Double) + WatchKitConnection.power = dPower + let dCadence = Double(result["cadence"] as! Double) + WatchKitConnection.cadence = dCadence }, errorHandler: { (error) in print(error) }) diff --git a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchWorkoutTracking.swift b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchWorkoutTracking.swift index a23aa5cc0..053886591 100755 --- a/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchWorkoutTracking.swift +++ b/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/watchkit Extension/WatchWorkoutTracking.swift @@ -31,6 +31,10 @@ class WorkoutTracking: NSObject { public static var cadenceTimeStamp = NSDate().timeIntervalSince1970 public static var cadenceLastSteps = Double() public static var cadenceSteps = 0 + public static var speed = Double() + public static var power = Double() + public static var cadence = Double() + public static var lastDateMetric = Date() var sport: Int = 0 let healthStore = HKHealthStore() let configuration = HKWorkoutConfiguration() @@ -146,14 +150,31 @@ extension WorkoutTracking: WorkoutTrackingProtocol { HKSampleType.workoutType() ]) - let infoToShare = Set([ - HKSampleType.quantityType(forIdentifier: .stepCount)!, - HKSampleType.quantityType(forIdentifier: .heartRate)!, - HKSampleType.quantityType(forIdentifier: .distanceCycling)!, - HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!, - HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!, - HKSampleType.workoutType() - ]) + var infoToShare: Set = [] + + if #available(watchOSApplicationExtension 10.0, *) { + infoToShare = Set([ + HKSampleType.quantityType(forIdentifier: .stepCount)!, + HKSampleType.quantityType(forIdentifier: .heartRate)!, + HKSampleType.quantityType(forIdentifier: .distanceCycling)!, + HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!, + HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!, + HKSampleType.quantityType(forIdentifier: .cyclingPower)!, + HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!, + HKSampleType.quantityType(forIdentifier: .cyclingCadence)!, + HKSampleType.workoutType() + ]) + } else { + // Fallback on earlier versions + infoToShare = Set([ + HKSampleType.quantityType(forIdentifier: .stepCount)!, + HKSampleType.quantityType(forIdentifier: .heartRate)!, + HKSampleType.quantityType(forIdentifier: .distanceCycling)!, + HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!, + HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!, + HKSampleType.workoutType() + ]) + } HKHealthStore().requestAuthorization(toShare: infoToShare, read: infoToRead) { (success, error) in if success { @@ -168,6 +189,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol { } func startWorkOut() { + WorkoutTracking.lastDateMetric = Date() print("Start workout") configWorkout() workoutSession.startActivity(with: Date()) @@ -312,6 +334,68 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate { handleSendStatisticsData(statistics) } } + + if #available(watchOSApplicationExtension 10.0, *) { + let wattPerInterval = HKQuantity(unit: HKUnit.watt(), + doubleValue: WorkoutTracking.power) + + if(WorkoutTracking.lastDateMetric.distance(to: Date()) < 1) { + return + } + + guard let powerType = HKQuantityType.quantityType( + forIdentifier: .cyclingPower) else { + return + } + let wattPerIntervalSample = HKQuantitySample(type: powerType, + quantity: wattPerInterval, + start: WorkoutTracking.lastDateMetric, + end: Date()) + workoutBuilder.add([wattPerIntervalSample]) {(success, error) in + if let error = error { + print(error) + } + } + + let cadencePerInterval = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.second()), + doubleValue: WorkoutTracking.cadence / 60.0) + + guard let cadenceType = HKQuantityType.quantityType( + forIdentifier: .cyclingCadence) else { + return + } + let cadencePerIntervalSample = HKQuantitySample(type: cadenceType, + quantity: cadencePerInterval, + start: WorkoutTracking.lastDateMetric, + end: Date()) + workoutBuilder.add([cadencePerIntervalSample]) {(success, error) in + if let error = error { + print(error) + } + } + + let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()), + doubleValue: WorkoutTracking.speed * 0.277778) + + guard let speedType = HKQuantityType.quantityType( + forIdentifier: .cyclingSpeed) else { + return + } + let speedPerIntervalSample = HKQuantitySample(type: speedType, + quantity: speedPerInterval, + start: WorkoutTracking.lastDateMetric, + end: Date()) + workoutBuilder.add([speedPerIntervalSample]) {(success, error) in + if let error = error { + print(error) + } + } + + } else { + // Fallback on earlier versions + } + + WorkoutTracking.lastDateMetric = Date() } func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) { diff --git a/defaults.pri b/defaults.pri index 883b398eb..807f6485c 100644 --- a/defaults.pri +++ b/defaults.pri @@ -12,3 +12,4 @@ ANDROID_PACKAGE_SOURCE_DIR = $$PWD/src/android ANDROID_ABIS = armeabi-v7a arm64-v8a x86 x86_64 +#QMAKE_CXXFLAGS += -Werror=suggest-override diff --git a/docs/10_Installation.md b/docs/10_Installation.md index bc1564b57..aee8c9b2e 100644 --- a/docs/10_Installation.md +++ b/docs/10_Installation.md @@ -77,7 +77,7 @@ Apply the changes `sudo systemctl restart dhcpcd.service` and ensure you have in #### Enable SSH access -You might want to access your raspberry remotely while it is attached to your fitness equipement. +You might want to access your raspberry remotely while it is attached to your fitness equipment. `sudo raspi-config` > `Interface Options` > `SSH` @@ -175,7 +175,7 @@ Then reboot to check operations (`sudo reboot`) ### (optional) Enable overlay FS -Once that everything is working as expected, and if you dedicate your raspeberry pi to this usage, you might want to enable the read-only overlay FS. +Once that everything is working as expected, and if you dedicate your Raspberry pi to this usage, you might want to enable the read-only overlay FS. By enabling the overlay read-only system, your SD card will be read-only only and every file written will be to RAM. Then at each reboot the RAM is erased and you'll revert to the initial status of the overlay file-system. diff --git a/docs/30_usage.md b/docs/30_usage.md index 528c8ef7d..24ec34bee 100644 --- a/docs/30_usage.md +++ b/docs/30_usage.md @@ -18,7 +18,7 @@ Please refer to this article for more information under [QML Operations](https:/ ## Configuration in NativeQT mode -This is the list of settings available in the application. These settings needs to be appended to the binary command line. +This is the list of settings available in the application. These settings need to be appended to the binary command line. *Example :* `sudo ./qdomyos-zwift -no-gui` for disabling any graphical interface. | **Option** | **Type** | **Default** | **Function** | @@ -35,8 +35,8 @@ This is the list of settings available in the application. These settings needs | -heart-service | Boolean | True | Simulate HR service (required for applications not reading FTMS) | | -only-virtualbike | Boolean | False | | | -only-virtualtreadmill | Boolean | False | | -| -no-reconnection | Boolean | False | QZ will not try to reconnect your fitness equipement if enabled | -| -bluetooth-relaxed | Boolean | False | In case of deconnections from QZ to your fitness equipement | +| -no-reconnection | Boolean | False | QZ will not try to reconnect your fitness equipment if enabled | +| -bluetooth-relaxed | Boolean | False | In case of deconnections from QZ to your fitness equipment | | -bike-cadence-sensor | Boolean | False | | | -bike-power-sensor | Boolean | False | | | -battery-service | Boolean | False | | @@ -45,7 +45,7 @@ This is the list of settings available in the application. These settings needs | -run-cadence-sensor | Boolean | False | | | -nordictrack-10-treadmill | Boolean | False | Enable NordicTrack compatibility mode | | -train | String | | Force training program | -| -name | String | | Force bluetooth device name (if QZ struggles finding your fitness equipment) | +| -name | String | | Force bluetooth device name (if QZ struggles to find your fitness equipment) | | -poll-device-time | Int | 200 (ms) | Frequency to refresh information from QZ to Fitness equipment | | -bike-resistance-gain | Int | | Adjust resistance from the fitness application | | -bike-resistance-offset | Int | | Set another resistance point than default | diff --git a/qHttpServerBin/5.15.2/headers/http_parser.c b/qHttpServerBin/5.15.2/headers/http_parser.c new file mode 100644 index 000000000..9be003e73 --- /dev/null +++ b/qHttpServerBin/5.15.2/headers/http_parser.c @@ -0,0 +1,2575 @@ +/* Copyright Joyent, Inc. and other Node contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include "http_parser.h" +#include +#include +#include +#include +#include + +static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE; + +#ifndef ULLONG_MAX +# define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ +#endif + +#ifndef MIN +# define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef ARRAY_SIZE +# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#endif + +#ifndef BIT_AT +# define BIT_AT(a, i) \ + (!!((unsigned int) (a)[(unsigned int) (i) >> 3] & \ + (1 << ((unsigned int) (i) & 7)))) +#endif + +#ifndef ELEM_AT +# define ELEM_AT(a, i, v) ((unsigned int) (i) < ARRAY_SIZE(a) ? (a)[(i)] : (v)) +#endif + +#define SET_ERRNO(e) \ +do { \ + parser->nread = nread; \ + parser->http_errno = (e); \ +} while(0) + +#define CURRENT_STATE() p_state +#define UPDATE_STATE(V) p_state = (enum state) (V); +#define RETURN(V) \ +do { \ + parser->nread = nread; \ + parser->state = CURRENT_STATE(); \ + return (V); \ +} while (0); +#define REEXECUTE() \ + goto reexecute; \ + + +#ifdef __GNUC__ +# define LIKELY(X) __builtin_expect(!!(X), 1) +# define UNLIKELY(X) __builtin_expect(!!(X), 0) +#else +# define LIKELY(X) (X) +# define UNLIKELY(X) (X) +#endif + + +/* Run the notify callback FOR, returning ER if it fails */ +#define CALLBACK_NOTIFY_(FOR, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != settings->on_##FOR(parser))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ +} while (0) + +/* Run the notify callback FOR and consume the current byte */ +#define CALLBACK_NOTIFY(FOR) CALLBACK_NOTIFY_(FOR, p - data + 1) + +/* Run the notify callback FOR and don't consume the current byte */ +#define CALLBACK_NOTIFY_NOADVANCE(FOR) CALLBACK_NOTIFY_(FOR, p - data) + +/* Run data callback FOR with LEN bytes, returning ER if it fails */ +#define CALLBACK_DATA_(FOR, LEN, ER) \ +do { \ + assert(HTTP_PARSER_ERRNO(parser) == HPE_OK); \ + \ + if (FOR##_mark) { \ + if (LIKELY(settings->on_##FOR)) { \ + parser->state = CURRENT_STATE(); \ + if (UNLIKELY(0 != \ + settings->on_##FOR(parser, FOR##_mark, (LEN)))) { \ + SET_ERRNO(HPE_CB_##FOR); \ + } \ + UPDATE_STATE(parser->state); \ + \ + /* We either errored above or got paused; get out */ \ + if (UNLIKELY(HTTP_PARSER_ERRNO(parser) != HPE_OK)) { \ + return (ER); \ + } \ + } \ + FOR##_mark = NULL; \ + } \ +} while (0) + +/* Run the data callback FOR and consume the current byte */ +#define CALLBACK_DATA(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) + +/* Run the data callback FOR and don't consume the current byte */ +#define CALLBACK_DATA_NOADVANCE(FOR) \ + CALLBACK_DATA_(FOR, p - FOR##_mark, p - data) + +/* Set the mark FOR; non-destructive if mark is already set */ +#define MARK(FOR) \ +do { \ + if (!FOR##_mark) { \ + FOR##_mark = p; \ + } \ +} while (0) + +/* Don't allow the total size of the HTTP headers (including the status + * line) to exceed max_header_size. This check is here to protect + * embedders against denial-of-service attacks where the attacker feeds + * us a never-ending header that the embedder keeps buffering. + * + * This check is arguably the responsibility of embedders but we're doing + * it on the embedder's behalf because most won't bother and this way we + * make the web a little safer. max_header_size is still far bigger + * than any reasonable request or response so this should never affect + * day-to-day operation. + */ +#define COUNT_HEADER_SIZE(V) \ +do { \ + nread += (uint32_t)(V); \ + if (UNLIKELY(nread > max_header_size)) { \ + SET_ERRNO(HPE_HEADER_OVERFLOW); \ + goto error; \ + } \ +} while (0) + + +#define PROXY_CONNECTION "proxy-connection" +#define CONNECTION "connection" +#define CONTENT_LENGTH "content-length" +#define TRANSFER_ENCODING "transfer-encoding" +#define UPGRADE "upgrade" +#define CHUNKED "chunked" +#define KEEP_ALIVE "keep-alive" +#define CLOSE "close" + + +static const char *method_strings[] = + { +#define XX(num, name, string) #string, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +static const char tokens[256] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0, 0, 0, 0, 0, 0, 0, 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + ' ', '!', 0, '#', '$', '%', '&', '\'', +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 0, 0, '*', '+', 0, '-', '.', 0, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + '0', '1', '2', '3', '4', '5', '6', '7', +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + '8', '9', 0, 0, 0, 0, 0, 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 'x', 'y', 'z', 0, 0, 0, '^', '_', +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 'x', 'y', 'z', 0, '|', 0, '~', 0 }; + + +static const int8_t unhex[256] = + {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + , 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; + + +#if HTTP_PARSER_STRICT +# define T(v) 0 +#else +# define T(v) v +#endif + + +static const uint8_t normal_url_char[32] = { +/* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ + 0 | T(2) | 0 | 0 | T(16) | 0 | 0 | 0, +/* 16 dle 17 dc1 18 dc2 19 dc3 20 dc4 21 nak 22 syn 23 etb */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ + 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0, +/* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ + 0 | 2 | 4 | 0 | 16 | 32 | 64 | 128, +/* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, +/* 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128, +/* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ + 1 | 2 | 4 | 8 | 16 | 32 | 64 | 0, }; + +#undef T + +enum state + { s_dead = 1 /* important that this is > 0 */ + + , s_start_req_or_res + , s_res_or_resp_H + , s_start_res + , s_res_H + , s_res_HT + , s_res_HTT + , s_res_HTTP + , s_res_http_major + , s_res_http_dot + , s_res_http_minor + , s_res_http_end + , s_res_first_status_code + , s_res_status_code + , s_res_status_start + , s_res_status + , s_res_line_almost_done + + , s_start_req + + , s_req_method + , s_req_spaces_before_url + , s_req_schema + , s_req_schema_slash + , s_req_schema_slash_slash + , s_req_server_start + , s_req_server + , s_req_server_with_at + , s_req_path + , s_req_query_string_start + , s_req_query_string + , s_req_fragment_start + , s_req_fragment + , s_req_http_start + , s_req_http_H + , s_req_http_HT + , s_req_http_HTT + , s_req_http_HTTP + , s_req_http_I + , s_req_http_IC + , s_req_http_major + , s_req_http_dot + , s_req_http_minor + , s_req_http_end + , s_req_line_almost_done + + , s_header_field_start + , s_header_field + , s_header_value_discard_ws + , s_header_value_discard_ws_almost_done + , s_header_value_discard_lws + , s_header_value_start + , s_header_value + , s_header_value_lws + + , s_header_almost_done + + , s_chunk_size_start + , s_chunk_size + , s_chunk_parameters + , s_chunk_size_almost_done + + , s_headers_almost_done + , s_headers_done + + /* Important: 's_headers_done' must be the last 'header' state. All + * states beyond this must be 'body' states. It is used for overflow + * checking. See the PARSING_HEADER() macro. + */ + + , s_chunk_data + , s_chunk_data_almost_done + , s_chunk_data_done + + , s_body_identity + , s_body_identity_eof + + , s_message_done + }; + + +#define PARSING_HEADER(state) (state <= s_headers_done) + + +enum header_states + { h_general = 0 + , h_C + , h_CO + , h_CON + + , h_matching_connection + , h_matching_proxy_connection + , h_matching_content_length + , h_matching_transfer_encoding + , h_matching_upgrade + + , h_connection + , h_content_length + , h_content_length_num + , h_content_length_ws + , h_transfer_encoding + , h_upgrade + + , h_matching_transfer_encoding_token_start + , h_matching_transfer_encoding_chunked + , h_matching_transfer_encoding_token + + , h_matching_connection_token_start + , h_matching_connection_keep_alive + , h_matching_connection_close + , h_matching_connection_upgrade + , h_matching_connection_token + + , h_transfer_encoding_chunked + , h_connection_keep_alive + , h_connection_close + , h_connection_upgrade + }; + +enum http_host_state + { + s_http_host_dead = 1 + , s_http_userinfo_start + , s_http_userinfo + , s_http_host_start + , s_http_host_v6_start + , s_http_host + , s_http_host_v6 + , s_http_host_v6_end + , s_http_host_v6_zone_start + , s_http_host_v6_zone + , s_http_host_port_start + , s_http_host_port +}; + +/* Macros for character classes; depends on strict-mode */ +#define CR '\r' +#define LF '\n' +#define LOWER(c) (unsigned char)(c | 0x20) +#define IS_ALPHA(c) (LOWER(c) >= 'a' && LOWER(c) <= 'z') +#define IS_NUM(c) ((c) >= '0' && (c) <= '9') +#define IS_ALPHANUM(c) (IS_ALPHA(c) || IS_NUM(c)) +#define IS_HEX(c) (IS_NUM(c) || (LOWER(c) >= 'a' && LOWER(c) <= 'f')) +#define IS_MARK(c) ((c) == '-' || (c) == '_' || (c) == '.' || \ + (c) == '!' || (c) == '~' || (c) == '*' || (c) == '\'' || (c) == '(' || \ + (c) == ')') +#define IS_USERINFO_CHAR(c) (IS_ALPHANUM(c) || IS_MARK(c) || (c) == '%' || \ + (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ + (c) == '$' || (c) == ',') + +#define STRICT_TOKEN(c) ((c == ' ') ? 0 : tokens[(unsigned char)c]) + +#if HTTP_PARSER_STRICT +#define TOKEN(c) STRICT_TOKEN(c) +#define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) +#define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') +#else +#define TOKEN(c) tokens[(unsigned char)c] +#define IS_URL_CHAR(c) \ + (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) +#define IS_HOST_CHAR(c) \ + (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') +#endif + +/** + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + **/ +#define IS_HEADER_CHAR(ch) \ + (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127)) + +#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) + + +#if HTTP_PARSER_STRICT +# define STRICT_CHECK(cond) \ +do { \ + if (cond) { \ + SET_ERRNO(HPE_STRICT); \ + goto error; \ + } \ +} while (0) +# define NEW_MESSAGE() (http_should_keep_alive(parser) ? start_state : s_dead) +#else +# define STRICT_CHECK(cond) +# define NEW_MESSAGE() start_state +#endif + + +/* Map errno values to strings for human-readable output */ +#define HTTP_STRERROR_GEN(n, s) { "HPE_" #n, s }, +static struct { + const char *name; + const char *description; +} http_strerror_tab[] = { + HTTP_ERRNO_MAP(HTTP_STRERROR_GEN) +}; +#undef HTTP_STRERROR_GEN + +int http_message_needs_eof(const http_parser *parser); + +/* Our URL parser. + * + * This is designed to be shared by http_parser_execute() for URL validation, + * hence it has a state transition + byte-for-byte interface. In addition, it + * is meant to be embedded in http_parser_parse_url(), which does the dirty + * work of turning state transitions URL components for its API. + * + * This function should only be invoked with non-space characters. It is + * assumed that the caller cares about (and can detect) the transition between + * URL and non-URL states by looking for these. + */ +static enum state +parse_url_char(enum state s, const char ch) +{ + if (ch == ' ' || ch == '\r' || ch == '\n') { + return s_dead; + } + +#if HTTP_PARSER_STRICT + if (ch == '\t' || ch == '\f') { + return s_dead; + } +#endif + + switch (s) { + case s_req_spaces_before_url: + /* Proxied requests are followed by scheme of an absolute URI (alpha). + * All methods except CONNECT are followed by '/' or '*'. + */ + + if (ch == '/' || ch == '*') { + return s_req_path; + } + + if (IS_ALPHA(ch)) { + return s_req_schema; + } + + break; + + case s_req_schema: + if (IS_ALPHA(ch)) { + return s; + } + + if (ch == ':') { + return s_req_schema_slash; + } + + break; + + case s_req_schema_slash: + if (ch == '/') { + return s_req_schema_slash_slash; + } + + break; + + case s_req_schema_slash_slash: + if (ch == '/') { + return s_req_server_start; + } + + break; + + case s_req_server_with_at: + if (ch == '@') { + return s_dead; + } + + /* fall through */ + case s_req_server_start: + case s_req_server: + if (ch == '/') { + return s_req_path; + } + + if (ch == '?') { + return s_req_query_string_start; + } + + if (ch == '@') { + return s_req_server_with_at; + } + + if (IS_USERINFO_CHAR(ch) || ch == '[' || ch == ']') { + return s_req_server; + } + + break; + + case s_req_path: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + return s_req_query_string_start; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_query_string_start: + case s_req_query_string: + if (IS_URL_CHAR(ch)) { + return s_req_query_string; + } + + switch (ch) { + case '?': + /* allow extra '?' in query string */ + return s_req_query_string; + + case '#': + return s_req_fragment_start; + } + + break; + + case s_req_fragment_start: + if (IS_URL_CHAR(ch)) { + return s_req_fragment; + } + + switch (ch) { + case '?': + return s_req_fragment; + + case '#': + return s; + } + + break; + + case s_req_fragment: + if (IS_URL_CHAR(ch)) { + return s; + } + + switch (ch) { + case '?': + case '#': + return s; + } + + break; + + default: + break; + } + + /* We should never fall out of the switch above unless there's an error */ + return s_dead; +} + +size_t http_parser_execute (http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len) +{ + char c, ch; + int8_t unhex_val; + const char *p = data; + const char *header_field_mark = 0; + const char *header_value_mark = 0; + const char *url_mark = 0; + const char *body_mark = 0; + const char *status_mark = 0; + enum state p_state = (enum state) parser->state; + const unsigned int lenient = parser->lenient_http_headers; + const unsigned int allow_chunked_length = parser->allow_chunked_length; + + uint32_t nread = parser->nread; + + /* We're in an error state. Don't bother doing anything. */ + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + return 0; + } + + if (len == 0) { + switch (CURRENT_STATE()) { + case s_body_identity_eof: + /* Use of CALLBACK_NOTIFY() here would erroneously return 1 byte read if + * we got paused. + */ + CALLBACK_NOTIFY_NOADVANCE(message_complete); + return 0; + + case s_dead: + case s_start_req_or_res: + case s_start_res: + case s_start_req: + return 0; + + default: + SET_ERRNO(HPE_INVALID_EOF_STATE); + return 1; + } + } + + + if (CURRENT_STATE() == s_header_field) + header_field_mark = data; + if (CURRENT_STATE() == s_header_value) + header_value_mark = data; + switch (CURRENT_STATE()) { + case s_req_path: + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_server: + case s_req_server_with_at: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + url_mark = data; + break; + case s_res_status: + status_mark = data; + break; + default: + break; + } + + for (p=data; p != data + len; p++) { + ch = *p; + + if (PARSING_HEADER(CURRENT_STATE())) + COUNT_HEADER_SIZE(1); + +reexecute: + switch (CURRENT_STATE()) { + + case s_dead: + /* this state is used after a 'Connection: close' message + * the parser will error out if it reads another message + */ + if (LIKELY(ch == CR || ch == LF)) + break; + + SET_ERRNO(HPE_CLOSED_CONNECTION); + goto error; + + case s_start_req_or_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->uses_transfer_encoding = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_or_resp_H); + + CALLBACK_NOTIFY(message_begin); + } else { + parser->type = HTTP_REQUEST; + UPDATE_STATE(s_start_req); + REEXECUTE(); + } + + break; + } + + case s_res_or_resp_H: + if (ch == 'T') { + parser->type = HTTP_RESPONSE; + UPDATE_STATE(s_res_HT); + } else { + if (UNLIKELY(ch != 'E')) { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + parser->type = HTTP_REQUEST; + parser->method = HTTP_HEAD; + parser->index = 2; + UPDATE_STATE(s_req_method); + } + break; + + case s_start_res: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->uses_transfer_encoding = 0; + parser->content_length = ULLONG_MAX; + + if (ch == 'H') { + UPDATE_STATE(s_res_H); + } else { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + + CALLBACK_NOTIFY(message_begin); + break; + } + + case s_res_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HT); + break; + + case s_res_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_res_HTT); + break; + + case s_res_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_res_HTTP); + break; + + case s_res_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_res_http_major); + break; + + case s_res_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_res_http_dot); + break; + + case s_res_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_http_minor); + break; + } + + case s_res_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_res_http_end); + break; + + case s_res_http_end: + { + if (UNLIKELY(ch != ' ')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_res_first_status_code); + break; + } + + case s_res_first_status_code: + { + if (!IS_NUM(ch)) { + if (ch == ' ') { + break; + } + + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + parser->status_code = ch - '0'; + UPDATE_STATE(s_res_status_code); + break; + } + + case s_res_status_code: + { + if (!IS_NUM(ch)) { + switch (ch) { + case ' ': + UPDATE_STATE(s_res_status_start); + break; + case CR: + case LF: + UPDATE_STATE(s_res_status_start); + REEXECUTE(); + break; + default: + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + break; + } + + parser->status_code *= 10; + parser->status_code += ch - '0'; + + if (UNLIKELY(parser->status_code > 999)) { + SET_ERRNO(HPE_INVALID_STATUS); + goto error; + } + + break; + } + + case s_res_status_start: + { + MARK(status); + UPDATE_STATE(s_res_status); + parser->index = 0; + + if (ch == CR || ch == LF) + REEXECUTE(); + + break; + } + + case s_res_status: + if (ch == CR) { + UPDATE_STATE(s_res_line_almost_done); + CALLBACK_DATA(status); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA(status); + break; + } + + break; + + case s_res_line_almost_done: + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_field_start); + break; + + case s_start_req: + { + if (ch == CR || ch == LF) + break; + parser->flags = 0; + parser->uses_transfer_encoding = 0; + parser->content_length = ULLONG_MAX; + + if (UNLIKELY(!IS_ALPHA(ch))) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + parser->method = (enum http_method) 0; + parser->index = 1; + switch (ch) { + case 'A': parser->method = HTTP_ACL; break; + case 'B': parser->method = HTTP_BIND; break; + case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; + case 'D': parser->method = HTTP_DELETE; break; + case 'G': parser->method = HTTP_GET; break; + case 'H': parser->method = HTTP_HEAD; break; + case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; + case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; + case 'N': parser->method = HTTP_NOTIFY; break; + case 'O': parser->method = HTTP_OPTIONS; break; + case 'P': parser->method = HTTP_POST; + /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ + break; + case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; + case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; + case 'T': parser->method = HTTP_TRACE; break; + case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + UPDATE_STATE(s_req_method); + + CALLBACK_NOTIFY(message_begin); + + break; + } + + case s_req_method: + { + const char *matcher; + if (UNLIKELY(ch == '\0')) { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + matcher = method_strings[parser->method]; + if (ch == ' ' && matcher[parser->index] == '\0') { + UPDATE_STATE(s_req_spaces_before_url); + } else if (ch == matcher[parser->index]) { + ; /* nada */ + } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { + + switch (parser->method << 16 | parser->index << 8 | ch) { +#define XX(meth, pos, ch, new_meth) \ + case (HTTP_##meth << 16 | pos << 8 | ch): \ + parser->method = HTTP_##new_meth; break; + + XX(POST, 1, 'U', PUT) + XX(POST, 1, 'A', PATCH) + XX(POST, 1, 'R', PROPFIND) + XX(PUT, 2, 'R', PURGE) + XX(CONNECT, 1, 'H', CHECKOUT) + XX(CONNECT, 2, 'P', COPY) + XX(MKCOL, 1, 'O', MOVE) + XX(MKCOL, 1, 'E', MERGE) + XX(MKCOL, 1, '-', MSEARCH) + XX(MKCOL, 2, 'A', MKACTIVITY) + XX(MKCOL, 3, 'A', MKCALENDAR) + XX(SUBSCRIBE, 1, 'E', SEARCH) + XX(SUBSCRIBE, 1, 'O', SOURCE) + XX(REPORT, 2, 'B', REBIND) + XX(PROPFIND, 4, 'P', PROPPATCH) + XX(LOCK, 1, 'I', LINK) + XX(UNLOCK, 2, 'S', UNSUBSCRIBE) + XX(UNLOCK, 2, 'B', UNBIND) + XX(UNLOCK, 3, 'I', UNLINK) +#undef XX + default: + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + } else { + SET_ERRNO(HPE_INVALID_METHOD); + goto error; + } + + ++parser->index; + break; + } + + case s_req_spaces_before_url: + { + if (ch == ' ') break; + + MARK(url); + if (parser->method == HTTP_CONNECT) { + UPDATE_STATE(s_req_server_start); + } + + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + + break; + } + + case s_req_schema: + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + { + switch (ch) { + /* No whitespace allowed here */ + case ' ': + case CR: + case LF: + SET_ERRNO(HPE_INVALID_URL); + goto error; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + + break; + } + + case s_req_server: + case s_req_server_with_at: + case s_req_path: + case s_req_query_string_start: + case s_req_query_string: + case s_req_fragment_start: + case s_req_fragment: + { + switch (ch) { + case ' ': + UPDATE_STATE(s_req_http_start); + CALLBACK_DATA(url); + break; + case CR: + case LF: + parser->http_major = 0; + parser->http_minor = 9; + UPDATE_STATE((ch == CR) ? + s_req_line_almost_done : + s_header_field_start); + CALLBACK_DATA(url); + break; + default: + UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); + if (UNLIKELY(CURRENT_STATE() == s_dead)) { + SET_ERRNO(HPE_INVALID_URL); + goto error; + } + } + break; + } + + case s_req_http_start: + switch (ch) { + case ' ': + break; + case 'H': + UPDATE_STATE(s_req_http_H); + break; + case 'I': + if (parser->method == HTTP_SOURCE) { + UPDATE_STATE(s_req_http_I); + break; + } + /* fall through */ + default: + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; + } + break; + + case s_req_http_H: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HT); + break; + + case s_req_http_HT: + STRICT_CHECK(ch != 'T'); + UPDATE_STATE(s_req_http_HTT); + break; + + case s_req_http_HTT: + STRICT_CHECK(ch != 'P'); + UPDATE_STATE(s_req_http_HTTP); + break; + + case s_req_http_I: + STRICT_CHECK(ch != 'C'); + UPDATE_STATE(s_req_http_IC); + break; + + case s_req_http_IC: + STRICT_CHECK(ch != 'E'); + UPDATE_STATE(s_req_http_HTTP); /* Treat "ICE" as "HTTP". */ + break; + + case s_req_http_HTTP: + STRICT_CHECK(ch != '/'); + UPDATE_STATE(s_req_http_major); + break; + + case s_req_http_major: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_major = ch - '0'; + UPDATE_STATE(s_req_http_dot); + break; + + case s_req_http_dot: + { + if (UNLIKELY(ch != '.')) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + UPDATE_STATE(s_req_http_minor); + break; + } + + case s_req_http_minor: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + } + + parser->http_minor = ch - '0'; + UPDATE_STATE(s_req_http_end); + break; + + case s_req_http_end: + { + if (ch == CR) { + UPDATE_STATE(s_req_line_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_field_start); + break; + } + + SET_ERRNO(HPE_INVALID_VERSION); + goto error; + break; + } + + /* end of request line */ + case s_req_line_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_field_start); + break; + } + + case s_header_field_start: + { + if (ch == CR) { + UPDATE_STATE(s_headers_almost_done); + break; + } + + if (ch == LF) { + /* they might be just sending \n instead of \r\n so this would be + * the second \n to denote the end of headers*/ + UPDATE_STATE(s_headers_almost_done); + REEXECUTE(); + } + + c = TOKEN(ch); + + if (UNLIKELY(!c)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + MARK(header_field); + + parser->index = 0; + UPDATE_STATE(s_header_field); + + switch (c) { + case 'c': + parser->header_state = h_C; + break; + + case 'p': + parser->header_state = h_matching_proxy_connection; + break; + + case 't': + parser->header_state = h_matching_transfer_encoding; + break; + + case 'u': + parser->header_state = h_matching_upgrade; + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_field: + { + const char* start = p; + for (; p != data + len; p++) { + ch = *p; + c = TOKEN(ch); + + if (!c) + break; + + switch (parser->header_state) { + case h_general: { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + while (p+1 < pe && TOKEN(p[1])) { + p++; + } + break; + } + + case h_C: + parser->index++; + parser->header_state = (c == 'o' ? h_CO : h_general); + break; + + case h_CO: + parser->index++; + parser->header_state = (c == 'n' ? h_CON : h_general); + break; + + case h_CON: + parser->index++; + switch (c) { + case 'n': + parser->header_state = h_matching_connection; + break; + case 't': + parser->header_state = h_matching_content_length; + break; + default: + parser->header_state = h_general; + break; + } + break; + + /* connection */ + + case h_matching_connection: + parser->index++; + if (parser->index > sizeof(CONNECTION)-1 + || c != CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* proxy-connection */ + + case h_matching_proxy_connection: + parser->index++; + if (parser->index > sizeof(PROXY_CONNECTION)-1 + || c != PROXY_CONNECTION[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { + parser->header_state = h_connection; + } + break; + + /* content-length */ + + case h_matching_content_length: + parser->index++; + if (parser->index > sizeof(CONTENT_LENGTH)-1 + || c != CONTENT_LENGTH[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { + parser->header_state = h_content_length; + } + break; + + /* transfer-encoding */ + + case h_matching_transfer_encoding: + parser->index++; + if (parser->index > sizeof(TRANSFER_ENCODING)-1 + || c != TRANSFER_ENCODING[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { + parser->header_state = h_transfer_encoding; + parser->uses_transfer_encoding = 1; + } + break; + + /* upgrade */ + + case h_matching_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE)-1 + || c != UPGRADE[parser->index]) { + parser->header_state = h_general; + } else if (parser->index == sizeof(UPGRADE)-2) { + parser->header_state = h_upgrade; + } + break; + + case h_connection: + case h_content_length: + case h_transfer_encoding: + case h_upgrade: + if (ch != ' ') parser->header_state = h_general; + break; + + default: + assert(0 && "Unknown header_state"); + break; + } + } + + if (p == data + len) { + --p; + COUNT_HEADER_SIZE(p - start); + break; + } + + COUNT_HEADER_SIZE(p - start); + + if (ch == ':') { + UPDATE_STATE(s_header_value_discard_ws); + CALLBACK_DATA(header_field); + break; + } + + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + case s_header_value_discard_ws: + if (ch == ' ' || ch == '\t') break; + + if (ch == CR) { + UPDATE_STATE(s_header_value_discard_ws_almost_done); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + /* fall through */ + + case s_header_value_start: + { + MARK(header_value); + + UPDATE_STATE(s_header_value); + parser->index = 0; + + c = LOWER(ch); + + switch (parser->header_state) { + case h_upgrade: + parser->flags |= F_UPGRADE; + parser->header_state = h_general; + break; + + case h_transfer_encoding: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + parser->header_state = h_matching_transfer_encoding_chunked; + } else { + parser->header_state = h_matching_transfer_encoding_token; + } + break; + + /* Multi-value `Transfer-Encoding` header */ + case h_matching_transfer_encoding_token_start: + break; + + case h_content_length: + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + if (parser->flags & F_CONTENTLENGTH) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + + parser->flags |= F_CONTENTLENGTH; + parser->content_length = ch - '0'; + parser->header_state = h_content_length_num; + break; + + /* when obsolete line folding is encountered for content length + * continue to the s_header_value state */ + case h_content_length_ws: + break; + + case h_connection: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + parser->header_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + parser->header_state = h_matching_connection_close; + } else if (c == 'u') { + parser->header_state = h_matching_connection_upgrade; + } else { + parser->header_state = h_matching_connection_token; + } + break; + + /* Multi-value `Connection` header */ + case h_matching_connection_token_start: + break; + + default: + parser->header_state = h_general; + break; + } + break; + } + + case s_header_value: + { + const char* start = p; + enum header_states h_state = (enum header_states) parser->header_state; + for (; p != data + len; p++) { + ch = *p; + if (ch == CR) { + UPDATE_STATE(s_header_almost_done); + parser->header_state = h_state; + CALLBACK_DATA(header_value); + break; + } + + if (ch == LF) { + UPDATE_STATE(s_header_almost_done); + COUNT_HEADER_SIZE(p - start); + parser->header_state = h_state; + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + + c = LOWER(ch); + + switch (h_state) { + case h_general: + { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + + for (; p != pe; p++) { + ch = *p; + if (ch == CR || ch == LF) { + --p; + break; + } + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + } + if (p == data + len) + --p; + break; + } + + case h_connection: + case h_transfer_encoding: + assert(0 && "Shouldn't get here."); + break; + + case h_content_length: + if (ch == ' ') break; + h_state = h_content_length_num; + /* fall through */ + + case h_content_length_num: + { + uint64_t t; + + if (ch == ' ') { + h_state = h_content_length_ws; + break; + } + + if (UNLIKELY(!IS_NUM(ch))) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + t = parser->content_length; + t *= 10; + t += ch - '0'; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 10) / 10 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + } + + parser->content_length = t; + break; + } + + case h_content_length_ws: + if (ch == ' ') break; + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + + /* Transfer-Encoding: chunked */ + case h_matching_transfer_encoding_token_start: + /* looking for 'Transfer-Encoding: chunked' */ + if ('c' == c) { + h_state = h_matching_transfer_encoding_chunked; + } else if (STRICT_TOKEN(c)) { + /* TODO(indutny): similar code below does this, but why? + * At the very least it seems to be inconsistent given that + * h_matching_transfer_encoding_token does not check for + * `STRICT_TOKEN` + */ + h_state = h_matching_transfer_encoding_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + case h_matching_transfer_encoding_chunked: + parser->index++; + if (parser->index > sizeof(CHUNKED)-1 + || c != CHUNKED[parser->index]) { + h_state = h_matching_transfer_encoding_token; + } else if (parser->index == sizeof(CHUNKED)-2) { + h_state = h_transfer_encoding_chunked; + } + break; + + case h_matching_transfer_encoding_token: + if (ch == ',') { + h_state = h_matching_transfer_encoding_token_start; + parser->index = 0; + } + break; + + case h_matching_connection_token_start: + /* looking for 'Connection: keep-alive' */ + if (c == 'k') { + h_state = h_matching_connection_keep_alive; + /* looking for 'Connection: close' */ + } else if (c == 'c') { + h_state = h_matching_connection_close; + } else if (c == 'u') { + h_state = h_matching_connection_upgrade; + } else if (STRICT_TOKEN(c)) { + h_state = h_matching_connection_token; + } else if (c == ' ' || c == '\t') { + /* Skip lws */ + } else { + h_state = h_general; + } + break; + + /* looking for 'Connection: keep-alive' */ + case h_matching_connection_keep_alive: + parser->index++; + if (parser->index > sizeof(KEEP_ALIVE)-1 + || c != KEEP_ALIVE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(KEEP_ALIVE)-2) { + h_state = h_connection_keep_alive; + } + break; + + /* looking for 'Connection: close' */ + case h_matching_connection_close: + parser->index++; + if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(CLOSE)-2) { + h_state = h_connection_close; + } + break; + + /* looking for 'Connection: upgrade' */ + case h_matching_connection_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE) - 1 || + c != UPGRADE[parser->index]) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(UPGRADE)-2) { + h_state = h_connection_upgrade; + } + break; + + case h_matching_connection_token: + if (ch == ',') { + h_state = h_matching_connection_token_start; + parser->index = 0; + } + break; + + case h_transfer_encoding_chunked: + if (ch != ' ') h_state = h_matching_transfer_encoding_token; + break; + + case h_connection_keep_alive: + case h_connection_close: + case h_connection_upgrade: + if (ch == ',') { + if (h_state == h_connection_keep_alive) { + parser->flags |= F_CONNECTION_KEEP_ALIVE; + } else if (h_state == h_connection_close) { + parser->flags |= F_CONNECTION_CLOSE; + } else if (h_state == h_connection_upgrade) { + parser->flags |= F_CONNECTION_UPGRADE; + } + h_state = h_matching_connection_token_start; + parser->index = 0; + } else if (ch != ' ') { + h_state = h_matching_connection_token; + } + break; + + default: + UPDATE_STATE(s_header_value); + h_state = h_general; + break; + } + } + parser->header_state = h_state; + + if (p == data + len) + --p; + + COUNT_HEADER_SIZE(p - start); + break; + } + + case s_header_almost_done: + { + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } + + UPDATE_STATE(s_header_value_lws); + break; + } + + case s_header_value_lws: + { + if (ch == ' ' || ch == '\t') { + if (parser->header_state == h_content_length_num) { + /* treat obsolete line folding as space */ + parser->header_state = h_content_length_ws; + } + UPDATE_STATE(s_header_value_start); + REEXECUTE(); + } + + /* finished the header */ + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + default: + break; + } + + UPDATE_STATE(s_header_field_start); + REEXECUTE(); + } + + case s_header_value_discard_ws_almost_done: + { + STRICT_CHECK(ch != LF); + UPDATE_STATE(s_header_value_discard_lws); + break; + } + + case s_header_value_discard_lws: + { + if (ch == ' ' || ch == '\t') { + UPDATE_STATE(s_header_value_discard_ws); + break; + } else { + switch (parser->header_state) { + case h_connection_keep_alive: + parser->flags |= F_CONNECTION_KEEP_ALIVE; + break; + case h_connection_close: + parser->flags |= F_CONNECTION_CLOSE; + break; + case h_connection_upgrade: + parser->flags |= F_CONNECTION_UPGRADE; + break; + case h_transfer_encoding_chunked: + parser->flags |= F_CHUNKED; + break; + case h_content_length: + /* do not allow empty content length */ + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + break; + default: + break; + } + + /* header value was empty */ + MARK(header_value); + UPDATE_STATE(s_header_field_start); + CALLBACK_DATA_NOADVANCE(header_value); + REEXECUTE(); + } + } + + case s_headers_almost_done: + { + STRICT_CHECK(ch != LF); + + if (parser->flags & F_TRAILING) { + /* End of a chunked request */ + UPDATE_STATE(s_message_done); + CALLBACK_NOTIFY_NOADVANCE(chunk_complete); + REEXECUTE(); + } + + /* Cannot use transfer-encoding and a content-length header together + per the HTTP specification. (RFC 7230 Section 3.3.3) */ + if ((parser->uses_transfer_encoding == 1) && + (parser->flags & F_CONTENTLENGTH)) { + /* Allow it for lenient parsing as long as `Transfer-Encoding` is + * not `chunked` or allow_length_with_encoding is set + */ + if (parser->flags & F_CHUNKED) { + if (!allow_chunked_length) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + } else if (!lenient) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + } + + UPDATE_STATE(s_headers_done); + + /* Set this here so that on_headers_complete() callbacks can see it */ + if ((parser->flags & F_UPGRADE) && + (parser->flags & F_CONNECTION_UPGRADE)) { + /* For responses, "Upgrade: foo" and "Connection: upgrade" are + * mandatory only when it is a 101 Switching Protocols response, + * otherwise it is purely informational, to announce support. + */ + parser->upgrade = + (parser->type == HTTP_REQUEST || parser->status_code == 101); + } else { + parser->upgrade = (parser->method == HTTP_CONNECT); + } + + /* Here we call the headers_complete callback. This is somewhat + * different than other callbacks because if the user returns 1, we + * will interpret that as saying that this message has no body. This + * is needed for the annoying case of recieving a response to a HEAD + * request. + * + * We'd like to use CALLBACK_NOTIFY_NOADVANCE() here but we cannot, so + * we have to simulate it by handling a change in errno below. + */ + if (settings->on_headers_complete) { + switch (settings->on_headers_complete(parser)) { + case 0: + break; + + case 2: + parser->upgrade = 1; + + /* fall through */ + case 1: + parser->flags |= F_SKIPBODY; + break; + + default: + SET_ERRNO(HPE_CB_headers_complete); + RETURN(p - data); /* Error */ + } + } + + if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { + RETURN(p - data); + } + + REEXECUTE(); + } + + case s_headers_done: + { + int hasBody; + STRICT_CHECK(ch != LF); + + parser->nread = 0; + nread = 0; + + hasBody = parser->flags & F_CHUNKED || + (parser->content_length > 0 && parser->content_length != ULLONG_MAX); + if (parser->upgrade && (parser->method == HTTP_CONNECT || + (parser->flags & F_SKIPBODY) || !hasBody)) { + /* Exit, the rest of the message is in a different protocol. */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + RETURN((p - data) + 1); + } + + if (parser->flags & F_SKIPBODY) { + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->flags & F_CHUNKED) { + /* chunked encoding - ignore Content-Length header, + * prepare for a chunk */ + UPDATE_STATE(s_chunk_size_start); + } else if (parser->uses_transfer_encoding == 1) { + if (parser->type == HTTP_REQUEST && !lenient) { + /* RFC 7230 3.3.3 */ + + /* If a Transfer-Encoding header field + * is present in a request and the chunked transfer coding is not + * the final encoding, the message body length cannot be determined + * reliably; the server MUST respond with the 400 (Bad Request) + * status code and then close the connection. + */ + SET_ERRNO(HPE_INVALID_TRANSFER_ENCODING); + RETURN(p - data); /* Error */ + } else { + /* RFC 7230 3.3.3 */ + + /* If a Transfer-Encoding header field is present in a response and + * the chunked transfer coding is not the final encoding, the + * message body length is determined by reading the connection until + * it is closed by the server. + */ + UPDATE_STATE(s_body_identity_eof); + } + } else { + if (parser->content_length == 0) { + /* Content-Length header given but zero: Content-Length: 0\r\n */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else if (parser->content_length != ULLONG_MAX) { + /* Content-Length header given and non-zero */ + UPDATE_STATE(s_body_identity); + } else { + if (!http_message_needs_eof(parser)) { + /* Assume content-length 0 - read the next */ + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + } else { + /* Read body until EOF */ + UPDATE_STATE(s_body_identity_eof); + } + } + } + + break; + } + + case s_body_identity: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* The difference between advancing content_length and p is because + * the latter will automaticaly advance on the next loop iteration. + * Further, if content_length ends up at 0, we want to see the last + * byte again for our message complete callback. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_message_done); + + /* Mimic CALLBACK_DATA_NOADVANCE() but with one extra byte. + * + * The alternative to doing this is to wait for the next byte to + * trigger the data callback, just as in every other case. The + * problem with this is that this makes it difficult for the test + * harness to distinguish between complete-on-EOF and + * complete-on-length. It's not clear that this distinction is + * important for applications, but let's keep it for now. + */ + CALLBACK_DATA_(body, p - body_mark + 1, p - data); + REEXECUTE(); + } + + break; + } + + /* read until EOF */ + case s_body_identity_eof: + MARK(body); + p = data + len - 1; + + break; + + case s_message_done: + UPDATE_STATE(NEW_MESSAGE()); + CALLBACK_NOTIFY(message_complete); + if (parser->upgrade) { + /* Exit, the rest of the message is in a different protocol. */ + RETURN((p - data) + 1); + } + break; + + case s_chunk_size_start: + { + assert(nread == 1); + assert(parser->flags & F_CHUNKED); + + unhex_val = unhex[(unsigned char)ch]; + if (UNLIKELY(unhex_val == -1)) { + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + parser->content_length = unhex_val; + UPDATE_STATE(s_chunk_size); + break; + } + + case s_chunk_size: + { + uint64_t t; + + assert(parser->flags & F_CHUNKED); + + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + + unhex_val = unhex[(unsigned char)ch]; + + if (unhex_val == -1) { + if (ch == ';' || ch == ' ') { + UPDATE_STATE(s_chunk_parameters); + break; + } + + SET_ERRNO(HPE_INVALID_CHUNK_SIZE); + goto error; + } + + t = parser->content_length; + t *= 16; + t += unhex_val; + + /* Overflow? Test against a conservative limit for simplicity. */ + if (UNLIKELY((ULLONG_MAX - 16) / 16 < parser->content_length)) { + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + } + + parser->content_length = t; + break; + } + + case s_chunk_parameters: + { + assert(parser->flags & F_CHUNKED); + /* just ignore this shit. TODO check for overflow */ + if (ch == CR) { + UPDATE_STATE(s_chunk_size_almost_done); + break; + } + break; + } + + case s_chunk_size_almost_done: + { + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + + parser->nread = 0; + nread = 0; + + if (parser->content_length == 0) { + parser->flags |= F_TRAILING; + UPDATE_STATE(s_header_field_start); + } else { + UPDATE_STATE(s_chunk_data); + } + CALLBACK_NOTIFY(chunk_header); + break; + } + + case s_chunk_data: + { + uint64_t to_read = MIN(parser->content_length, + (uint64_t) ((data + len) - p)); + + assert(parser->flags & F_CHUNKED); + assert(parser->content_length != 0 + && parser->content_length != ULLONG_MAX); + + /* See the explanation in s_body_identity for why the content + * length and data pointers are managed this way. + */ + MARK(body); + parser->content_length -= to_read; + p += to_read - 1; + + if (parser->content_length == 0) { + UPDATE_STATE(s_chunk_data_almost_done); + } + + break; + } + + case s_chunk_data_almost_done: + assert(parser->flags & F_CHUNKED); + assert(parser->content_length == 0); + STRICT_CHECK(ch != CR); + UPDATE_STATE(s_chunk_data_done); + CALLBACK_DATA(body); + break; + + case s_chunk_data_done: + assert(parser->flags & F_CHUNKED); + STRICT_CHECK(ch != LF); + parser->nread = 0; + nread = 0; + UPDATE_STATE(s_chunk_size_start); + CALLBACK_NOTIFY(chunk_complete); + break; + + default: + assert(0 && "unhandled state"); + SET_ERRNO(HPE_INVALID_INTERNAL_STATE); + goto error; + } + } + + /* Run callbacks for any marks that we have leftover after we ran out of + * bytes. There should be at most one of these set, so it's OK to invoke + * them in series (unset marks will not result in callbacks). + * + * We use the NOADVANCE() variety of callbacks here because 'p' has already + * overflowed 'data' and this allows us to correct for the off-by-one that + * we'd otherwise have (since CALLBACK_DATA() is meant to be run with a 'p' + * value that's in-bounds). + */ + + assert(((header_field_mark ? 1 : 0) + + (header_value_mark ? 1 : 0) + + (url_mark ? 1 : 0) + + (body_mark ? 1 : 0) + + (status_mark ? 1 : 0)) <= 1); + + CALLBACK_DATA_NOADVANCE(header_field); + CALLBACK_DATA_NOADVANCE(header_value); + CALLBACK_DATA_NOADVANCE(url); + CALLBACK_DATA_NOADVANCE(body); + CALLBACK_DATA_NOADVANCE(status); + + RETURN(len); + +error: + if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { + SET_ERRNO(HPE_UNKNOWN); + } + + RETURN(p - data); +} + + +/* Does the parser need to see an EOF to find the end of the message? */ +int +http_message_needs_eof (const http_parser *parser) +{ + if (parser->type == HTTP_REQUEST) { + return 0; + } + + /* See RFC 2616 section 4.4 */ + if (parser->status_code / 100 == 1 || /* 1xx e.g. Continue */ + parser->status_code == 204 || /* No Content */ + parser->status_code == 304 || /* Not Modified */ + parser->flags & F_SKIPBODY) { /* response to a HEAD request */ + return 0; + } + + /* RFC 7230 3.3.3, see `s_headers_almost_done` */ + if ((parser->uses_transfer_encoding == 1) && + (parser->flags & F_CHUNKED) == 0) { + return 1; + } + + if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) { + return 0; + } + + return 1; +} + + +int +http_should_keep_alive (const http_parser *parser) +{ + if (parser->http_major > 0 && parser->http_minor > 0) { + /* HTTP/1.1 */ + if (parser->flags & F_CONNECTION_CLOSE) { + return 0; + } + } else { + /* HTTP/1.0 or earlier */ + if (!(parser->flags & F_CONNECTION_KEEP_ALIVE)) { + return 0; + } + } + + return !http_message_needs_eof(parser); +} + + +const char * +http_method_str (enum http_method m) +{ + return ELEM_AT(method_strings, m, ""); +} + +const char * +http_status_str (enum http_status s) +{ + switch (s) { +#define XX(num, name, string) case HTTP_STATUS_##name: return #string; + HTTP_STATUS_MAP(XX) +#undef XX + default: return ""; + } +} + +void +http_parser_init (http_parser *parser, enum http_parser_type t) +{ + void *data = parser->data; /* preserve application data */ + memset(parser, 0, sizeof(*parser)); + parser->data = data; + parser->type = t; + parser->state = (t == HTTP_REQUEST ? s_start_req : (t == HTTP_RESPONSE ? s_start_res : s_start_req_or_res)); + parser->http_errno = HPE_OK; +} + +void +http_parser_settings_init(http_parser_settings *settings) +{ + memset(settings, 0, sizeof(*settings)); +} + +const char * +http_errno_name(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].name; +} + +const char * +http_errno_description(enum http_errno err) { + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); + return http_strerror_tab[err].description; +} + +static enum http_host_state +http_parse_host_char(enum http_host_state s, const char ch) { + switch(s) { + case s_http_userinfo: + case s_http_userinfo_start: + if (ch == '@') { + return s_http_host_start; + } + + if (IS_USERINFO_CHAR(ch)) { + return s_http_userinfo; + } + break; + + case s_http_host_start: + if (ch == '[') { + return s_http_host_v6_start; + } + + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + break; + + case s_http_host: + if (IS_HOST_CHAR(ch)) { + return s_http_host; + } + + /* fall through */ + case s_http_host_v6_end: + if (ch == ':') { + return s_http_host_port_start; + } + + break; + + case s_http_host_v6: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_start: + if (IS_HEX(ch) || ch == ':' || ch == '.') { + return s_http_host_v6; + } + + if (s == s_http_host_v6 && ch == '%') { + return s_http_host_v6_zone_start; + } + break; + + case s_http_host_v6_zone: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_zone_start: + /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ + if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || + ch == '~') { + return s_http_host_v6_zone; + } + break; + + case s_http_host_port: + case s_http_host_port_start: + if (IS_NUM(ch)) { + return s_http_host_port; + } + + break; + + default: + break; + } + return s_http_host_dead; +} + +static int +http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { + enum http_host_state s; + + const char *p; + size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; + + assert(u->field_set & (1 << UF_HOST)); + + u->field_data[UF_HOST].len = 0; + + s = found_at ? s_http_userinfo_start : s_http_host_start; + + for (p = buf + u->field_data[UF_HOST].off; p < buf + buflen; p++) { + enum http_host_state new_s = http_parse_host_char(s, *p); + + if (new_s == s_http_host_dead) { + return 1; + } + + switch(new_s) { + case s_http_host: + if (s != s_http_host) { + u->field_data[UF_HOST].off = (uint16_t)(p - buf); + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6: + if (s != s_http_host_v6) { + u->field_data[UF_HOST].off = (uint16_t)(p - buf); + } + u->field_data[UF_HOST].len++; + break; + + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + u->field_data[UF_HOST].len++; + break; + + case s_http_host_port: + if (s != s_http_host_port) { + u->field_data[UF_PORT].off = (uint16_t)(p - buf); + u->field_data[UF_PORT].len = 0; + u->field_set |= (1 << UF_PORT); + } + u->field_data[UF_PORT].len++; + break; + + case s_http_userinfo: + if (s != s_http_userinfo) { + u->field_data[UF_USERINFO].off = (uint16_t)(p - buf); + u->field_data[UF_USERINFO].len = 0; + u->field_set |= (1 << UF_USERINFO); + } + u->field_data[UF_USERINFO].len++; + break; + + default: + break; + } + s = new_s; + } + + /* Make sure we don't end somewhere unexpected */ + switch (s) { + case s_http_host_start: + case s_http_host_v6_start: + case s_http_host_v6: + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + case s_http_host_port_start: + case s_http_userinfo: + case s_http_userinfo_start: + return 1; + default: + break; + } + + return 0; +} + +void +http_parser_url_init(struct http_parser_url *u) { + memset(u, 0, sizeof(*u)); +} + +int +http_parser_parse_url(const char *buf, size_t buflen, int is_connect, + struct http_parser_url *u) +{ + enum state s; + const char *p; + enum http_parser_url_fields uf, old_uf; + int found_at = 0; + + if (buflen == 0) { + return 1; + } + + u->port = u->field_set = 0; + s = is_connect ? s_req_server_start : s_req_spaces_before_url; + old_uf = UF_MAX; + + for (p = buf; p < buf + buflen; p++) { + s = parse_url_char(s, *p); + + /* Figure out the next field that we're operating on */ + switch (s) { + case s_dead: + return 1; + + /* Skip delimeters */ + case s_req_schema_slash: + case s_req_schema_slash_slash: + case s_req_server_start: + case s_req_query_string_start: + case s_req_fragment_start: + continue; + + case s_req_schema: + uf = UF_SCHEMA; + break; + + case s_req_server_with_at: + found_at = 1; + + /* fall through */ + case s_req_server: + uf = UF_HOST; + break; + + case s_req_path: + uf = UF_PATH; + break; + + case s_req_query_string: + uf = UF_QUERY; + break; + + case s_req_fragment: + uf = UF_FRAGMENT; + break; + + default: + assert(!"Unexpected state"); + return 1; + } + + /* Nothing's changed; soldier on */ + if (uf == old_uf) { + u->field_data[uf].len++; + continue; + } + + u->field_data[uf].off = (uint16_t)(p - buf); + u->field_data[uf].len = 1; + + u->field_set |= (1 << uf); + old_uf = uf; + } + + /* host must be present if there is a schema */ + /* parsing http:///toto will fail */ + if ((u->field_set & (1 << UF_SCHEMA)) && + (u->field_set & (1 << UF_HOST)) == 0) { + return 1; + } + + if (u->field_set & (1 << UF_HOST)) { + if (http_parse_host(buf, u, found_at) != 0) { + return 1; + } + } + + /* CONNECT requests can only contain "hostname:port" */ + if (is_connect && u->field_set != ((1 << UF_HOST)|(1 << UF_PORT))) { + return 1; + } + + if (u->field_set & (1 << UF_PORT)) { + uint16_t off; + uint16_t len; + const char* p; + const char* end; + unsigned long v; + + off = u->field_data[UF_PORT].off; + len = u->field_data[UF_PORT].len; + end = buf + off + len; + + /* NOTE: The characters are already validated and are in the [0-9] range */ + assert((size_t) (off + len) <= buflen && "Port number overflow"); + v = 0; + for (p = buf + off; p < end; p++) { + v *= 10; + v += *p - '0'; + + /* Ports have a max value of 2^16 */ + if (v > 0xffff) { + return 1; + } + } + + u->port = (uint16_t) v; + } + + return 0; +} + +void +http_parser_pause(http_parser *parser, int paused) { + /* Users should only be pausing/unpausing a parser that is not in an error + * state. In non-debug builds, there's not much that we can do about this + * other than ignore it. + */ + if (HTTP_PARSER_ERRNO(parser) == HPE_OK || + HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { + uint32_t nread = parser->nread; /* used by the SET_ERRNO macro */ + SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); + } else { + assert(0 && "Attempting to pause parser in error state"); + } +} + +int +http_body_is_final(const struct http_parser *parser) { + return parser->state == s_message_done; +} + +unsigned long +http_parser_version(void) { + return HTTP_PARSER_VERSION_MAJOR * 0x10000 | + HTTP_PARSER_VERSION_MINOR * 0x00100 | + HTTP_PARSER_VERSION_PATCH * 0x00001; +} + +void +http_parser_set_max_header_size(uint32_t size) { + max_header_size = size; +} diff --git a/qHttpServerBin/5.15.2/headers/http_parser.h b/qHttpServerBin/5.15.2/headers/http_parser.h new file mode 100644 index 000000000..3772b3994 --- /dev/null +++ b/qHttpServerBin/5.15.2/headers/http_parser.h @@ -0,0 +1,449 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#ifndef http_parser_h +#define http_parser_h +#ifdef __cplusplus +extern "C" { +#endif + +/* Also update SONAME in the Makefile whenever you change these. */ +#define HTTP_PARSER_VERSION_MAJOR 2 +#define HTTP_PARSER_VERSION_MINOR 9 +#define HTTP_PARSER_VERSION_PATCH 4 + +#include +#if defined(_WIN32) && !defined(__MINGW32__) && \ + (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__) +#include +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +#elif (defined(__sun) || defined(__sun__)) && defined(__SunOS_5_9) +#include +#else +#include +#endif + +/* Compile with -DHTTP_PARSER_STRICT=0 to make less checks, but run + * faster + */ +#ifndef HTTP_PARSER_STRICT +# define HTTP_PARSER_STRICT 1 +#endif + +/* Maximium header size allowed. If the macro is not defined + * before including this header then the default is used. To + * change the maximum header size, define the macro in the build + * environment (e.g. -DHTTP_MAX_HEADER_SIZE=). To remove + * the effective limit on the size of the header, define the macro + * to a very large number (e.g. -DHTTP_MAX_HEADER_SIZE=0x7fffffff) + */ +#ifndef HTTP_MAX_HEADER_SIZE +# define HTTP_MAX_HEADER_SIZE (80*1024) +#endif + +typedef struct http_parser http_parser; +typedef struct http_parser_settings http_parser_settings; + + +/* Callbacks should return non-zero to indicate an error. The parser will + * then halt execution. + * + * The one exception is on_headers_complete. In a HTTP_RESPONSE parser + * returning '1' from on_headers_complete will tell the parser that it + * should not expect a body. This is used when receiving a response to a + * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: + * chunked' headers that indicate the presence of a body. + * + * Returning `2` from on_headers_complete will tell parser that it should not + * expect neither a body nor any futher responses on this connection. This is + * useful for handling responses to a CONNECT request which may not contain + * `Upgrade` or `Connection: upgrade` headers. + * + * http_data_cb does not return data chunks. It will be called arbitrarily + * many times for each string. E.G. you might get 10 callbacks for "on_url" + * each providing just a few characters more data. + */ +typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); +typedef int (*http_cb) (http_parser*); + + +/* Status Codes */ +#define HTTP_STATUS_MAP(XX) \ + XX(100, CONTINUE, Continue) \ + XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ + XX(102, PROCESSING, Processing) \ + XX(200, OK, OK) \ + XX(201, CREATED, Created) \ + XX(202, ACCEPTED, Accepted) \ + XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \ + XX(204, NO_CONTENT, No Content) \ + XX(205, RESET_CONTENT, Reset Content) \ + XX(206, PARTIAL_CONTENT, Partial Content) \ + XX(207, MULTI_STATUS, Multi-Status) \ + XX(208, ALREADY_REPORTED, Already Reported) \ + XX(226, IM_USED, IM Used) \ + XX(300, MULTIPLE_CHOICES, Multiple Choices) \ + XX(301, MOVED_PERMANENTLY, Moved Permanently) \ + XX(302, FOUND, Found) \ + XX(303, SEE_OTHER, See Other) \ + XX(304, NOT_MODIFIED, Not Modified) \ + XX(305, USE_PROXY, Use Proxy) \ + XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ + XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ + XX(400, BAD_REQUEST, Bad Request) \ + XX(401, UNAUTHORIZED, Unauthorized) \ + XX(402, PAYMENT_REQUIRED, Payment Required) \ + XX(403, FORBIDDEN, Forbidden) \ + XX(404, NOT_FOUND, Not Found) \ + XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ + XX(406, NOT_ACCEPTABLE, Not Acceptable) \ + XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ + XX(408, REQUEST_TIMEOUT, Request Timeout) \ + XX(409, CONFLICT, Conflict) \ + XX(410, GONE, Gone) \ + XX(411, LENGTH_REQUIRED, Length Required) \ + XX(412, PRECONDITION_FAILED, Precondition Failed) \ + XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ + XX(414, URI_TOO_LONG, URI Too Long) \ + XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ + XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ + XX(417, EXPECTATION_FAILED, Expectation Failed) \ + XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ + XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ + XX(423, LOCKED, Locked) \ + XX(424, FAILED_DEPENDENCY, Failed Dependency) \ + XX(426, UPGRADE_REQUIRED, Upgrade Required) \ + XX(428, PRECONDITION_REQUIRED, Precondition Required) \ + XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ + XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ + XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ + XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ + XX(501, NOT_IMPLEMENTED, Not Implemented) \ + XX(502, BAD_GATEWAY, Bad Gateway) \ + XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ + XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ + XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ + XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ + XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ + XX(508, LOOP_DETECTED, Loop Detected) \ + XX(510, NOT_EXTENDED, Not Extended) \ + XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \ + +enum http_status + { +#define XX(num, name, string) HTTP_STATUS_##name = num, + HTTP_STATUS_MAP(XX) +#undef XX + }; + + +/* Request Methods */ +#define HTTP_METHOD_MAP(XX) \ + XX(0, DELETE, DELETE) \ + XX(1, GET, GET) \ + XX(2, HEAD, HEAD) \ + XX(3, POST, POST) \ + XX(4, PUT, PUT) \ + /* pathological */ \ + XX(5, CONNECT, CONNECT) \ + XX(6, OPTIONS, OPTIONS) \ + XX(7, TRACE, TRACE) \ + /* WebDAV */ \ + XX(8, COPY, COPY) \ + XX(9, LOCK, LOCK) \ + XX(10, MKCOL, MKCOL) \ + XX(11, MOVE, MOVE) \ + XX(12, PROPFIND, PROPFIND) \ + XX(13, PROPPATCH, PROPPATCH) \ + XX(14, SEARCH, SEARCH) \ + XX(15, UNLOCK, UNLOCK) \ + XX(16, BIND, BIND) \ + XX(17, REBIND, REBIND) \ + XX(18, UNBIND, UNBIND) \ + XX(19, ACL, ACL) \ + /* subversion */ \ + XX(20, REPORT, REPORT) \ + XX(21, MKACTIVITY, MKACTIVITY) \ + XX(22, CHECKOUT, CHECKOUT) \ + XX(23, MERGE, MERGE) \ + /* upnp */ \ + XX(24, MSEARCH, M-SEARCH) \ + XX(25, NOTIFY, NOTIFY) \ + XX(26, SUBSCRIBE, SUBSCRIBE) \ + XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ + /* RFC-5789 */ \ + XX(28, PATCH, PATCH) \ + XX(29, PURGE, PURGE) \ + /* CalDAV */ \ + XX(30, MKCALENDAR, MKCALENDAR) \ + /* RFC-2068, section 19.6.1.2 */ \ + XX(31, LINK, LINK) \ + XX(32, UNLINK, UNLINK) \ + /* icecast */ \ + XX(33, SOURCE, SOURCE) \ + +enum http_method + { +#define XX(num, name, string) HTTP_##name = num, + HTTP_METHOD_MAP(XX) +#undef XX + }; + + +enum http_parser_type { HTTP_REQUEST, HTTP_RESPONSE, HTTP_BOTH }; + + +/* Flag values for http_parser.flags field */ +enum flags + { F_CHUNKED = 1 << 0 + , F_CONNECTION_KEEP_ALIVE = 1 << 1 + , F_CONNECTION_CLOSE = 1 << 2 + , F_CONNECTION_UPGRADE = 1 << 3 + , F_TRAILING = 1 << 4 + , F_UPGRADE = 1 << 5 + , F_SKIPBODY = 1 << 6 + , F_CONTENTLENGTH = 1 << 7 + }; + + +/* Map for errno-related constants + * + * The provided argument should be a macro that takes 2 arguments. + */ +#define HTTP_ERRNO_MAP(XX) \ + /* No error */ \ + XX(OK, "success") \ + \ + /* Callback-related errors */ \ + XX(CB_message_begin, "the on_message_begin callback failed") \ + XX(CB_url, "the on_url callback failed") \ + XX(CB_header_field, "the on_header_field callback failed") \ + XX(CB_header_value, "the on_header_value callback failed") \ + XX(CB_headers_complete, "the on_headers_complete callback failed") \ + XX(CB_body, "the on_body callback failed") \ + XX(CB_message_complete, "the on_message_complete callback failed") \ + XX(CB_status, "the on_status callback failed") \ + XX(CB_chunk_header, "the on_chunk_header callback failed") \ + XX(CB_chunk_complete, "the on_chunk_complete callback failed") \ + \ + /* Parsing-related errors */ \ + XX(INVALID_EOF_STATE, "stream ended at an unexpected time") \ + XX(HEADER_OVERFLOW, \ + "too many header bytes seen; overflow detected") \ + XX(CLOSED_CONNECTION, \ + "data received after completed connection: close message") \ + XX(INVALID_VERSION, "invalid HTTP version") \ + XX(INVALID_STATUS, "invalid HTTP status code") \ + XX(INVALID_METHOD, "invalid HTTP method") \ + XX(INVALID_URL, "invalid URL") \ + XX(INVALID_HOST, "invalid host") \ + XX(INVALID_PORT, "invalid port") \ + XX(INVALID_PATH, "invalid path") \ + XX(INVALID_QUERY_STRING, "invalid query string") \ + XX(INVALID_FRAGMENT, "invalid fragment") \ + XX(LF_EXPECTED, "LF character expected") \ + XX(INVALID_HEADER_TOKEN, "invalid character in header") \ + XX(INVALID_CONTENT_LENGTH, \ + "invalid character in content-length header") \ + XX(UNEXPECTED_CONTENT_LENGTH, \ + "unexpected content-length header") \ + XX(INVALID_CHUNK_SIZE, \ + "invalid character in chunk size header") \ + XX(INVALID_CONSTANT, "invalid constant string") \ + XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\ + XX(STRICT, "strict mode assertion failed") \ + XX(PAUSED, "parser is paused") \ + XX(UNKNOWN, "an unknown error occurred") \ + XX(INVALID_TRANSFER_ENCODING, \ + "request has invalid transfer-encoding") \ + + +/* Define HPE_* values for each errno value above */ +#define HTTP_ERRNO_GEN(n, s) HPE_##n, +enum http_errno { + HTTP_ERRNO_MAP(HTTP_ERRNO_GEN) +}; +#undef HTTP_ERRNO_GEN + + +/* Get an http_errno value from an http_parser */ +#define HTTP_PARSER_ERRNO(p) ((enum http_errno) (p)->http_errno) + + +struct http_parser { + /** PRIVATE **/ + unsigned int type : 2; /* enum http_parser_type */ + unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ + unsigned int state : 7; /* enum state from http_parser.c */ + unsigned int header_state : 7; /* enum header_state from http_parser.c */ + unsigned int index : 5; /* index into current matcher */ + unsigned int uses_transfer_encoding : 1; /* Transfer-Encoding header is present */ + unsigned int allow_chunked_length : 1; /* Allow headers with both + * `Content-Length` and + * `Transfer-Encoding: chunked` set */ + unsigned int lenient_http_headers : 1; + + uint32_t nread; /* # bytes read in various scenarios */ + uint64_t content_length; /* # bytes in body. `(uint64_t) -1` (all bits one) + * if no Content-Length header. + */ + + /** READ-ONLY **/ + unsigned short http_major; + unsigned short http_minor; + unsigned int status_code : 16; /* responses only */ + unsigned int method : 8; /* requests only */ + unsigned int http_errno : 7; + + /* 1 = Upgrade header was present and the parser has exited because of that. + * 0 = No upgrade header present. + * Should be checked when http_parser_execute() returns in addition to + * error checking. + */ + unsigned int upgrade : 1; + + /** PUBLIC **/ + void *data; /* A pointer to get hook to the "connection" or "socket" object */ +}; + + +struct http_parser_settings { + http_cb on_message_begin; + http_data_cb on_url; + http_data_cb on_status; + http_data_cb on_header_field; + http_data_cb on_header_value; + http_cb on_headers_complete; + http_data_cb on_body; + http_cb on_message_complete; + /* When on_chunk_header is called, the current chunk length is stored + * in parser->content_length. + */ + http_cb on_chunk_header; + http_cb on_chunk_complete; +}; + + +enum http_parser_url_fields + { UF_SCHEMA = 0 + , UF_HOST = 1 + , UF_PORT = 2 + , UF_PATH = 3 + , UF_QUERY = 4 + , UF_FRAGMENT = 5 + , UF_USERINFO = 6 + , UF_MAX = 7 + }; + + +/* Result structure for http_parser_parse_url(). + * + * Callers should index into field_data[] with UF_* values iff field_set + * has the relevant (1 << UF_*) bit set. As a courtesy to clients (and + * because we probably have padding left over), we convert any port to + * a uint16_t. + */ +struct http_parser_url { + uint16_t field_set; /* Bitmask of (1 << UF_*) values */ + uint16_t port; /* Converted UF_PORT string */ + + struct { + uint16_t off; /* Offset into buffer in which field starts */ + uint16_t len; /* Length of run in buffer */ + } field_data[UF_MAX]; +}; + + +/* Returns the library version. Bits 16-23 contain the major version number, + * bits 8-15 the minor version number and bits 0-7 the patch level. + * Usage example: + * + * unsigned long version = http_parser_version(); + * unsigned major = (version >> 16) & 255; + * unsigned minor = (version >> 8) & 255; + * unsigned patch = version & 255; + * printf("http_parser v%u.%u.%u\n", major, minor, patch); + */ +unsigned long http_parser_version(void); + +void http_parser_init(http_parser *parser, enum http_parser_type type); + + +/* Initialize http_parser_settings members to 0 + */ +void http_parser_settings_init(http_parser_settings *settings); + + +/* Executes the parser. Returns number of parsed bytes. Sets + * `parser->http_errno` on error. */ +size_t http_parser_execute(http_parser *parser, + const http_parser_settings *settings, + const char *data, + size_t len); + + +/* If http_should_keep_alive() in the on_headers_complete or + * on_message_complete callback returns 0, then this should be + * the last message on the connection. + * If you are the server, respond with the "Connection: close" header. + * If you are the client, close the connection. + */ +int http_should_keep_alive(const http_parser *parser); + +/* Returns a string version of the HTTP method. */ +const char *http_method_str(enum http_method m); + +/* Returns a string version of the HTTP status code. */ +const char *http_status_str(enum http_status s); + +/* Return a string name of the given error */ +const char *http_errno_name(enum http_errno err); + +/* Return a string description of the given error */ +const char *http_errno_description(enum http_errno err); + +/* Initialize all http_parser_url members to 0 */ +void http_parser_url_init(struct http_parser_url *u); + +/* Parse a URL; return nonzero on failure */ +int http_parser_parse_url(const char *buf, size_t buflen, + int is_connect, + struct http_parser_url *u); + +/* Pause or un-pause the parser; a nonzero value pauses */ +void http_parser_pause(http_parser *parser, int paused); + +/* Checks if this is the final chunk of the body. */ +int http_body_is_final(const http_parser *parser); + +/* Change the maximum header size provided at compile time. */ +void http_parser_set_max_header_size(uint32_t size); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/qdomyos-zwift.pro b/qdomyos-zwift.pro index 4ff41304b..202cf2a02 100644 --- a/qdomyos-zwift.pro +++ b/qdomyos-zwift.pro @@ -1,7 +1,7 @@ TEMPLATE = subdirs CONFIG+=ordered -!android: { +!ios: !android: { SUBDIRS = \ src/qdomyos-zwift-lib.pro \ src/qdomyos-zwift.pro \ @@ -10,9 +10,15 @@ SUBDIRS = \ tst.depends = src/qdomyos-zwift-lib.pro } -android: { +android: { SUBDIRS = \ src/qdomyos-zwift.pro } +ios: { + SUBDIRS = \ + src/qdomyos-zwift-lib.pro \ + src/qdomyos-zwift.pro +} + diff --git a/qt-patches/android/5.15.0/jar/QtAndroidBluetooth.jar b/qt-patches/android/5.15.0/jar/QtAndroidBluetooth.jar new file mode 100644 index 000000000..6abef17d7 Binary files /dev/null and b/qt-patches/android/5.15.0/jar/QtAndroidBluetooth.jar differ diff --git a/qt-patches/windows/5.15.2/binary/mingw64/Qt5Bluetooth.dll b/qt-patches/windows/5.15.2/binary/mingw64/Qt5Bluetooth.dll new file mode 100644 index 000000000..09a74ae61 Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/mingw64/Qt5Bluetooth.dll differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.dll b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.dll new file mode 100644 index 000000000..38b554896 Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.dll differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.exp b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.exp new file mode 100644 index 000000000..414456042 Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.exp differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.lib b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.lib new file mode 100644 index 000000000..32608f1c3 Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.lib differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.prl b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.prl new file mode 100644 index 000000000..202d72f5d --- /dev/null +++ b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetooth.prl @@ -0,0 +1,5 @@ +QMAKE_PRL_BUILD_DIR = C:/qt-everywhere-src-5.15.2/qtconnectivity/src/bluetooth +QMAKE_PRO_INPUT = bluetooth.pro +QMAKE_PRL_TARGET = Qt5Bluetooth.lib +QMAKE_PRL_CONFIG = lex yacc depend_includepath testcase_targets import_plugins import_qpa_plugin windows prepare_docs qt_docs_targets qt_build_extra file_copies qmake_use qt warn_on release link_prl flat debug_and_release precompile_header autogen_precompile_source embed_manifest_dll embed_manifest_exe shared shared release no_plugin_manifest win32 msvc copy_dir_files sse2 aesni sse3 ssse3 sse4_1 sse4_2 avx avx2 avx512f avx512bw avx512cd avx512dq avx512er avx512ifma avx512pf avx512vbmi avx512vl compile_examples f16c force_debug_info largefile precompile_header rdrnd rdseed shani x86SimdAlways prefix_build force_independent utf8_source create_prl link_prl no_private_qt_headers_warning QTDIR_build qt_example_installs exceptions_off testcase_exceptions release ReleaseBuild Release build_pass c++11 generated_privates relative_qt_rpath target_qt c++11 strict_c++ c++14 c++1z qt_install_headers need_fwd_pri qt_install_module debug_and_release build_all create_cmake skip_target_version_ext release ReleaseBuild Release build_pass have_target dll exclusive_builds debug_info no_autoqmake thread moc resources +QMAKE_PRL_VERSION = 5.15.2 diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.dll b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.dll new file mode 100644 index 000000000..2224ad354 Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.dll differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.exp b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.exp new file mode 100644 index 000000000..122b7758a Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.exp differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.lib b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.lib new file mode 100644 index 000000000..ee300702c Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.lib differ diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.prl b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.prl new file mode 100644 index 000000000..8a27cb6d3 --- /dev/null +++ b/qt-patches/windows/5.15.2/binary/msvc2019/Qt5Bluetoothd.prl @@ -0,0 +1,5 @@ +QMAKE_PRL_BUILD_DIR = C:/qt-everywhere-src-5.15.2/qtconnectivity/src/bluetooth +QMAKE_PRO_INPUT = bluetooth.pro +QMAKE_PRL_TARGET = Qt5Bluetoothd.lib +QMAKE_PRL_CONFIG = lex yacc debug depend_includepath testcase_targets import_plugins import_qpa_plugin windows prepare_docs qt_docs_targets qt_build_extra file_copies qmake_use qt warn_on link_prl flat debug_and_release precompile_header autogen_precompile_source embed_manifest_dll embed_manifest_exe shared shared no_plugin_manifest win32 msvc copy_dir_files sse2 aesni sse3 ssse3 sse4_1 sse4_2 avx avx2 avx512f avx512bw avx512cd avx512dq avx512er avx512ifma avx512pf avx512vbmi avx512vl compile_examples f16c force_debug_info largefile precompile_header rdrnd rdseed shani x86SimdAlways prefix_build force_independent utf8_source create_prl link_prl no_private_qt_headers_warning QTDIR_build qt_example_installs exceptions_off testcase_exceptions debug DebugBuild Debug build_pass c++11 generated_privates relative_qt_rpath target_qt c++11 strict_c++ c++14 c++1z qt_install_headers need_fwd_pri qt_install_module debug_and_release build_all create_cmake skip_target_version_ext debug DebugBuild Debug build_pass have_target dll no_plist exclusive_builds debug_info no_autoqmake thread moc resources +QMAKE_PRL_VERSION = 5.15.2 diff --git a/qt-patches/windows/5.15.2/binary/msvc2019/ucrtbased.dll b/qt-patches/windows/5.15.2/binary/msvc2019/ucrtbased.dll new file mode 100644 index 000000000..77456cc34 Binary files /dev/null and b/qt-patches/windows/5.15.2/binary/msvc2019/ucrtbased.dll differ diff --git a/qt-patches/windows/5.15.2/qlowenergycontroller_win.cpp b/qt-patches/windows/5.15.2/qlowenergycontroller_win.cpp new file mode 100644 index 000000000..0f20b1870 --- /dev/null +++ b/qt-patches/windows/5.15.2/qlowenergycontroller_win.cpp @@ -0,0 +1,1357 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2014 Denis Shienkov +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergycontroller_win_p.h" +#include "qbluetoothdevicediscoveryagent_p.h" + +#include +#include // for open modes +#include +#include +#include +#include +#include + +#include // for std::max + +#include + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINDOWS) + +Q_GLOBAL_STATIC(QLibrary, bluetoothapis) + +Q_GLOBAL_STATIC(QVector, qControllers) +static QMutex controllersGuard(QMutex::NonRecursive); + +const QEvent::Type CharacteristicValueEventType = static_cast(QEvent::User + 1); + +class CharacteristicValueEvent : public QEvent +{ +public: + explicit CharacteristicValueEvent(const BLUETOOTH_GATT_VALUE_CHANGED_EVENT *gattValueChangedEvent) + : QEvent(CharacteristicValueEventType) + , m_handle(0) + { + if (!gattValueChangedEvent || gattValueChangedEvent->CharacteristicValueDataSize == 0) + return; + + m_handle = gattValueChangedEvent->ChangedAttributeHandle; + + const PBTH_LE_GATT_CHARACTERISTIC_VALUE gattValue = gattValueChangedEvent->CharacteristicValue; + if (!gattValue) + return; + + m_value = QByteArray(reinterpret_cast(&gattValue->Data[0]), + int(gattValue->DataSize)); + } + + QByteArray m_value; + QLowEnergyHandle m_handle; +}; + +// Bit masks of ClientCharacteristicConfiguration value, see btle spec. +namespace ClientCharacteristicConfigurationValue { +enum { UseNotifications = 0x1, UseIndications = 0x2 }; +} + +static bool gattFunctionsResolved = false; + +static QBluetoothAddress getDeviceAddress(const QString &servicePath) +{ + const int firstbound = servicePath.lastIndexOf(QStringLiteral("_")); + const int lastbound = servicePath.indexOf(QLatin1Char('#'), firstbound); + const QString hex = servicePath.mid(firstbound + 1, lastbound - firstbound - 1); + bool ok = false; + return QBluetoothAddress(hex.toULongLong(&ok, 16)); +} + +static QString getServiceSystemPath(const QBluetoothAddress &deviceAddress, + const QBluetoothUuid &serviceUuid, int *systemErrorCode) +{ + const HDEVINFO deviceInfoSet = ::SetupDiGetClassDevs( + reinterpret_cast(&serviceUuid), + nullptr, + nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + *systemErrorCode = int(::GetLastError()); + return QString(); + } + + QString foundSystemPath; + DWORD index = 0; + + for (;;) { + SP_DEVICE_INTERFACE_DATA deviceInterfaceData; + ::ZeroMemory(&deviceInterfaceData, sizeof(deviceInterfaceData)); + deviceInterfaceData.cbSize = sizeof(deviceInterfaceData); + + if (!::SetupDiEnumDeviceInterfaces( + deviceInfoSet, + nullptr, + reinterpret_cast(&serviceUuid), + index++, + &deviceInterfaceData)) { + *systemErrorCode = int(::GetLastError()); + break; + } + + DWORD deviceInterfaceDetailDataSize = 0; + if (!::SetupDiGetDeviceInterfaceDetail( + deviceInfoSet, + &deviceInterfaceData, + nullptr, + deviceInterfaceDetailDataSize, + &deviceInterfaceDetailDataSize, + nullptr)) { + const int error = int(::GetLastError()); + if (error != ERROR_INSUFFICIENT_BUFFER) { + *systemErrorCode = error; + break; + } + } + + SP_DEVINFO_DATA deviceInfoData; + ::ZeroMemory(&deviceInfoData, sizeof(deviceInfoData)); + deviceInfoData.cbSize = sizeof(deviceInfoData); + + QByteArray deviceInterfaceDetailDataBuffer( + int(deviceInterfaceDetailDataSize), 0); + + PSP_INTERFACE_DEVICE_DETAIL_DATA deviceInterfaceDetailData = + reinterpret_cast + (deviceInterfaceDetailDataBuffer.data()); + + deviceInterfaceDetailData->cbSize = + sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); + + if (!::SetupDiGetDeviceInterfaceDetail( + deviceInfoSet, + &deviceInterfaceData, + deviceInterfaceDetailData, + DWORD(deviceInterfaceDetailDataBuffer.size()), + &deviceInterfaceDetailDataSize, + &deviceInfoData)) { + *systemErrorCode = int(::GetLastError()); + break; + } + + // We need to check on required device address which contains in a + // system path. As it is not enough to use only service UUID for this. + const auto candidateSystemPath = QString::fromWCharArray(deviceInterfaceDetailData->DevicePath); + const auto candidateDeviceAddress = getDeviceAddress(candidateSystemPath); + if (candidateDeviceAddress == deviceAddress) { + foundSystemPath = candidateSystemPath; + *systemErrorCode = NO_ERROR; + break; + } + } + + ::SetupDiDestroyDeviceInfoList(deviceInfoSet); + return foundSystemPath; +} + +static HANDLE openSystemDevice( + const QString &systemPath, QIODevice::OpenMode openMode, int *systemErrorCode) +{ + DWORD desiredAccess = 0; + DWORD shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; + + if (openMode & QIODevice::ReadOnly) { + desiredAccess |= GENERIC_READ; + } + + if (openMode & QIODevice::WriteOnly) { + desiredAccess |= GENERIC_WRITE; + shareMode &= ~DWORD(FILE_SHARE_WRITE); + } + + const HANDLE hDevice = ::CreateFile( + reinterpret_cast(systemPath.utf16()), + desiredAccess, + shareMode, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + *systemErrorCode = (INVALID_HANDLE_VALUE == hDevice) + ? int(::GetLastError()) : NO_ERROR; + return hDevice; +} + +static HANDLE openSystemService(const QBluetoothAddress &deviceAddress, + const QBluetoothUuid &service, QIODevice::OpenMode openMode, int *systemErrorCode) +{ + const QString serviceSystemPath = getServiceSystemPath( + deviceAddress, service, systemErrorCode); + + if (*systemErrorCode != NO_ERROR) + return INVALID_HANDLE_VALUE; + + const HANDLE hService = openSystemDevice( + serviceSystemPath, openMode, systemErrorCode); + + if (*systemErrorCode != NO_ERROR) + return INVALID_HANDLE_VALUE; + + return hService; +} + +static void closeSystemDevice(HANDLE hDevice) +{ + if (hDevice && hDevice != INVALID_HANDLE_VALUE) + ::CloseHandle(hDevice); +} + +static QVector enumeratePrimaryGattServices( + HANDLE hDevice, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return QVector(); + } + + QVector foundServices; + USHORT servicesCount = 0; + for (;;) { + const HRESULT hr = ::BluetoothGATTGetServices( + hDevice, + servicesCount, + foundServices.isEmpty() ? nullptr : &foundServices[0], + &servicesCount, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) { + *systemErrorCode = NO_ERROR; + return foundServices; + } else { + const int error = WIN32_FROM_HRESULT(hr); + if (error == ERROR_MORE_DATA) { + foundServices.resize(servicesCount); + } else { + *systemErrorCode = error; + return QVector(); + } + } + } +} + +static QVector enumerateGattCharacteristics( + HANDLE hService, PBTH_LE_GATT_SERVICE gattService, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return QVector(); + } + + QVector foundCharacteristics; + USHORT characteristicsCount = 0; + for (;;) { + const HRESULT hr = ::BluetoothGATTGetCharacteristics( + hService, + gattService, + characteristicsCount, + foundCharacteristics.isEmpty() ? nullptr : &foundCharacteristics[0], + &characteristicsCount, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) { + *systemErrorCode = NO_ERROR; + return foundCharacteristics; + } else { + const int error = WIN32_FROM_HRESULT(hr); + if (error == ERROR_MORE_DATA) { + foundCharacteristics.resize(characteristicsCount); + } else { + *systemErrorCode = error; + return QVector(); + } + } + } +} + +static QByteArray getGattCharacteristicValue( + HANDLE hService, PBTH_LE_GATT_CHARACTERISTIC gattCharacteristic, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return QByteArray(); + } + + QByteArray valueBuffer; + USHORT valueBufferSize = 0; + for (;;) { + const auto valuePtr = valueBuffer.isEmpty() + ? nullptr + : reinterpret_cast(valueBuffer.data()); + + const HRESULT hr = ::BluetoothGATTGetCharacteristicValue( + hService, + gattCharacteristic, + valueBufferSize, + valuePtr, + &valueBufferSize, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) { + *systemErrorCode = NO_ERROR; + return QByteArray(reinterpret_cast(&valuePtr->Data[0]), + int(valuePtr->DataSize)); + } else { + const int error = WIN32_FROM_HRESULT(hr); + if (error == ERROR_MORE_DATA) { + valueBuffer.resize(valueBufferSize); + valueBuffer.fill(0); + } else { + *systemErrorCode = error; + return QByteArray(); + } + } + } +} + +static void setGattCharacteristicValue( + HANDLE hService, PBTH_LE_GATT_CHARACTERISTIC gattCharacteristic, + const QByteArray &value, DWORD flags, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return; + } + + QByteArray valueBuffer; + QDataStream out(&valueBuffer, QIODevice::WriteOnly); + ULONG dataSize = ULONG(value.size()); + out.writeRawData(reinterpret_cast(&dataSize), sizeof(dataSize)); + out.writeRawData(value.constData(), value.size()); + + BTH_LE_GATT_RELIABLE_WRITE_CONTEXT reliableWriteContext = 0; + + const HRESULT hr = ::BluetoothGATTSetCharacteristicValue( + hService, + gattCharacteristic, + reinterpret_cast(valueBuffer.data()), + reliableWriteContext, + flags); + + if (SUCCEEDED(hr)) + *systemErrorCode = NO_ERROR; + else + *systemErrorCode = WIN32_FROM_HRESULT(hr); +} + +static QVector enumerateGattDescriptors( + HANDLE hService, PBTH_LE_GATT_CHARACTERISTIC gattCharacteristic, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return QVector(); + } + + QVector foundDescriptors; + USHORT descriptorsCount = 0; + for (;;) { + const HRESULT hr = ::BluetoothGATTGetDescriptors( + hService, + gattCharacteristic, + descriptorsCount, + foundDescriptors.isEmpty() ? nullptr : &foundDescriptors[0], + &descriptorsCount, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) { + *systemErrorCode = NO_ERROR; + return foundDescriptors; + } else { + const int error = WIN32_FROM_HRESULT(hr); + if (error == ERROR_MORE_DATA) { + foundDescriptors.resize(descriptorsCount); + } else { + *systemErrorCode = error; + return QVector(); + } + } + } +} + +static QByteArray getGattDescriptorValue( + HANDLE hService, PBTH_LE_GATT_DESCRIPTOR gattDescriptor, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return QByteArray(); + } + + QByteArray valueBuffer; + USHORT valueBufferSize = 0; + for (;;) { + const auto valuePtr = valueBuffer.isEmpty() + ? nullptr + : reinterpret_cast(valueBuffer.data()); + + const HRESULT hr = ::BluetoothGATTGetDescriptorValue( + hService, + gattDescriptor, + valueBufferSize, + valuePtr, + &valueBufferSize, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) { + *systemErrorCode = NO_ERROR; + if (gattDescriptor->DescriptorType == CharacteristicUserDescription) { + QString valueString = QString::fromUtf16(reinterpret_cast(&valuePtr->Data[0]), + valuePtr->DataSize/2); + return valueString.toUtf8(); + } + return QByteArray(reinterpret_cast(&valuePtr->Data[0]), + int(valuePtr->DataSize)); + } else { + const int error = WIN32_FROM_HRESULT(hr); + if (error == ERROR_MORE_DATA) { + valueBuffer.resize(valueBufferSize); + valueBuffer.fill(0); + } else { + *systemErrorCode = error; + return QByteArray(); + } + } + } +} + +static void setGattDescriptorValue( + HANDLE hService, PBTH_LE_GATT_DESCRIPTOR gattDescriptor, + QByteArray value, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return; + } + + const int requiredValueBufferSize = int(sizeof(BTH_LE_GATT_DESCRIPTOR_VALUE)) + + value.size(); + + QByteArray valueBuffer(requiredValueBufferSize, 0); + + PBTH_LE_GATT_DESCRIPTOR_VALUE gattValue = reinterpret_cast< + PBTH_LE_GATT_DESCRIPTOR_VALUE>(valueBuffer.data()); + + gattValue->DescriptorType = gattDescriptor->DescriptorType; + + if (gattValue->DescriptorType == ClientCharacteristicConfiguration) { + QDataStream in(value); + quint8 u; + in >> u; + + // We need to setup appropriate fields that allow to subscribe for events. + gattValue->ClientCharacteristicConfiguration.IsSubscribeToNotification = + bool(u & ClientCharacteristicConfigurationValue::UseNotifications); + gattValue->ClientCharacteristicConfiguration.IsSubscribeToIndication = + bool(u & ClientCharacteristicConfigurationValue::UseIndications); + } + + gattValue->DataSize = ULONG(value.size()); + ::memcpy(gattValue->Data, value.constData(), size_t(value.size())); + + const HRESULT hr = ::BluetoothGATTSetDescriptorValue( + hService, + gattDescriptor, + gattValue, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) + *systemErrorCode = NO_ERROR; + else + *systemErrorCode = WIN32_FROM_HRESULT(hr); +} + +static void WINAPI eventChangedCallbackEntry( + BTH_LE_GATT_EVENT_TYPE eventType, PVOID eventOutParameter, PVOID context) +{ + if ((eventType != CharacteristicValueChangedEvent) || !eventOutParameter || !context) + return; + + QMutexLocker locker(&controllersGuard); + const auto target = static_cast(context); + if (!qControllers->contains(target)) + return; + + CharacteristicValueEvent *e = new CharacteristicValueEvent( + reinterpret_cast(eventOutParameter)); + + QCoreApplication::postEvent(target, e); +} + +static HANDLE registerEvent( + HANDLE hService, BTH_LE_GATT_CHARACTERISTIC gattCharacteristic, + PVOID context, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return INVALID_HANDLE_VALUE; + } + + HANDLE hEvent = INVALID_HANDLE_VALUE; + + BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION registration; + ::ZeroMemory(®istration, sizeof(registration)); + registration.NumCharacteristics = 1; + registration.Characteristics[0] = gattCharacteristic; + + const HRESULT hr = ::BluetoothGATTRegisterEvent( + hService, + CharacteristicValueChangedEvent, + ®istration, + eventChangedCallbackEntry, + context, + &hEvent, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) + *systemErrorCode = NO_ERROR; + else + *systemErrorCode = WIN32_FROM_HRESULT(hr); + + return hEvent; +} + +static void unregisterEvent(HANDLE hEvent, int *systemErrorCode) +{ + if (!gattFunctionsResolved) { + *systemErrorCode = ERROR_NOT_SUPPORTED; + return; + } + + const HRESULT hr = ::BluetoothGATTUnregisterEvent( + hEvent, + BLUETOOTH_GATT_FLAG_NONE); + + if (SUCCEEDED(hr)) + *systemErrorCode = NO_ERROR; + else + *systemErrorCode = WIN32_FROM_HRESULT(hr); +} + +static QBluetoothUuid qtBluetoothUuidFromNativeLeUuid(const BTH_LE_UUID &uuid) +{ + return uuid.IsShortUuid ? QBluetoothUuid(uuid.Value.ShortUuid) + : QBluetoothUuid(uuid.Value.LongUuid); +} + +static BTH_LE_UUID nativeLeUuidFromQtBluetoothUuid(const QBluetoothUuid &uuid) +{ + BTH_LE_UUID gattUuid; + ::ZeroMemory(&gattUuid, sizeof(gattUuid)); + if (uuid.minimumSize() == 2) { + gattUuid.IsShortUuid = TRUE; + gattUuid.Value.ShortUuid = USHORT(uuid.data1); // other fields should be empty! + } else { + gattUuid.Value.LongUuid = uuid; + } + return gattUuid; +} + +static BTH_LE_GATT_CHARACTERISTIC recoverNativeLeGattCharacteristic( + QLowEnergyHandle serviceHandle, QLowEnergyHandle characteristicHandle, + const QLowEnergyServicePrivate::CharData &characteristicData) +{ + BTH_LE_GATT_CHARACTERISTIC gattCharacteristic; + + gattCharacteristic.ServiceHandle = serviceHandle; + gattCharacteristic.AttributeHandle = characteristicHandle; + gattCharacteristic.CharacteristicValueHandle = characteristicData.valueHandle; + + gattCharacteristic.CharacteristicUuid = nativeLeUuidFromQtBluetoothUuid( + characteristicData.uuid); + + gattCharacteristic.HasExtendedProperties = bool(characteristicData.properties + & QLowEnergyCharacteristic::ExtendedProperty); + gattCharacteristic.IsBroadcastable = bool(characteristicData.properties + & QLowEnergyCharacteristic::Broadcasting); + gattCharacteristic.IsIndicatable = bool(characteristicData.properties + & QLowEnergyCharacteristic::Indicate); + gattCharacteristic.IsNotifiable = bool(characteristicData.properties + & QLowEnergyCharacteristic::Notify); + gattCharacteristic.IsReadable = bool(characteristicData.properties + & QLowEnergyCharacteristic::Read); + gattCharacteristic.IsSignedWritable = bool(characteristicData.properties + & QLowEnergyCharacteristic::WriteSigned); + gattCharacteristic.IsWritable = bool(characteristicData.properties + & QLowEnergyCharacteristic::Write); + gattCharacteristic.IsWritableWithoutResponse = bool(characteristicData.properties + & QLowEnergyCharacteristic::WriteNoResponse); + + return gattCharacteristic; +} + +static BTH_LE_GATT_DESCRIPTOR_TYPE nativeLeGattDescriptorTypeFromUuid( + const QBluetoothUuid &uuid) +{ + switch (uuid.toUInt16()) { + case QBluetoothUuid::CharacteristicExtendedProperties: + return CharacteristicExtendedProperties; + case QBluetoothUuid::CharacteristicUserDescription: + return CharacteristicUserDescription; + case QBluetoothUuid::ClientCharacteristicConfiguration: + return ClientCharacteristicConfiguration; + case QBluetoothUuid::ServerCharacteristicConfiguration: + return ServerCharacteristicConfiguration; + case QBluetoothUuid::CharacteristicPresentationFormat: + return CharacteristicFormat; + case QBluetoothUuid::CharacteristicAggregateFormat: + return CharacteristicAggregateFormat; + default: + return CustomDescriptor; + } +} + +static BTH_LE_GATT_DESCRIPTOR recoverNativeLeGattDescriptor( + QLowEnergyHandle serviceHandle, QLowEnergyHandle characteristicHandle, + QLowEnergyHandle descriptorHandle, + const QLowEnergyServicePrivate::DescData &descriptorData) +{ + BTH_LE_GATT_DESCRIPTOR gattDescriptor; + + gattDescriptor.ServiceHandle = serviceHandle; + gattDescriptor.CharacteristicHandle = characteristicHandle; + gattDescriptor.AttributeHandle = descriptorHandle; + + gattDescriptor.DescriptorUuid = nativeLeUuidFromQtBluetoothUuid( + descriptorData.uuid); + + gattDescriptor.DescriptorType = nativeLeGattDescriptorTypeFromUuid + (descriptorData.uuid); + + return gattDescriptor; +} + +void QLowEnergyControllerPrivateWin32::customEvent(QEvent *e) +{ + if (e->type() != CharacteristicValueEventType) + return; + + const CharacteristicValueEvent *characteristicEvent + = static_cast(e); + + updateValueOfCharacteristic(characteristicEvent->m_handle, + characteristicEvent->m_value, false); + + const QSharedPointer service = serviceForHandle( + characteristicEvent->m_handle); + if (service.isNull()) + return; + + const QLowEnergyCharacteristic ch(service, characteristicEvent->m_handle); + emit service->characteristicChanged(ch, characteristicEvent->m_value); +} + +QLowEnergyControllerPrivateWin32::QLowEnergyControllerPrivateWin32() + : QLowEnergyControllerPrivate() +{ + QMutexLocker locker(&controllersGuard); + qControllers()->append(this); + + gattFunctionsResolved = resolveFunctions(bluetoothapis()); + if (!gattFunctionsResolved) { + qCWarning(QT_BT_WINDOWS) << "LE is not supported on this OS"; + return; + } +} + +QLowEnergyControllerPrivateWin32::~QLowEnergyControllerPrivateWin32() +{ + QMutexLocker locker(&controllersGuard); + qControllers()->removeAll(this); +} + +void QLowEnergyControllerPrivateWin32::init() +{ +} + +void QLowEnergyControllerPrivateWin32::connectToDevice() +{ + // required to pass unit test on default backend + if (remoteDevice.isNull()) { + qWarning() << "Invalid/null remote device address"; + setError(QLowEnergyController::UnknownRemoteDeviceError); + return; + } + + if (!deviceSystemPath.isEmpty()) { + qCDebug(QT_BT_WINDOWS) << "Already is connected"; + return; + } + + setState(QLowEnergyController::ConnectingState); + + deviceSystemPath = + QBluetoothDeviceDiscoveryAgentPrivate::discoveredLeDeviceSystemPath( + remoteDevice); + + if (deviceSystemPath.isEmpty()) { + qCWarning(QT_BT_WINDOWS) << qt_error_string(ERROR_PATH_NOT_FOUND); + setError(QLowEnergyController::UnknownRemoteDeviceError); + setState(QLowEnergyController::UnconnectedState); + return; + } + + setState(QLowEnergyController::ConnectedState); + + thread = new QThread; + threadWorker = new ThreadWorker; + threadWorker->moveToThread(thread); + connect(threadWorker, &ThreadWorker::jobFinished, this, &QLowEnergyControllerPrivateWin32::jobFinished); + connect(thread, &QThread::finished, threadWorker, &ThreadWorker::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + + Q_Q(QLowEnergyController); + emit q->connected(); +} + +void QLowEnergyControllerPrivateWin32::disconnectFromDevice() +{ + if (deviceSystemPath.isEmpty()) { + qCDebug(QT_BT_WINDOWS) << "Already is disconnected"; + return; + } + + setState(QLowEnergyController::ClosingState); + deviceSystemPath.clear(); + setState(QLowEnergyController::UnconnectedState); + + if (thread) { + disconnect(threadWorker, &ThreadWorker::jobFinished, this, &QLowEnergyControllerPrivateWin32::jobFinished); + thread->quit(); + thread = nullptr; + } + + for (const auto &servicePrivate: serviceList) + closeSystemDevice(servicePrivate->hService); + + Q_Q(QLowEnergyController); + emit q->disconnected(); +} + +void QLowEnergyControllerPrivateWin32::discoverServices() +{ + int systemErrorCode = NO_ERROR; + + const HANDLE hDevice = openSystemDevice( + deviceSystemPath, QIODevice::ReadOnly, &systemErrorCode); + + if (systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << qt_error_string(systemErrorCode); + setError(QLowEnergyController::NetworkError); + setState(QLowEnergyController::ConnectedState); + return; + } + + const QVector foundServices = + enumeratePrimaryGattServices(hDevice, &systemErrorCode); + + closeSystemDevice(hDevice); + + if (systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << qt_error_string(systemErrorCode); + setError(QLowEnergyController::NetworkError); + setState(QLowEnergyController::ConnectedState); + return; + } + + setState(QLowEnergyController::DiscoveringState); + + Q_Q(QLowEnergyController); + + for (const BTH_LE_GATT_SERVICE &service : foundServices) { + const QBluetoothUuid uuid = qtBluetoothUuidFromNativeLeUuid( + service.ServiceUuid); + qCDebug(QT_BT_WINDOWS) << "Found uuid:" << uuid; + + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = uuid; + priv->type = QLowEnergyService::PrimaryService; + priv->startHandle = service.AttributeHandle; + priv->setController(this); + + QSharedPointer pointer(priv); + serviceList.insert(uuid, pointer); + + emit q->serviceDiscovered(uuid); + } + + setState(QLowEnergyController::DiscoveredState); + emit q->discoveryFinished(); +} + +void QLowEnergyControllerPrivateWin32::discoverServiceDetails( + const QBluetoothUuid &service) +{ + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINDOWS) << "Discovery of unknown service" << service.toString() + << "not possible"; + return; + } + + const QSharedPointer servicePrivate = + serviceList.value(service); + + int systemErrorCode = NO_ERROR; + + // Only open a service once and close it in the QLowEnergyServicePrivate destructor + if (!servicePrivate->hService || servicePrivate->hService == INVALID_HANDLE_VALUE) { + servicePrivate->hService = openSystemService(remoteDevice, service, + QIODevice::ReadOnly | QIODevice::WriteOnly, + &systemErrorCode); + if (systemErrorCode != NO_ERROR) { + servicePrivate->hService = openSystemService(remoteDevice, service, + QIODevice::ReadOnly, + &systemErrorCode); + } + } + + if (systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service.toString() + << ":" << qt_error_string(systemErrorCode); + servicePrivate->setError(QLowEnergyService::UnknownError); + servicePrivate->setState(QLowEnergyService::DiscoveryRequired); + return; + } + + // We assume that the service does not have any characteristics with descriptors. + servicePrivate->endHandle = servicePrivate->startHandle; + + const QVector foundCharacteristics = + enumerateGattCharacteristics(servicePrivate->hService, nullptr, &systemErrorCode); + + if (systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to get characteristics for service" << service.toString() + << ":" << qt_error_string(systemErrorCode); + servicePrivate->setError(QLowEnergyService::CharacteristicReadError); + servicePrivate->setState(QLowEnergyService::DiscoveryRequired); + return; + } + + for (const BTH_LE_GATT_CHARACTERISTIC &gattCharacteristic : foundCharacteristics) { + const QLowEnergyHandle characteristicHandle = gattCharacteristic.AttributeHandle; + + QLowEnergyServicePrivate::CharData detailsData; + + detailsData.hValueChangeEvent = nullptr; + + detailsData.uuid = qtBluetoothUuidFromNativeLeUuid( + gattCharacteristic.CharacteristicUuid); + detailsData.valueHandle = gattCharacteristic.CharacteristicValueHandle; + + QLowEnergyCharacteristic::PropertyTypes properties = QLowEnergyCharacteristic::Unknown; + if (gattCharacteristic.HasExtendedProperties) + properties |= QLowEnergyCharacteristic::ExtendedProperty; + if (gattCharacteristic.IsBroadcastable) + properties |= QLowEnergyCharacteristic::Broadcasting; + if (gattCharacteristic.IsIndicatable) + properties |= QLowEnergyCharacteristic::Indicate; + if (gattCharacteristic.IsNotifiable) + properties |= QLowEnergyCharacteristic::Notify; + if (gattCharacteristic.IsReadable) + properties |= QLowEnergyCharacteristic::Read; + if (gattCharacteristic.IsSignedWritable) + properties |= QLowEnergyCharacteristic::WriteSigned; + if (gattCharacteristic.IsWritable) + properties |= QLowEnergyCharacteristic::Write; + if (gattCharacteristic.IsWritableWithoutResponse) + properties |= QLowEnergyCharacteristic::WriteNoResponse; + + detailsData.properties = properties; + detailsData.value = getGattCharacteristicValue( + servicePrivate->hService, const_cast( + &gattCharacteristic), &systemErrorCode); + + if (systemErrorCode != NO_ERROR) { + // We do not interrupt enumerating of characteristics + // if value can not be read + qCWarning(QT_BT_WINDOWS) << "Unable to get value for characteristic" + << detailsData.uuid.toString() + << "of the service" << service.toString() + << ":" << qt_error_string(systemErrorCode); + } + + // We assume that the characteristic has no any descriptors. So, the + // biggest characteristic + 1 will indicate an end handle of service. + servicePrivate->endHandle = std::max( + servicePrivate->endHandle, + QLowEnergyHandle(gattCharacteristic.AttributeHandle + 1)); + + const QVector foundDescriptors = enumerateGattDescriptors( + servicePrivate->hService, const_cast( + &gattCharacteristic), &systemErrorCode); + + if (systemErrorCode != NO_ERROR) { + if (systemErrorCode != ERROR_NOT_FOUND) { + qCWarning(QT_BT_WINDOWS) << "Unable to get descriptor for characteristic" + << detailsData.uuid.toString() + << "of the service" << service.toString() + << ":" << qt_error_string(systemErrorCode); + servicePrivate->setError(QLowEnergyService::DescriptorReadError); + servicePrivate->setState(QLowEnergyService::DiscoveryRequired); + return; + } + } + + for (const BTH_LE_GATT_DESCRIPTOR &gattDescriptor : foundDescriptors) { + const QLowEnergyHandle descriptorHandle = gattDescriptor.AttributeHandle; + + QLowEnergyServicePrivate::DescData data; + data.uuid = qtBluetoothUuidFromNativeLeUuid( + gattDescriptor.DescriptorUuid); + + data.value = getGattDescriptorValue(servicePrivate->hService, const_cast( + &gattDescriptor), &systemErrorCode); + + /* QZ rviola + if (systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to get value for descriptor" + << data.uuid.toString() + << "for characteristic" + << detailsData.uuid.toString() + << "of the service" << service.toString() + << ":" << qt_error_string(systemErrorCode); + servicePrivate->setError(QLowEnergyService::DescriptorReadError); + servicePrivate->setState(QLowEnergyService::DiscoveryRequired); + return; + }*/ + + // Biggest descriptor will contain an end handle of service. + servicePrivate->endHandle = std::max( + servicePrivate->endHandle, + QLowEnergyHandle(gattDescriptor.AttributeHandle)); + + detailsData.descriptorList.insert(descriptorHandle, data); + } + + servicePrivate->characteristicList.insert(characteristicHandle, detailsData); + } + + servicePrivate->setState(QLowEnergyService::ServiceDiscovered); +} + +void QLowEnergyControllerPrivateWin32::startAdvertising(const QLowEnergyAdvertisingParameters &, const QLowEnergyAdvertisingData &, const QLowEnergyAdvertisingData &) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWin32::stopAdvertising() +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWin32::requestConnectionUpdate(const QLowEnergyConnectionParameters &) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWin32::readCharacteristic( + const QSharedPointer service, + const QLowEnergyHandle charHandle) +{ + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) + return; + + const QLowEnergyServicePrivate::CharData &charDetails + = service->characteristicList[charHandle]; + if (!(charDetails.properties & QLowEnergyCharacteristic::Read)) { + // if this succeeds the device has a bug, char is advertised as + // non-readable. We try to be permissive and let the remote + // device answer to the read attempt + qCWarning(QT_BT_WINDOWS) << "Reading non-readable char" << charHandle; + } + + ReadCharData data; + data.systemErrorCode = NO_ERROR; + data.hService = service->hService; + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::CharacteristicReadError); + return; + } + + data.gattCharacteristic = recoverNativeLeGattCharacteristic( + service->startHandle, charHandle, charDetails); + + ThreadWorkerJob job; + job.operation = ThreadWorkerJob::ReadChar; + job.data = QVariant::fromValue(data); + + QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection, + Q_ARG(ThreadWorkerJob, job)); +} + +void QLowEnergyControllerPrivateWin32::writeCharacteristic( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QByteArray &newValue, + QLowEnergyService::WriteMode mode) +{ + Q_ASSERT(!service.isNull()); + + if (!service->characteristicList.contains(charHandle)) { + service->setError(QLowEnergyService::CharacteristicWriteError); + return; + } + + WriteCharData data; + data.systemErrorCode = NO_ERROR; + data.hService = service->hService; + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::CharacteristicWriteError); + return; + } + + const QLowEnergyServicePrivate::CharData &charDetails + = service->characteristicList[charHandle]; + + data.gattCharacteristic = recoverNativeLeGattCharacteristic( + service->startHandle, charHandle, charDetails); + + data.flags = (mode == QLowEnergyService::WriteWithResponse) + ? BLUETOOTH_GATT_FLAG_NONE + : BLUETOOTH_GATT_FLAG_WRITE_WITHOUT_RESPONSE; + + ThreadWorkerJob job; + job.operation = ThreadWorkerJob::WriteChar; + data.newValue = newValue; + data.mode = mode; + job.data = QVariant::fromValue(data); + + QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection, + Q_ARG(ThreadWorkerJob, job)); +} + +void QLowEnergyControllerPrivateWin32::jobFinished(const ThreadWorkerJob &job) +{ + switch (job.operation) { + case ThreadWorkerJob::WriteChar: + { + const WriteCharData data = job.data.value(); + const QLowEnergyHandle charHandle = static_cast(data.gattCharacteristic.AttributeHandle); + const QSharedPointer service = serviceForHandle(charHandle); + + if (data.systemErrorCode != NO_ERROR) { + const QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle]; + qCWarning(QT_BT_WINDOWS) << "Unable to set value for characteristic" + << charDetails.uuid.toString() + << "of the service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::CharacteristicWriteError); + return; + } + + updateValueOfCharacteristic(charHandle, data.newValue, false); + + if (data.mode == QLowEnergyService::WriteWithResponse) { + const QLowEnergyCharacteristic ch = characteristicForHandle(charHandle); + emit service->characteristicWritten(ch, data.newValue); + } + } + break; + case ThreadWorkerJob::ReadChar: + { + const ReadCharData data = job.data.value(); + const QLowEnergyHandle charHandle = static_cast(data.gattCharacteristic.AttributeHandle); + const QSharedPointer service = serviceForHandle(charHandle); + + if (data.systemErrorCode != NO_ERROR) { + const QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle]; + qCWarning(QT_BT_WINDOWS) << "Unable to get value for characteristic" + << charDetails.uuid.toString() + << "of the service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::CharacteristicReadError); + return; + } + + updateValueOfCharacteristic(charHandle, data.value, false); + + const QLowEnergyCharacteristic ch(service, charHandle); + emit service->characteristicRead(ch, data.value); + } + break; + case ThreadWorkerJob::WriteDescr: + { + WriteDescData data = job.data.value(); + const QLowEnergyHandle descriptorHandle = static_cast(data.gattDescriptor.AttributeHandle); + const QLowEnergyHandle charHandle = static_cast(data.gattDescriptor.CharacteristicHandle); + const QSharedPointer service = serviceForHandle(charHandle); + QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle]; + const QLowEnergyServicePrivate::DescData &dscrDetails = charDetails.descriptorList[descriptorHandle]; + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to set value for descriptor" + << dscrDetails.uuid.toString() + << "for characteristic" + << charDetails.uuid.toString() + << "of the service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + + if (data.gattDescriptor.DescriptorType == ClientCharacteristicConfiguration) { + + QDataStream in(data.newValue); + quint8 u; + in >> u; + + if (u & ClientCharacteristicConfigurationValue::UseNotifications + || u & ClientCharacteristicConfigurationValue::UseIndications) { + if (!charDetails.hValueChangeEvent) { + BTH_LE_GATT_CHARACTERISTIC gattCharacteristic = recoverNativeLeGattCharacteristic( + service->startHandle, charHandle, charDetails); + + // note: if the service handle is closed the event registration is no longer valid. + charDetails.hValueChangeEvent = registerEvent( + data.hService, gattCharacteristic, this, &data.systemErrorCode); + } + } else { + if (charDetails.hValueChangeEvent) { + unregisterEvent(charDetails.hValueChangeEvent, &data.systemErrorCode); + charDetails.hValueChangeEvent = nullptr; + } + } + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to subscribe events for descriptor" + << dscrDetails.uuid.toString() + << "for characteristic" + << charDetails.uuid.toString() + << "of the service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + } + + updateValueOfDescriptor(charHandle, descriptorHandle, data.newValue, false); + + const QLowEnergyDescriptor dscr(service, charHandle, descriptorHandle); + emit service->descriptorWritten(dscr, data.newValue); + } + break; + case ThreadWorkerJob::ReadDescr: + { + ReadDescData data = job.data.value(); + const QLowEnergyHandle descriptorHandle = static_cast(data.gattDescriptor.AttributeHandle); + const QLowEnergyHandle charHandle = static_cast(data.gattDescriptor.CharacteristicHandle); + const QSharedPointer service = serviceForHandle(charHandle); + QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle]; + const QLowEnergyServicePrivate::DescData &dscrDetails = charDetails.descriptorList[descriptorHandle]; + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to get value for descriptor" + << dscrDetails.uuid.toString() + << "for characteristic" + << charDetails.uuid.toString() + << "of the service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::DescriptorReadError); + return; + } + + updateValueOfDescriptor(charHandle, descriptorHandle, data.value, false); + + QLowEnergyDescriptor dscr(service, charHandle, descriptorHandle); + emit service->descriptorRead(dscr, data.value); + } + break; + } + + QMetaObject::invokeMethod(threadWorker, "runPendingJob", Qt::QueuedConnection); +} + +void QLowEnergyControllerPrivateWin32::readDescriptor( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descriptorHandle) +{ + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) + return; + + const QLowEnergyServicePrivate::CharData &charDetails + = service->characteristicList[charHandle]; + if (!charDetails.descriptorList.contains(descriptorHandle)) + return; + + ReadDescData data; + data.systemErrorCode = NO_ERROR; + data.hService = service->hService; + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::DescriptorReadError); + return; + } + + const QLowEnergyServicePrivate::DescData &dscrDetails + = charDetails.descriptorList[descriptorHandle]; + + data.gattDescriptor = recoverNativeLeGattDescriptor( + service->startHandle, charHandle, descriptorHandle, dscrDetails); + + ThreadWorkerJob job; + job.operation = ThreadWorkerJob::ReadDescr; + job.data = QVariant::fromValue(data); + + QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection, + Q_ARG(ThreadWorkerJob, job)); +} + +void QLowEnergyControllerPrivateWin32::writeDescriptor( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descriptorHandle, + const QByteArray &newValue) +{ + Q_ASSERT(!service.isNull()); + if (!service->characteristicList.contains(charHandle)) + return; + + QLowEnergyServicePrivate::CharData &charDetails + = service->characteristicList[charHandle]; + if (!charDetails.descriptorList.contains(descriptorHandle)) + return; + + WriteDescData data; + data.systemErrorCode = NO_ERROR; + data.newValue = newValue; + data.hService = service->hService; + + if (data.systemErrorCode != NO_ERROR) { + qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString() + << ":" << qt_error_string(data.systemErrorCode); + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + + const QLowEnergyServicePrivate::DescData &dscrDetails + = charDetails.descriptorList[descriptorHandle]; + + data.gattDescriptor = recoverNativeLeGattDescriptor( + service->startHandle, charHandle, descriptorHandle, dscrDetails); + + ThreadWorkerJob job; + job.operation = ThreadWorkerJob::WriteDescr; + job.data = QVariant::fromValue(data); + + QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection, + Q_ARG(ThreadWorkerJob, job)); +} + +void QLowEnergyControllerPrivateWin32::addToGenericAttributeList(const QLowEnergyServiceData &, QLowEnergyHandle) +{ + Q_UNIMPLEMENTED(); +} + +void ThreadWorker::putJob(const ThreadWorkerJob &job) +{ + m_jobs.append(job); + if (m_jobs.count() == 1) + runPendingJob(); +} + +void ThreadWorker::runPendingJob() +{ + if (!m_jobs.count()) + return; + + ThreadWorkerJob job = m_jobs.first(); + + switch (job.operation) { + case ThreadWorkerJob::WriteChar: + { + WriteCharData data = job.data.value(); + setGattCharacteristicValue(data.hService, &data.gattCharacteristic, + data.newValue, data.flags, &data.systemErrorCode); + job.data = QVariant::fromValue(data); + } + break; + case ThreadWorkerJob::ReadChar: + { + ReadCharData data = job.data.value(); + data.value = getGattCharacteristicValue( + data.hService, &data.gattCharacteristic, &data.systemErrorCode); + job.data = QVariant::fromValue(data); + } + break; + case ThreadWorkerJob::WriteDescr: + { + WriteDescData data = job.data.value(); + setGattDescriptorValue(data.hService, &data.gattDescriptor, + data.newValue, &data.systemErrorCode); + job.data = QVariant::fromValue(data); + } + break; + case ThreadWorkerJob::ReadDescr: + { + ReadDescData data = job.data.value(); + data.value = getGattDescriptorValue( + data.hService, + const_cast(&data.gattDescriptor), + &data.systemErrorCode); + job.data = QVariant::fromValue(data); + } + break; + } + + m_jobs.removeFirst(); + emit jobFinished(job); +} + +QT_END_NAMESPACE diff --git a/qt-patches/windows/5.15.2/qlowenergycontroller_winrt.cpp b/qt-patches/windows/5.15.2/qlowenergycontroller_winrt.cpp new file mode 100644 index 000000000..f1fa1aaf5 --- /dev/null +++ b/qt-patches/windows/5.15.2/qlowenergycontroller_winrt.cpp @@ -0,0 +1,1162 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergycontroller_winrt_p.h" +#include "qbluetoothutils_winrt_p.h" + +#include +#include + +#ifdef CLASSIC_APP_BUILD +#define Q_OS_WINRT +#endif +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Devices; +using namespace ABI::Windows::Devices::Bluetooth; +using namespace ABI::Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Storage::Streams; + +QT_BEGIN_NAMESPACE + +typedef ITypedEventHandler StatusHandler; +typedef ITypedEventHandler ValueChangedHandler; +typedef GattReadClientCharacteristicConfigurationDescriptorResult ClientCharConfigDescriptorResult; +typedef IGattReadClientCharacteristicConfigurationDescriptorResult IClientCharConfigDescriptorResult; + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT) +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT_SERVICE_THREAD) + +static QByteArray byteArrayFromGattResult(const ComPtr &gattResult, bool isWCharString = false) +{ + ComPtr buffer; + HRESULT hr; + hr = gattResult->get_Value(&buffer); + Q_ASSERT_SUCCEEDED(hr); + return byteArrayFromBuffer(buffer, isWCharString); +} + +class QWinRTLowEnergyServiceHandler : public QObject +{ + Q_OBJECT +public: + QWinRTLowEnergyServiceHandler(const QBluetoothUuid &service, const ComPtr &deviceService) + : mService(service) + , mDeviceService(deviceService) + { + qCDebug(QT_BT_WINRT) << __FUNCTION__; + } + + ~QWinRTLowEnergyServiceHandler() + { + } + +public slots: + void obtainCharList() + { + QVector indicateChars; + quint16 startHandle = 0; + quint16 endHandle = 0; + qCDebug(QT_BT_WINRT) << __FUNCTION__; + ComPtr> characteristics; + HRESULT hr = mDeviceService->GetAllCharacteristics(&characteristics); + Q_ASSERT_SUCCEEDED(hr); + if (!characteristics) { + emit charListObtained(mService, mCharacteristicList, indicateChars, startHandle, endHandle); + QThread::currentThread()->quit(); + return; + } + + uint characteristicsCount; + hr = characteristics->get_Size(&characteristicsCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < characteristicsCount; ++i) { + ComPtr characteristic; + hr = characteristics->GetAt(i, &characteristic); + Q_ASSERT_SUCCEEDED(hr); + quint16 handle; + hr = characteristic->get_AttributeHandle(&handle); + Q_ASSERT_SUCCEEDED(hr); + QLowEnergyServicePrivate::CharData charData; + charData.valueHandle = handle + 1; + if (startHandle == 0 || startHandle > handle) + startHandle = handle; + if (endHandle == 0 || endHandle < handle) + endHandle = handle; + GUID guuid; + hr = characteristic->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + charData.uuid = QBluetoothUuid(guuid); + GattCharacteristicProperties properties; + hr = characteristic->get_CharacteristicProperties(&properties); + Q_ASSERT_SUCCEEDED(hr); + charData.properties = QLowEnergyCharacteristic::PropertyTypes(properties & 0xff); + if (charData.properties & QLowEnergyCharacteristic::Read) { + ComPtr> readOp; + hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + ComPtr readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + if (readResult) + charData.value = byteArrayFromGattResult(readResult); + } + ComPtr characteristic2; + hr = characteristic.As(&characteristic2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> descriptors; + hr = characteristic2->GetAllDescriptors(&descriptors); + Q_ASSERT_SUCCEEDED(hr); + uint descriptorCount; + hr = descriptors->get_Size(&descriptorCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint j = 0; j < descriptorCount; ++j) { + QLowEnergyServicePrivate::DescData descData; + ComPtr descriptor; + hr = descriptors->GetAt(j, &descriptor); + Q_ASSERT_SUCCEEDED(hr); + quint16 descHandle; + hr = descriptor->get_AttributeHandle(&descHandle); + Q_ASSERT_SUCCEEDED(hr); + GUID descriptorUuid; + hr = descriptor->get_Uuid(&descriptorUuid); + Q_ASSERT_SUCCEEDED(hr); + descData.uuid = QBluetoothUuid(descriptorUuid); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + ComPtr> readOp; + hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp); + Q_ASSERT_SUCCEEDED(hr); + ComPtr readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + GattClientCharacteristicConfigurationDescriptorValue value; + hr = readResult->get_ClientCharacteristicConfigurationDescriptor(&value); + Q_ASSERT_SUCCEEDED(hr); + quint16 result = 0; + bool correct = false; + if (value & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + result |= GattClientCharacteristicConfigurationDescriptorValue_Indicate; + correct = true; + } + if (value & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + result |= GattClientCharacteristicConfigurationDescriptorValue_Notify; + correct = true; + } + if (value == GattClientCharacteristicConfigurationDescriptorValue_None) { + correct = true; + } + if (!correct) + continue; + + descData.value = QByteArray(2, Qt::Uninitialized); + qToLittleEndian(result, descData.value.data()); + indicateChars << charData.uuid; + } else { + ComPtr> readOp; + hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + ComPtr readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + if (descData.uuid == QBluetoothUuid::CharacteristicUserDescription) + descData.value = byteArrayFromGattResult(readResult, true); + else + descData.value = byteArrayFromGattResult(readResult); + } + charData.descriptorList.insert(descHandle, descData); + } + mCharacteristicList.insert(handle, charData); + } + emit charListObtained(mService, mCharacteristicList, indicateChars, startHandle, endHandle); + QThread::currentThread()->quit(); + } + +public: + QBluetoothUuid mService; + ComPtr mDeviceService; + QHash mCharacteristicList; + +signals: + void charListObtained(const QBluetoothUuid &service, QHash charList, + QVector indicateChars, + QLowEnergyHandle startHandle, QLowEnergyHandle endHandle); +}; + +QLowEnergyControllerPrivateWinRT::QLowEnergyControllerPrivateWinRT() + : QLowEnergyControllerPrivate() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + + registerQLowEnergyControllerMetaType(); + connect(this, &QLowEnergyControllerPrivateWinRT::characteristicChanged, + this, &QLowEnergyControllerPrivateWinRT::handleCharacteristicChanged, + Qt::QueuedConnection); +} + +QLowEnergyControllerPrivateWinRT::~QLowEnergyControllerPrivateWinRT() +{ + if (mDevice && mStatusChangedToken.value) + mDevice->remove_ConnectionStatusChanged(mStatusChangedToken); + + unregisterFromValueChanges(); +} + +void QLowEnergyControllerPrivateWinRT::init() +{ +} + +void QLowEnergyControllerPrivateWinRT::connectToDevice() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + Q_Q(QLowEnergyController); + if (remoteDevice.isNull()) { + qWarning() << "Invalid/null remote device address"; + setError(QLowEnergyController::UnknownRemoteDeviceError); + return; + } + + setState(QLowEnergyController::ConnectingState); + + ComPtr deviceStatics; + HRESULT hr = GetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Bluetooth_BluetoothLEDevice).Get(), &deviceStatics); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> deviceFromIdOperation; + hr = deviceStatics->FromBluetoothAddressAsync(remoteDevice.toUInt64(), &deviceFromIdOperation); + Q_ASSERT_SUCCEEDED(hr); + hr = QWinRTFunctions::await(deviceFromIdOperation, mDevice.GetAddressOf()); + Q_ASSERT_SUCCEEDED(hr); + + if (!mDevice) { + qCDebug(QT_BT_WINRT) << "Could not find LE device"; + setError(QLowEnergyController::InvalidBluetoothAdapterError); + setState(QLowEnergyController::UnconnectedState); + return; + } + BluetoothConnectionStatus status; + hr = mDevice->get_ConnectionStatus(&status); + Q_ASSERT_SUCCEEDED(hr); + hr = QEventDispatcherWinRT::runOnXamlThread([this, q]() { + HRESULT hr; + hr = mDevice->add_ConnectionStatusChanged(Callback([this, q](IBluetoothLEDevice *dev, IInspectable *) { + BluetoothConnectionStatus status; + HRESULT hr; + hr = dev->get_ConnectionStatus(&status); + Q_ASSERT_SUCCEEDED(hr); + if (state == QLowEnergyController::ConnectingState + && status == BluetoothConnectionStatus::BluetoothConnectionStatus_Connected) { + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + } else if (state != QLowEnergyController::UnconnectedState + && status == BluetoothConnectionStatus::BluetoothConnectionStatus_Disconnected) { + invalidateServices(); + unregisterFromValueChanges(); + setError(QLowEnergyController::RemoteHostClosedError); + setState(QLowEnergyController::UnconnectedState); + emit q->disconnected(); + } + return S_OK; + }).Get(), &mStatusChangedToken); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); + + if (status == BluetoothConnectionStatus::BluetoothConnectionStatus_Connected) { + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + return; + } + + ComPtr> deviceServices; + hr = mDevice->get_GattServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + // Windows Phone automatically connects to the device as soon as a service value is read/written. + // Thus we read one value in order to establish the connection. + for (uint i = 0; i < serviceCount; ++i) { + ComPtr service; + hr = deviceServices->GetAt(i, &service); + Q_ASSERT_SUCCEEDED(hr); + ComPtr service2; + hr = service.As(&service2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> characteristics; + hr = service2->GetAllCharacteristics(&characteristics); + if (hr == E_ACCESSDENIED) { + // Everything will work as expected up until this point if the manifest capabilties + // for bluetooth LE are not set. + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic list. Please check your " + "manifest capabilities"; + setState(QLowEnergyController::UnconnectedState); + setError(QLowEnergyController::ConnectionError); + return; + } else { + Q_ASSERT_SUCCEEDED(hr); + } + uint characteristicsCount; + hr = characteristics->get_Size(&characteristicsCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint j = 0; j < characteristicsCount; ++j) { + ComPtr characteristic; + hr = characteristics->GetAt(j, &characteristic); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> op; + GattCharacteristicProperties props; + hr = characteristic->get_CharacteristicProperties(&props); + Q_ASSERT_SUCCEEDED(hr); + if (!(props & GattCharacteristicProperties_Read)) + continue; + /* QZ rviola + hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode::BluetoothCacheMode_Uncached, &op); + Q_ASSERT_SUCCEEDED(hr); + ComPtr result; + hr = QWinRTFunctions::await(op, result.GetAddressOf()); + if (hr == E_INVALIDARG) { + // E_INVALIDARG happens when user tries to connect to a device that was paired + // before but is not available. + qCDebug(QT_BT_WINRT) << "Could not obtain characteristic read result that triggers" + "device connection. Is the device reachable?"; + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + return; + } else if (hr != S_OK) { + qCWarning(QT_BT_WINRT) << "Connecting to device failed: " + << qt_error_string(hr); + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + return; + } + ComPtr buffer; + hr = result->get_Value(&buffer); + Q_ASSERT_SUCCEEDED(hr); + if (!buffer) { + qCDebug(QT_BT_WINRT) << "Problem reading value"; + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + } + */ + return; + } + } +} + +void QLowEnergyControllerPrivateWinRT::disconnectFromDevice() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + Q_Q(QLowEnergyController); + setState(QLowEnergyController::ClosingState); + unregisterFromValueChanges(); + if (mDevice) { + if (mStatusChangedToken.value) { + mDevice->remove_ConnectionStatusChanged(mStatusChangedToken); + mStatusChangedToken.value = 0; + } + mDevice = nullptr; + } + setState(QLowEnergyController::UnconnectedState); + emit q->disconnected(); +} + +ComPtr QLowEnergyControllerPrivateWinRT::getNativeService(const QBluetoothUuid &serviceUuid) +{ + ComPtr deviceService; + HRESULT hr; + hr = mDevice->GetGattService(serviceUuid, &deviceService); + if (FAILED(hr)) + qCDebug(QT_BT_WINRT) << "Could not obtain native service for Uuid" << serviceUuid; + return deviceService; +} + +ComPtr QLowEnergyControllerPrivateWinRT::getNativeCharacteristic(const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid) +{ + ComPtr service = getNativeService(serviceUuid); + if (!service) + return nullptr; + + ComPtr> characteristics; + HRESULT hr = service->GetCharacteristics(charUuid, &characteristics); + RETURN_IF_FAILED("Could not obtain native characteristics for service", return nullptr); + ComPtr characteristic; + hr = characteristics->GetAt(0, &characteristic); + RETURN_IF_FAILED("Could not obtain first characteristic for service", return nullptr); + return characteristic; +} + +void QLowEnergyControllerPrivateWinRT::registerForValueChanges(const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid) +{ + qCDebug(QT_BT_WINRT) << "Registering characteristic" << charUuid << "in service" + << serviceUuid << "for value changes"; + for (const ValueChangedEntry &entry : qAsConst(mValueChangedTokens)) { + GUID guuid; + HRESULT hr; + hr = entry.characteristic->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + if (QBluetoothUuid(guuid) == charUuid) + return; + } + ComPtr characteristic = getNativeCharacteristic(serviceUuid, charUuid); + + EventRegistrationToken token; + HRESULT hr; + hr = characteristic->add_ValueChanged(Callback([this](IGattCharacteristic *characteristic, IGattValueChangedEventArgs *args) { + HRESULT hr; + quint16 handle; + hr = characteristic->get_AttributeHandle(&handle); + Q_ASSERT_SUCCEEDED(hr); + ComPtr buffer; + hr = args->get_CharacteristicValue(&buffer); + Q_ASSERT_SUCCEEDED(hr); + emit characteristicChanged(handle, byteArrayFromBuffer(buffer)); + return S_OK; + }).Get(), &token); + Q_ASSERT_SUCCEEDED(hr); + mValueChangedTokens.append(ValueChangedEntry(characteristic, token)); + qCDebug(QT_BT_WINRT) << "Characteristic" << charUuid << "in service" + << serviceUuid << "registered for value changes"; +} + +void QLowEnergyControllerPrivateWinRT::unregisterFromValueChanges() +{ + qCDebug(QT_BT_WINRT) << "Unregistering " << mValueChangedTokens.count() << " value change tokens"; + HRESULT hr; + for (const ValueChangedEntry &entry : qAsConst(mValueChangedTokens)) { + hr = entry.characteristic->remove_ValueChanged(entry.token); + Q_ASSERT_SUCCEEDED(hr); + } + mValueChangedTokens.clear(); +} + +void QLowEnergyControllerPrivateWinRT::obtainIncludedServices(QSharedPointer servicePointer, + ComPtr service) +{ + Q_Q(QLowEnergyController); + ComPtr service2; + HRESULT hr = service.As(&service2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> includedServices; + hr = service2->GetAllIncludedServices(&includedServices); + // Some devices return ERROR_ACCESS_DISABLED_BY_POLICY + if (FAILED(hr)) + return; + + uint count; + hr = includedServices->get_Size(&count); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < count; ++i) { + ComPtr includedService; + hr = includedServices->GetAt(i, &includedService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = includedService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + const QBluetoothUuid includedUuid(guuid); + QSharedPointer includedPointer; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ + << "Changing service pointer from thread" + << QThread::currentThread(); + if (serviceList.contains(includedUuid)) { + includedPointer = serviceList.value(includedUuid); + } else { + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = includedUuid; + priv->setController(this); + + includedPointer = QSharedPointer(priv); + serviceList.insert(includedUuid, includedPointer); + } + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ + << "Changing service pointer from thread" + << QThread::currentThread(); + includedPointer->type |= QLowEnergyService::IncludedService; + servicePointer->includedServices.append(includedUuid); + + obtainIncludedServices(includedPointer, includedService); + + emit q->serviceDiscovered(includedUuid); + } +} + +void QLowEnergyControllerPrivateWinRT::discoverServices() +{ + Q_Q(QLowEnergyController); + + qCDebug(QT_BT_WINRT) << "Service discovery initiated"; + ComPtr> deviceServices; + HRESULT hr = mDevice->get_GattServices(&deviceServices); + Q_ASSERT_SUCCEEDED(hr); + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < serviceCount; ++i) { + ComPtr deviceService; + hr = deviceServices->GetAt(i, &deviceService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = deviceService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + const QBluetoothUuid service(guuid); + + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ + << "Changing service pointer from thread" + << QThread::currentThread(); + QSharedPointer pointer; + if (serviceList.contains(service)) { + pointer = serviceList.value(service); + } else { + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = service; + priv->setController(this); + + pointer = QSharedPointer(priv); + serviceList.insert(service, pointer); + } + pointer->type |= QLowEnergyService::PrimaryService; + + obtainIncludedServices(pointer, deviceService); + + emit q->serviceDiscovered(service); + } + + setState(QLowEnergyController::DiscoveredState); + emit q->discoveryFinished(); +} + +void QLowEnergyControllerPrivateWinRT::discoverServiceDetails(const QBluetoothUuid &service) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service; + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINRT) << "Discovery done of unknown service:" + << service.toString(); + return; + } + + ComPtr deviceService = getNativeService(service); + if (!deviceService) { + qCDebug(QT_BT_WINRT) << "Could not obtain native service for uuid " << service; + return; + } + + //update service data + QSharedPointer pointer = serviceList.value(service); + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + + pointer->setState(QLowEnergyService::DiscoveringServices); + ComPtr deviceService2; + HRESULT hr = deviceService.As(&deviceService2); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> deviceServices; + hr = deviceService2->GetAllIncludedServices(&deviceServices); + if (FAILED(hr)) { // ERROR_ACCESS_DISABLED_BY_POLICY + qCDebug(QT_BT_WINRT) << "Could not obtain included services list for" << service; + pointer->setError(QLowEnergyService::UnknownError); + pointer->setState(QLowEnergyService::InvalidService); + return; + } + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + Q_ASSERT_SUCCEEDED(hr); + for (uint i = 0; i < serviceCount; ++i) { + ComPtr includedService; + hr = deviceServices->GetAt(i, &includedService); + Q_ASSERT_SUCCEEDED(hr); + GUID guuid; + hr = includedService->get_Uuid(&guuid); + Q_ASSERT_SUCCEEDED(hr); + + const QBluetoothUuid service(guuid); + if (service.isNull()) { + qCDebug(QT_BT_WINRT) << "Could not find service"; + return; + } + + pointer->includedServices.append(service); + + // update the type of the included service + QSharedPointer otherService = serviceList.value(service); + if (!otherService.isNull()) + otherService->type |= QLowEnergyService::IncludedService; + } + + QWinRTLowEnergyServiceHandler *worker = new QWinRTLowEnergyServiceHandler(service, deviceService2); + QThread *thread = new QThread; + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &QWinRTLowEnergyServiceHandler::obtainCharList); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, &QThread::finished, worker, &QObject::deleteLater); + connect(worker, &QWinRTLowEnergyServiceHandler::charListObtained, + [this, thread](const QBluetoothUuid &service, QHash charList + , QVector indicateChars + , QLowEnergyHandle startHandle, QLowEnergyHandle endHandle) { + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINRT) << "Discovery done of unknown service:" + << service.toString(); + return; + } + + QSharedPointer pointer = serviceList.value(service); + pointer->startHandle = startHandle; + pointer->endHandle = endHandle; + pointer->characteristicList = charList; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([indicateChars, service, this]() { + for (const QBluetoothUuid &indicateChar : qAsConst(indicateChars)) + registerForValueChanges(service, indicateChar); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); + + pointer->setState(QLowEnergyService::ServiceDiscovered); + thread->exit(0); + }); + thread->start(); +} + +void QLowEnergyControllerPrivateWinRT::startAdvertising(const QLowEnergyAdvertisingParameters &, const QLowEnergyAdvertisingData &, const QLowEnergyAdvertisingData &) +{ + setError(QLowEnergyController::AdvertisingError); + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRT::stopAdvertising() +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRT::requestConnectionUpdate(const QLowEnergyConnectionParameters &) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRT::readCharacteristic(const QSharedPointer service, + const QLowEnergyHandle charHandle) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::CharacteristicReadError); + Q_UNIMPLEMENTED(); + return; + } + + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << charHandle << "could not be found in service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicReadError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, service, this]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + if (!(charData.properties & QLowEnergyCharacteristic::Read)) + qCDebug(QT_BT_WINRT) << "Read flag is not set for characteristic" << charData.uuid; + + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + ComPtr> readOp; + HRESULT hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + auto readCompletedLambda = [charData, charHandle, service] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "read operation failed."; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + ComPtr characteristicValue; + HRESULT hr; + hr = op->GetResults(&characteristicValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for characteristic" << charHandle; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + + const QByteArray value = byteArrayFromGattResult(characteristicValue); + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + charData.value = value; + service->characteristicList.insert(charHandle, charData); + emit service->characteristicRead(QLowEnergyCharacteristic(service, charHandle), value); + return S_OK; + }; + hr = readOp->put_Completed(Callback>(readCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +void QLowEnergyControllerPrivateWinRT::readDescriptor(const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descHandle) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << descHandle; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::DescriptorReadError); + Q_UNIMPLEMENTED(); + return; + } + + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "in characteristic" << charHandle + << "cannot be found in service" << service->uuid; + service->setError(QLowEnergyService::DescriptorReadError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, descHandle, service, this]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + + // Get native descriptor + if (!charData.descriptorList.contains(descHandle)) + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "cannot be found in characteristic" << charHandle; + const QLowEnergyServicePrivate::DescData descData = charData.descriptorList.value(descHandle); + const QBluetoothUuid descUuid = descData.uuid; + if (descUuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + ComPtr> readOp; + HRESULT hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp); + Q_ASSERT_SUCCEEDED(hr); + auto readCompletedLambda = [charHandle, descHandle, service] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "read operation failed"; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + ComPtr iValue; + HRESULT hr; + hr = op->GetResults(&iValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + GattClientCharacteristicConfigurationDescriptorValue value; + hr = iValue->get_ClientCharacteristicConfigurationDescriptor(&value); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain value for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + quint16 result = 0; + bool correct = false; + if (value & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + result |= QLowEnergyCharacteristic::Indicate; + correct = true; + } + if (value & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + result |= QLowEnergyCharacteristic::Notify; + correct = true; + } + if (value == GattClientCharacteristicConfigurationDescriptorValue_None) + correct = true; + if (!correct) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle + << "read operation failed. Obtained unexpected value."; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + QLowEnergyServicePrivate::DescData descData; + descData.uuid = QBluetoothUuid::ClientCharacteristicConfiguration; + descData.value = QByteArray(2, Qt::Uninitialized); + qToLittleEndian(result, descData.value.data()); + service->characteristicList[charHandle].descriptorList[descHandle] = descData; + emit service->descriptorRead(QLowEnergyDescriptor(service, charHandle, descHandle), + descData.value); + return S_OK; + }; + hr = readOp->put_Completed(Callback>(readCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + } else { + ComPtr> descriptors; + HRESULT hr = characteristic->GetDescriptors(descData.uuid, &descriptors); + Q_ASSERT_SUCCEEDED(hr); + ComPtr descriptor; + hr = descriptors->GetAt(0, &descriptor); + Q_ASSERT_SUCCEEDED(hr); + ComPtr> readOp; + hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + Q_ASSERT_SUCCEEDED(hr); + auto readCompletedLambda = [charHandle, descHandle, descUuid, service] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "read operation failed"; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + ComPtr descriptorValue; + HRESULT hr; + hr = op->GetResults(&descriptorValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + QLowEnergyServicePrivate::DescData descData; + if (descUuid == QBluetoothUuid::CharacteristicUserDescription) + descData.value = byteArrayFromGattResult(descriptorValue, true); + else + descData.value = byteArrayFromGattResult(descriptorValue); + service->characteristicList[charHandle].descriptorList[descHandle] = descData; + emit service->descriptorRead(QLowEnergyDescriptor(service, charHandle, descHandle), + descData.value); + return S_OK; + }; + hr = readOp->put_Completed(Callback>(readCompletedLambda).Get()); + return S_OK; + } + }); + Q_ASSERT_SUCCEEDED(hr); +} + +void QLowEnergyControllerPrivateWinRT::writeCharacteristic(const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QByteArray &newValue, + QLowEnergyService::WriteMode mode) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << newValue << mode; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::CharacteristicWriteError); + Q_UNIMPLEMENTED(); + return; + } + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "cannot be found in service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicWriteError); + return; + } + + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + const bool writeWithResponse = mode == QLowEnergyService::WriteWithResponse; + if (!(charData.properties & (writeWithResponse ? QLowEnergyCharacteristic::Write : QLowEnergyCharacteristic::WriteNoResponse))) + qCDebug(QT_BT_WINRT) << "Write flag is not set for characteristic" << charHandle; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charData, charHandle, this, service, newValue, writeWithResponse]() { + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + ComPtr bufferFactory; + HRESULT hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), &bufferFactory); + Q_ASSERT_SUCCEEDED(hr); + ComPtr buffer; + const int length = newValue.length(); + hr = bufferFactory->Create(length, &buffer); + Q_ASSERT_SUCCEEDED(hr); + hr = buffer->put_Length(length); + Q_ASSERT_SUCCEEDED(hr); + ComPtr byteAccess; + hr = buffer.As(&byteAccess); + Q_ASSERT_SUCCEEDED(hr); + byte *bytes; + hr = byteAccess->Buffer(&bytes); + Q_ASSERT_SUCCEEDED(hr); + memcpy(bytes, newValue, length); + ComPtr> writeOp; + GattWriteOption option = writeWithResponse ? GattWriteOption_WriteWithResponse : GattWriteOption_WriteWithoutResponse; + hr = characteristic->WriteValueWithOptionAsync(buffer.Get(), option, &writeOp); + Q_ASSERT_SUCCEEDED(hr); + auto writeCompletedLambda =[charData, charHandle, newValue, service, writeWithResponse, this] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation failed"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (hr == E_BLUETOOTH_ATT_INVALID_ATTRIBUTE_VALUE_LENGTH) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation was tried with invalid value length"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + Q_ASSERT_SUCCEEDED(hr); + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation failed"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + // only update cache when property is readable. Otherwise it remains + // empty. + if (charData.properties & QLowEnergyCharacteristic::Read) + updateValueOfCharacteristic(charHandle, newValue, false); + if (writeWithResponse) + emit service->characteristicWritten(QLowEnergyCharacteristic(service, charHandle), newValue); + return S_OK; + }; + hr = writeOp->put_Completed(Callback>(writeCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + +void QLowEnergyControllerPrivateWinRT::writeDescriptor( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descHandle, + const QByteArray &newValue) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << descHandle << newValue; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::DescriptorWriteError); + Q_UNIMPLEMENTED(); + return; + } + + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "in characteristic" << charHandle + << "could not be found in service" << service->uuid; + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, descHandle, this, service, newValue]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + + // Get native descriptor + if (!charData.descriptorList.contains(descHandle)) + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "could not be found in Characteristic" << charHandle; + + QLowEnergyServicePrivate::DescData descData = charData.descriptorList.value(descHandle); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + GattClientCharacteristicConfigurationDescriptorValue value; + quint16 intValue = qFromLittleEndian(newValue); + if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Indicate && intValue & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + qCWarning(QT_BT_WINRT) << "Setting both Indicate and Notify is not supported on WinRT"; + value = (GattClientCharacteristicConfigurationDescriptorValue)(GattClientCharacteristicConfigurationDescriptorValue_Indicate | GattClientCharacteristicConfigurationDescriptorValue_Notify); + } else if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + value = GattClientCharacteristicConfigurationDescriptorValue_Indicate; + } else if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + value = GattClientCharacteristicConfigurationDescriptorValue_Notify; + } else if (intValue == 0) { + value = GattClientCharacteristicConfigurationDescriptorValue_None; + } else { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed: Invalid value"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + ComPtr> writeOp; + HRESULT hr = characteristic->WriteClientCharacteristicConfigurationDescriptorAsync(value, &writeOp); + Q_ASSERT_SUCCEEDED(hr); + auto writeCompletedLambda = [charHandle, descHandle, newValue, service, this] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + updateValueOfDescriptor(charHandle, descHandle, newValue, false); + emit service->descriptorWritten(QLowEnergyDescriptor(service, charHandle, descHandle), newValue); + return S_OK; + }; + hr = writeOp->put_Completed(Callback>(writeCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + } else { + ComPtr> descriptors; + HRESULT hr = characteristic->GetDescriptors(descData.uuid, &descriptors); + Q_ASSERT_SUCCEEDED(hr); + ComPtr descriptor; + hr = descriptors->GetAt(0, &descriptor); + Q_ASSERT_SUCCEEDED(hr); + ComPtr bufferFactory; + hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), &bufferFactory); + Q_ASSERT_SUCCEEDED(hr); + ComPtr buffer; + const int length = newValue.length(); + hr = bufferFactory->Create(length, &buffer); + Q_ASSERT_SUCCEEDED(hr); + hr = buffer->put_Length(length); + Q_ASSERT_SUCCEEDED(hr); + ComPtr byteAccess; + hr = buffer.As(&byteAccess); + Q_ASSERT_SUCCEEDED(hr); + byte *bytes; + hr = byteAccess->Buffer(&bytes); + Q_ASSERT_SUCCEEDED(hr); + memcpy(bytes, newValue, length); + ComPtr> writeOp; + hr = descriptor->WriteValueAsync(buffer.Get(), &writeOp); + Q_ASSERT_SUCCEEDED(hr); + auto writeCompletedLambda = [charHandle, descHandle, newValue, service, this] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + updateValueOfDescriptor(charHandle, descHandle, newValue, false); + emit service->descriptorWritten(QLowEnergyDescriptor(service, charHandle, descHandle), newValue); + return S_OK; + }; + hr = writeOp->put_Completed(Callback>(writeCompletedLambda).Get()); + Q_ASSERT_SUCCEEDED(hr); + return S_OK; + } + return S_OK; + }); + Q_ASSERT_SUCCEEDED(hr); +} + + +void QLowEnergyControllerPrivateWinRT::addToGenericAttributeList(const QLowEnergyServiceData &, QLowEnergyHandle) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRT::handleCharacteristicChanged( + quint16 charHandle, const QByteArray &data) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << charHandle << data; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + QSharedPointer service = + serviceForHandle(charHandle); + if (service.isNull()) + return; + + qCDebug(QT_BT_WINRT) << "Characteristic change notification" << service->uuid + << charHandle << data.toHex(); + + QLowEnergyCharacteristic characteristic = characteristicForHandle(charHandle); + if (!characteristic.isValid()) { + qCWarning(QT_BT_WINRT) << "characteristicChanged: Cannot find characteristic"; + return; + } + + // only update cache when property is readable. Otherwise it remains + // empty. + if (characteristic.properties() & QLowEnergyCharacteristic::Read) + updateValueOfCharacteristic(characteristic.attributeHandle(), + data, false); + emit service->characteristicChanged(characteristic, data); +} + +QT_END_NAMESPACE + +#include "qlowenergycontroller_winrt.moc" diff --git a/qt-patches/windows/5.15.2/qlowenergycontroller_winrt_new.cpp b/qt-patches/windows/5.15.2/qlowenergycontroller_winrt_new.cpp new file mode 100644 index 000000000..1398316c9 --- /dev/null +++ b/qt-patches/windows/5.15.2/qlowenergycontroller_winrt_new.cpp @@ -0,0 +1,1738 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlowenergycontroller_winrt_new_p.h" +#include "qlowenergycontroller_winrt_p.h" +#include "qbluetoothutils_winrt_p.h" + +#include +#include +#include +#include + +#ifdef CLASSIC_APP_BUILD +#define Q_OS_WINRT +#endif +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Foundation::Metadata; +using namespace ABI::Windows::Devices; +using namespace ABI::Windows::Devices::Bluetooth; +using namespace ABI::Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Storage::Streams; + +QT_BEGIN_NAMESPACE + +typedef ITypedEventHandler StatusHandler; +typedef ITypedEventHandler ValueChangedHandler; +typedef GattReadClientCharacteristicConfigurationDescriptorResult ClientCharConfigDescriptorResult; +typedef IGattReadClientCharacteristicConfigurationDescriptorResult IClientCharConfigDescriptorResult; + +#define EMIT_WORKER_ERROR_AND_QUIT_IF_FAILED(hr, ret) \ + if (FAILED(hr)) { \ + emitErrorAndQuitThread(hr); \ + ret; \ + } + +#define WARN_AND_CONTINUE_IF_FAILED(hr, msg) \ + if (FAILED(hr)) { \ + qCWarning(QT_BT_WINRT) << msg; \ + continue; \ + } + +#define CHECK_FOR_DEVICE_CONNECTION_ERROR_IMPL(this, hr, msg, ret) \ + if (FAILED(hr)) { \ + qCWarning(QT_BT_WINRT) << msg; \ + this->unregisterFromStatusChanges(); \ + this->setError(QLowEnergyController::ConnectionError); \ + this->setState(QLowEnergyController::UnconnectedState); \ + ret; \ + } + +#define CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, msg, ret) \ + CHECK_FOR_DEVICE_CONNECTION_ERROR_IMPL(this, hr, msg, ret) + +#define CHECK_HR_AND_SET_SERVICE_ERROR(hr, msg, service, error, ret) \ + if (FAILED(hr)) { \ + qCDebug(QT_BT_WINRT) << msg; \ + service->setError(error); \ + ret; \ + } + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT) +Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT_SERVICE_THREAD) + +QLowEnergyControllerPrivate *createWinRTLowEnergyController() +{ + if (supportsNewLEApi()) { + qCDebug(QT_BT_WINRT) << "Using new low energy controller"; + return new QLowEnergyControllerPrivateWinRTNew(); + } + + qCDebug(QT_BT_WINRT) << "Using pre 15063 low energy controller"; + return new QLowEnergyControllerPrivateWinRT(); +} + +static QByteArray byteArrayFromGattResult(const ComPtr &gattResult, + bool isWCharString = false) +{ + ComPtr buffer; + HRESULT hr; + hr = gattResult->get_Value(&buffer); + if (FAILED(hr) || !buffer) { + qCWarning(QT_BT_WINRT) << "Could not obtain buffer from GattReadResult"; + return QByteArray(); + } + return byteArrayFromBuffer(buffer, isWCharString); +} + +class QWinRTLowEnergyServiceHandlerNew : public QObject +{ + Q_OBJECT +public: + QWinRTLowEnergyServiceHandlerNew(const QBluetoothUuid &service, + const ComPtr &deviceService) + : mService(service) + , mDeviceService(deviceService) + { + qCDebug(QT_BT_WINRT) << __FUNCTION__; + } + + ~QWinRTLowEnergyServiceHandlerNew() + { + } + +public slots: + void obtainCharList() + { + mIndicateChars.clear(); + qCDebug(QT_BT_WINRT) << __FUNCTION__; + ComPtr> characteristicsOp; + ComPtr characteristicsResult; + HRESULT hr = mDeviceService->GetCharacteristicsAsync(&characteristicsOp); + EMIT_WORKER_ERROR_AND_QUIT_IF_FAILED(hr, return); + hr = QWinRTFunctions::await(characteristicsOp, characteristicsResult.GetAddressOf(), + QWinRTFunctions::ProcessMainThreadEvents, 5000); + EMIT_WORKER_ERROR_AND_QUIT_IF_FAILED(hr, return); + GattCommunicationStatus status; + hr = characteristicsResult->get_Status(&status); + EMIT_WORKER_ERROR_AND_QUIT_IF_FAILED(hr, return); + if (status != GattCommunicationStatus_Success) { + emitErrorAndQuitThread(QLatin1String("Could not obtain char list")); + return; + } + ComPtr> characteristics; + hr = characteristicsResult->get_Characteristics(&characteristics); + EMIT_WORKER_ERROR_AND_QUIT_IF_FAILED(hr, return); + + uint characteristicsCount; + hr = characteristics->get_Size(&characteristicsCount); + EMIT_WORKER_ERROR_AND_QUIT_IF_FAILED(hr, return); + + mCharacteristicsCountToBeDiscovered = characteristicsCount; + for (uint i = 0; i < characteristicsCount; ++i) { + ComPtr characteristic; + hr = characteristics->GetAt(i, &characteristic); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic at" << i; + --mCharacteristicsCountToBeDiscovered; + continue; + } + + ComPtr characteristic3; + hr = characteristic.As(&characteristic3); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not cast characteristic"; + --mCharacteristicsCountToBeDiscovered; + continue; + } + + // For some strange reason, Windows doesn't discover descriptors of characteristics (if not paired). + // Qt API assumes that all characteristics and their descriptors are discovered in one go. + // So we start 'GetDescriptorsAsync' for each discovered characteristic and finish only + // when GetDescriptorsAsync for all characteristics return. + ComPtr> descAsyncResult; + hr = characteristic3->GetDescriptorsAsync(&descAsyncResult); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain list of descriptors"; + --mCharacteristicsCountToBeDiscovered; + continue; + } + hr = descAsyncResult->put_Completed( + Callback>( + [this, characteristic] + (IAsyncOperation *op, + AsyncStatus status) { + if (status != AsyncStatus::Completed) { + qCWarning(QT_BT_WINRT) << "Descriptor operation unsuccessful"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + quint16 handle; + + HRESULT hr = characteristic->get_AttributeHandle(&handle); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic's attribute handle"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + QLowEnergyServicePrivate::CharData charData; + charData.valueHandle = handle + 1; + if (mStartHandle == 0 || mStartHandle > handle) + mStartHandle = handle; + if (mEndHandle == 0 || mEndHandle < handle) + mEndHandle = handle; + GUID guuid; + hr = characteristic->get_Uuid(&guuid); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic's Uuid"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + charData.uuid = QBluetoothUuid(guuid); + GattCharacteristicProperties properties; + hr = characteristic->get_CharacteristicProperties(&properties); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic's properties"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + charData.properties = QLowEnergyCharacteristic::PropertyTypes(properties & 0xff); + if (charData.properties & QLowEnergyCharacteristic::Read) { + ComPtr> readOp; + hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, + &readOp); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not read characteristic"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + ComPtr readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic read result"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + if (!readResult) + qCWarning(QT_BT_WINRT) << "Characteristic read result is null"; + else + charData.value = byteArrayFromGattResult(readResult); + } + mCharacteristicList.insert(handle, charData); + + ComPtr> descriptors; + + ComPtr result; + hr = op->GetResults(&result); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain descriptor read result"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + GattCommunicationStatus commStatus; + hr = result->get_Status(&commStatus); + if (FAILED(hr) || commStatus != GattCommunicationStatus_Success) { + qCWarning(QT_BT_WINRT) << "Descriptor operation failed"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + + hr = result->get_Descriptors(&descriptors); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain list of descriptors"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + + uint descriptorCount; + hr = descriptors->get_Size(&descriptorCount); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not obtain list of descriptors' size"; + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + } + for (uint j = 0; j < descriptorCount; ++j) { + QLowEnergyServicePrivate::DescData descData; + ComPtr descriptor; + hr = descriptors->GetAt(j, &descriptor); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain descriptor") + quint16 descHandle; + hr = descriptor->get_AttributeHandle(&descHandle); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain descriptor's attribute handle") + GUID descriptorUuid; + hr = descriptor->get_Uuid(&descriptorUuid); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain descriptor's Uuid") + descData.uuid = QBluetoothUuid(descriptorUuid); + charData.descriptorList.insert(descHandle, descData); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + ComPtr> readOp; + hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not read descriptor value") + ComPtr readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not await descriptor read result") + GattClientCharacteristicConfigurationDescriptorValue value; + hr = readResult->get_ClientCharacteristicConfigurationDescriptor(&value); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not get descriptor value from result") + quint16 result = 0; + bool correct = false; + if (value & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + result |= GattClientCharacteristicConfigurationDescriptorValue_Indicate; + correct = true; + } + if (value & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + result |= GattClientCharacteristicConfigurationDescriptorValue_Notify; + correct = true; + } + if (value == GattClientCharacteristicConfigurationDescriptorValue_None) { + correct = true; + } + if (!correct) + continue; + + descData.value = QByteArray(2, Qt::Uninitialized); + qToLittleEndian(result, descData.value.data()); + mIndicateChars << charData.uuid; + } else { + ComPtr> readOp; + hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, + &readOp); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not read descriptor value") + ComPtr readResult; + hr = QWinRTFunctions::await(readOp, readResult.GetAddressOf()); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could await descriptor read result") + if (descData.uuid == QBluetoothUuid::CharacteristicUserDescription) + descData.value = byteArrayFromGattResult(readResult, true); + else + descData.value = byteArrayFromGattResult(readResult); + } + charData.descriptorList.insert(descHandle, descData); + } + + mCharacteristicList.insert(handle, charData); + --mCharacteristicsCountToBeDiscovered; + checkAllCharacteristicsDiscovered(); + return S_OK; + }).Get()); + if (FAILED(hr)) { + qCWarning(QT_BT_WINRT) << "Could not register descriptor callback"; + --mCharacteristicsCountToBeDiscovered; + continue; + } + } + checkAllCharacteristicsDiscovered(); + } + +private: + bool checkAllCharacteristicsDiscovered(); + void emitErrorAndQuitThread(HRESULT hr); + void emitErrorAndQuitThread(const QString &error); + +public: + QBluetoothUuid mService; + ComPtr mDeviceService; + QHash mCharacteristicList; + uint mCharacteristicsCountToBeDiscovered; + quint16 mStartHandle = 0; + quint16 mEndHandle = 0; + QVector mIndicateChars; + +signals: + void charListObtained(const QBluetoothUuid &service, QHash charList, + QVector indicateChars, + QLowEnergyHandle startHandle, QLowEnergyHandle endHandle); + void errorOccured(const QString &error); +}; + +bool QWinRTLowEnergyServiceHandlerNew::checkAllCharacteristicsDiscovered() +{ + if (mCharacteristicsCountToBeDiscovered == 0) { + emit charListObtained(mService, mCharacteristicList, mIndicateChars, + mStartHandle, mEndHandle); + QThread::currentThread()->quit(); + return true; + } + + return false; +} + +void QWinRTLowEnergyServiceHandlerNew::emitErrorAndQuitThread(HRESULT hr) +{ + emitErrorAndQuitThread(qt_error_string(hr)); +} + +void QWinRTLowEnergyServiceHandlerNew::emitErrorAndQuitThread(const QString &error) +{ + emit errorOccured(error); + QThread::currentThread()->quit(); +} + +QLowEnergyControllerPrivateWinRTNew::QLowEnergyControllerPrivateWinRTNew() + : QLowEnergyControllerPrivate() +{ + registerQLowEnergyControllerMetaType(); + connect(this, &QLowEnergyControllerPrivateWinRTNew::characteristicChanged, + this, &QLowEnergyControllerPrivateWinRTNew::handleCharacteristicChanged, + Qt::QueuedConnection); +} + +QLowEnergyControllerPrivateWinRTNew::~QLowEnergyControllerPrivateWinRTNew() +{ + unregisterFromStatusChanges(); + unregisterFromValueChanges(); + mAbortPending = true; +} + +void QLowEnergyControllerPrivateWinRTNew::init() +{ +} + +void QLowEnergyControllerPrivateWinRTNew::connectToDevice() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + mAbortPending = false; + Q_Q(QLowEnergyController); + if (remoteDevice.isNull()) { + qWarning() << "Invalid/null remote device address"; + setError(QLowEnergyController::UnknownRemoteDeviceError); + return; + } + + setState(QLowEnergyController::ConnectingState); + + ComPtr deviceStatics; + HRESULT hr = GetActivationFactory( + HString::MakeReference(RuntimeClass_Windows_Devices_Bluetooth_BluetoothLEDevice).Get(), + &deviceStatics); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain device factory", return) + ComPtr> deviceFromIdOperation; + hr = deviceStatics->FromBluetoothAddressAsync(remoteDevice.toUInt64(), &deviceFromIdOperation); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not find LE device from address", return) + hr = QWinRTFunctions::await(deviceFromIdOperation, mDevice.GetAddressOf(), + QWinRTFunctions::ProcessMainThreadEvents, 5000); + if (FAILED(hr) || !mDevice) { + qCWarning(QT_BT_WINRT) << "Could not find LE device"; + setError(QLowEnergyController::InvalidBluetoothAdapterError); + setState(QLowEnergyController::UnconnectedState); + return; + } + BluetoothConnectionStatus status; + hr = mDevice->get_ConnectionStatus(&status); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain device's connection status", return) + if (status == BluetoothConnectionStatus::BluetoothConnectionStatus_Connected) { + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + return; + } + + QBluetoothLocalDevice localDevice; + QBluetoothLocalDevice::Pairing pairing = localDevice.pairingStatus(remoteDevice); + if (pairing == QBluetoothLocalDevice::Unpaired) + connectToUnpairedDevice(); + else + connectToPairedDevice(); +} + +void QLowEnergyControllerPrivateWinRTNew::disconnectFromDevice() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + Q_Q(QLowEnergyController); + setState(QLowEnergyController::ClosingState); + unregisterFromValueChanges(); + unregisterFromStatusChanges(); + mAbortPending = true; + mDevice = nullptr; + setState(QLowEnergyController::UnconnectedState); + emit q->disconnected(); +} + +ComPtr QLowEnergyControllerPrivateWinRTNew::getNativeService( + const QBluetoothUuid &serviceUuid) +{ + ComPtr deviceService; + HRESULT hr; + hr = mDevice->GetGattService(serviceUuid, &deviceService); + if (FAILED(hr)) + qCDebug(QT_BT_WINRT) << "Could not obtain native service for Uuid" << serviceUuid; + return deviceService; +} + +ComPtr QLowEnergyControllerPrivateWinRTNew::getNativeCharacteristic( + const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid) +{ + ComPtr service = getNativeService(serviceUuid); + if (!service) + return nullptr; + + ComPtr service3; + HRESULT hr = service.As(&service3); + RETURN_IF_FAILED("Could not cast service", return nullptr); + + ComPtr> op; + ComPtr result; + hr = service3->GetCharacteristicsForUuidAsync(charUuid, &op); + RETURN_IF_FAILED("Could not obtain native characteristics for service", return nullptr); + hr = QWinRTFunctions::await(op, result.GetAddressOf(), QWinRTFunctions::ProcessMainThreadEvents, 5000); + RETURN_IF_FAILED("Could not await completion of characteristic operation", return nullptr); + GattCommunicationStatus status; + hr = result->get_Status(&status); + if (FAILED(hr) || status != GattCommunicationStatus_Success) { + qErrnoWarning(hr, "Native characteristic operation failed."); + return nullptr; + } + ComPtr> characteristics; + hr = result->get_Characteristics(&characteristics); + RETURN_IF_FAILED("Could not obtain characteristic list.", return nullptr); + uint size; + hr = characteristics->get_Size(&size); + RETURN_IF_FAILED("Could not obtain characteristic list's size.", return nullptr); + if (size != 1) + qErrnoWarning("More than 1 characteristic found."); + ComPtr characteristic; + hr = characteristics->GetAt(0, &characteristic); + RETURN_IF_FAILED("Could not obtain first characteristic for service", return nullptr); + return characteristic; +} + +void QLowEnergyControllerPrivateWinRTNew::registerForValueChanges(const QBluetoothUuid &serviceUuid, + const QBluetoothUuid &charUuid) +{ + qCDebug(QT_BT_WINRT) << "Registering characteristic" << charUuid << "in service" + << serviceUuid << "for value changes"; + for (const ValueChangedEntry &entry : qAsConst(mValueChangedTokens)) { + GUID guuid; + HRESULT hr; + hr = entry.characteristic->get_Uuid(&guuid); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain characteristic's Uuid") + if (QBluetoothUuid(guuid) == charUuid) + return; + } + ComPtr characteristic = getNativeCharacteristic(serviceUuid, charUuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT).nospace() << "Could not obtain native characteristic " << charUuid + << " from service " << serviceUuid << ". Qt will not be able to signal" + << " changes for this characteristic."; + return; + } + + EventRegistrationToken token; + HRESULT hr; + hr = characteristic->add_ValueChanged( + Callback(this, &QLowEnergyControllerPrivateWinRTNew::onValueChange).Get(), + &token); + RETURN_IF_FAILED("Could not register characteristic for value changes", return) + mValueChangedTokens.append(ValueChangedEntry(characteristic, token)); + qCDebug(QT_BT_WINRT) << "Characteristic" << charUuid << "in service" + << serviceUuid << "registered for value changes"; +} + +void QLowEnergyControllerPrivateWinRTNew::unregisterFromValueChanges() +{ + qCDebug(QT_BT_WINRT) << "Unregistering " << mValueChangedTokens.count() << " value change tokens"; + HRESULT hr; + for (const ValueChangedEntry &entry : qAsConst(mValueChangedTokens)) { + if (!entry.characteristic) { + qCWarning(QT_BT_WINRT) << "Unregistering from value changes for characteristic failed." + << "Characteristic has been deleted"; + continue; + } + hr = entry.characteristic->remove_ValueChanged(entry.token); + if (FAILED(hr)) + qCWarning(QT_BT_WINRT) << "Unregistering from value changes for characteristic failed."; + } + mValueChangedTokens.clear(); +} + +HRESULT QLowEnergyControllerPrivateWinRTNew::onValueChange(IGattCharacteristic *characteristic, IGattValueChangedEventArgs *args) +{ + HRESULT hr; + quint16 handle; + hr = characteristic->get_AttributeHandle(&handle); + RETURN_IF_FAILED("Could not obtain characteristic's handle", return S_OK) + ComPtr buffer; + hr = args->get_CharacteristicValue(&buffer); + RETURN_IF_FAILED("Could not obtain characteristic's value", return S_OK) + emit characteristicChanged(handle, byteArrayFromBuffer(buffer)); + return S_OK; +} + +bool QLowEnergyControllerPrivateWinRTNew::registerForStatusChanges() +{ + if (!mDevice) + return false; + + qCDebug(QT_BT_WINRT) << __FUNCTION__; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([this]() { + HRESULT hr; + hr = mDevice->add_ConnectionStatusChanged( + Callback(this, &QLowEnergyControllerPrivateWinRTNew::onStatusChange).Get(), + &mStatusChangedToken); + RETURN_IF_FAILED("Could not register connection status callback", return hr) + return S_OK; + }); + RETURN_FALSE_IF_FAILED("Could not add status callback on Xaml thread") + return true; +} + +void QLowEnergyControllerPrivateWinRTNew::unregisterFromStatusChanges() +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__; + if (mDevice && mStatusChangedToken.value) { + mDevice->remove_ConnectionStatusChanged(mStatusChangedToken); + mStatusChangedToken.value = 0; + } +} + +HRESULT QLowEnergyControllerPrivateWinRTNew::onStatusChange(IBluetoothLEDevice *dev, IInspectable *) +{ + Q_Q(QLowEnergyController); + BluetoothConnectionStatus status; + HRESULT hr; + hr = dev->get_ConnectionStatus(&status); + RETURN_IF_FAILED("Could not obtain connection status", return S_OK) + if (state == QLowEnergyController::ConnectingState + && status == BluetoothConnectionStatus::BluetoothConnectionStatus_Connected) { + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + } else if (state != QLowEnergyController::UnconnectedState + && status == BluetoothConnectionStatus::BluetoothConnectionStatus_Disconnected) { + invalidateServices(); + unregisterFromValueChanges(); + unregisterFromStatusChanges(); + mDevice = nullptr; + setError(QLowEnergyController::RemoteHostClosedError); + setState(QLowEnergyController::UnconnectedState); + emit q->disconnected(); + } + return S_OK; +} + +void QLowEnergyControllerPrivateWinRTNew::obtainIncludedServices( + QSharedPointer servicePointer, + ComPtr service) +{ + Q_Q(QLowEnergyController); + ComPtr service3; + HRESULT hr = service.As(&service3); + RETURN_IF_FAILED("Could not cast service", return); + ComPtr> op; + hr = service3->GetIncludedServicesAsync(&op); + // Some devices return ERROR_ACCESS_DISABLED_BY_POLICY + RETURN_IF_FAILED("Could not obtain included services", return); + ComPtr result; + hr = QWinRTFunctions::await(op, result.GetAddressOf(), QWinRTFunctions::ProcessMainThreadEvents, 5000); + RETURN_IF_FAILED("Could not await service operation", return); + GattCommunicationStatus status; + hr = result->get_Status(&status); + if (FAILED(hr) || status != GattCommunicationStatus_Success) { + qErrnoWarning("Could not obtain list of included services"); + return; + } + ComPtr> includedServices; + hr = result->get_Services(&includedServices); + RETURN_IF_FAILED("Could not obtain service list", return); + + uint count; + hr = includedServices->get_Size(&count); + RETURN_IF_FAILED("Could not obtain service list's size", return); + for (uint i = 0; i < count; ++i) { + ComPtr includedService; + hr = includedServices->GetAt(i, &includedService); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain service from list"); + GUID guuid; + hr = includedService->get_Uuid(&guuid); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain included service's Uuid"); + const QBluetoothUuid includedUuid(guuid); + QSharedPointer includedPointer; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ + << "Changing service pointer from thread" + << QThread::currentThread(); + if (serviceList.contains(includedUuid)) { + includedPointer = serviceList.value(includedUuid); + } else { + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = includedUuid; + priv->setController(this); + + includedPointer = QSharedPointer(priv); + serviceList.insert(includedUuid, includedPointer); + } + includedPointer->type |= QLowEnergyService::IncludedService; + servicePointer->includedServices.append(includedUuid); + + obtainIncludedServices(includedPointer, includedService); + + emit q->serviceDiscovered(includedUuid); + } +} + +HRESULT QLowEnergyControllerPrivateWinRTNew::onServiceDiscoveryFinished(ABI::Windows::Foundation::IAsyncOperation *op, AsyncStatus status) +{ + Q_Q(QLowEnergyController); + if (status != AsyncStatus::Completed) { + qCDebug(QT_BT_WINRT) << "Could not obtain services"; + return S_OK; + } + ComPtr result; + ComPtr> deviceServices; + HRESULT hr = op->GetResults(&result); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain service discovery result", + return S_OK); + GattCommunicationStatus commStatus; + hr = result->get_Status(&commStatus); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain service discovery status", + return S_OK); + if (commStatus != GattCommunicationStatus_Success) + return S_OK; + + hr = result->get_Services(&deviceServices); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain service list", + return S_OK); + + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain service list size", + return S_OK); + for (uint i = 0; i < serviceCount; ++i) { + ComPtr deviceService; + hr = deviceServices->GetAt(i, &deviceService); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain service"); + GUID guuid; + hr = deviceService->get_Uuid(&guuid); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain service's Uuid"); + const QBluetoothUuid service(guuid); + + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ + << "Changing service pointer from thread" + << QThread::currentThread(); + QSharedPointer pointer; + if (serviceList.contains(service)) { + pointer = serviceList.value(service); + } else { + QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate(); + priv->uuid = service; + priv->setController(this); + + pointer = QSharedPointer(priv); + serviceList.insert(service, pointer); + } + pointer->type |= QLowEnergyService::PrimaryService; + + obtainIncludedServices(pointer, deviceService); + + // If QLowEnergyService::discoverDetails is called from this callback, + // deviceService3->GetIncludedServicesAsync can fail with AccessDenied + // error. To work around this, we defer emitting the signal. + QTimer::singleShot(0, [q, service] () { + emit q->serviceDiscovered(service); + }); + } + + setState(QLowEnergyController::DiscoveredState); + // If QLowEnergyService::discoverDetails is called from this callback, + // deviceService3->GetIncludedServicesAsync can fail with AccessDenied + // error. To work around this, we defer emitting the signal. + QTimer::singleShot(0, [q] () { + emit q->discoveryFinished(); + }); + + return S_OK; +} + +void QLowEnergyControllerPrivateWinRTNew::discoverServices() +{ + qCDebug(QT_BT_WINRT) << "Service discovery initiated"; + + ComPtr device3; + HRESULT hr = mDevice.As(&device3); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not cast device", return); + ComPtr> asyncResult; + hr = device3->GetGattServicesAsync(&asyncResult); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain services", return); + hr = QEventDispatcherWinRT::runOnXamlThread( [asyncResult, this] () { + HRESULT hr = asyncResult->put_Completed( + Callback>( + this, &QLowEnergyControllerPrivateWinRTNew::onServiceDiscoveryFinished).Get()); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not register service discovery callback", + return S_OK) + return hr; + }); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not run registration in Xaml thread", + return) +} + +void QLowEnergyControllerPrivateWinRTNew::discoverServiceDetails(const QBluetoothUuid &service) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service; + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINRT) << "Discovery done of unknown service:" + << service.toString(); + return; + } + + ComPtr deviceService = getNativeService(service); + if (!deviceService) { + qCDebug(QT_BT_WINRT) << "Could not obtain native service for uuid " << service; + return; + } + + auto reactOnDiscoveryError = [](QSharedPointer service, + const QString &msg) + { + qCDebug(QT_BT_WINRT) << msg; + service->setError(QLowEnergyService::UnknownError); + service->setState(QLowEnergyService::DiscoveryRequired); + }; + //update service data + QSharedPointer pointer = serviceList.value(service); + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + pointer->setState(QLowEnergyService::DiscoveringServices); + ComPtr deviceService3; + HRESULT hr = deviceService.As(&deviceService3); + if (FAILED(hr)) { + reactOnDiscoveryError(pointer, QStringLiteral("Could not cast service: %1").arg(hr)); + return; + } + ComPtr> op; + hr = deviceService3->GetIncludedServicesAsync(&op); + if (FAILED(hr)) { + reactOnDiscoveryError(pointer, QStringLiteral("Could not obtain included service list: %1").arg(hr)); + return; + } + ComPtr result; + hr = QWinRTFunctions::await(op, result.GetAddressOf()); + if (FAILED(hr)) { + reactOnDiscoveryError(pointer, QStringLiteral("Could not await service operation: %1").arg(hr)); + return; + } + GattCommunicationStatus status; + hr = result->get_Status(&status); + if (FAILED(hr) || status != GattCommunicationStatus_Success) { + reactOnDiscoveryError(pointer, + QStringLiteral("Obtaining list of included services failed: %1").arg(hr)); + return; + } + ComPtr> deviceServices; + hr = result->get_Services(&deviceServices); + if (FAILED(hr)) { + reactOnDiscoveryError(pointer, + QStringLiteral("Could not obtain service list from result: %1").arg(hr)); + return; + } + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + if (FAILED(hr)) { + reactOnDiscoveryError(pointer, + QStringLiteral("Could not obtain included service list's size: %1").arg(hr)); + return; + } + for (uint i = 0; i < serviceCount; ++i) { + ComPtr includedService; + hr = deviceServices->GetAt(i, &includedService); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain service from list") + GUID guuid; + hr = includedService->get_Uuid(&guuid); + WARN_AND_CONTINUE_IF_FAILED(hr, "Could not obtain service Uuid") + + const QBluetoothUuid service(guuid); + if (service.isNull()) { + qCDebug(QT_BT_WINRT) << "Could not find service"; + continue; + } + + pointer->includedServices.append(service); + + // update the type of the included service + QSharedPointer otherService = serviceList.value(service); + if (!otherService.isNull()) + otherService->type |= QLowEnergyService::IncludedService; + } + + QWinRTLowEnergyServiceHandlerNew *worker + = new QWinRTLowEnergyServiceHandlerNew(service, deviceService3); + QThread *thread = new QThread; + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &QWinRTLowEnergyServiceHandlerNew::obtainCharList); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, &QThread::finished, worker, &QObject::deleteLater); + connect(worker, &QWinRTLowEnergyServiceHandlerNew::errorOccured, + this, &QLowEnergyControllerPrivateWinRTNew::handleServiceHandlerError); + connect(worker, &QWinRTLowEnergyServiceHandlerNew::charListObtained, + [this, reactOnDiscoveryError, thread](const QBluetoothUuid &service, QHash charList, QVector indicateChars, + QLowEnergyHandle startHandle, QLowEnergyHandle endHandle) { + if (!serviceList.contains(service)) { + qCWarning(QT_BT_WINRT) << "Discovery done of unknown service:" + << service.toString(); + return; + } + + QSharedPointer pointer = serviceList.value(service); + pointer->startHandle = startHandle; + pointer->endHandle = endHandle; + pointer->characteristicList = charList; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([indicateChars, service, this]() { + for (const QBluetoothUuid &indicateChar : qAsConst(indicateChars)) + registerForValueChanges(service, indicateChar); + return S_OK; + }); + if (FAILED(hr)) { + reactOnDiscoveryError(pointer, + QStringLiteral("Could not register for value changes in Xaml thread: %1").arg(hr)); + thread->exit(0); + return; + } + + pointer->setState(QLowEnergyService::ServiceDiscovered); + thread->exit(0); + }); + thread->start(); +} + +void QLowEnergyControllerPrivateWinRTNew::startAdvertising( + const QLowEnergyAdvertisingParameters &, + const QLowEnergyAdvertisingData &, + const QLowEnergyAdvertisingData &) +{ + setError(QLowEnergyController::AdvertisingError); + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRTNew::stopAdvertising() +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRTNew::requestConnectionUpdate(const QLowEnergyConnectionParameters &) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRTNew::readCharacteristic( + const QSharedPointer service, + const QLowEnergyHandle charHandle) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::CharacteristicReadError); + Q_UNIMPLEMENTED(); + return; + } + + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << charHandle << "could not be found in service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicReadError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, service, this]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + if (!(charData.properties & QLowEnergyCharacteristic::Read)) + qCDebug(QT_BT_WINRT) << "Read flag is not set for characteristic" << charData.uuid; + + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + ComPtr> readOp; + HRESULT hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not read characteristic", + service, QLowEnergyService::CharacteristicReadError, return S_OK) + auto readCompletedLambda = [charData, charHandle, service] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "read operation failed."; + service->setError(QLowEnergyService::CharacteristicReadError); + return S_OK; + } + ComPtr characteristicValue; + HRESULT hr; + hr = op->GetResults(&characteristicValue); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain result for characteristic", + service, QLowEnergyService::CharacteristicReadError, return S_OK) + + const QByteArray value = byteArrayFromGattResult(characteristicValue); + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + charData.value = value; + service->characteristicList.insert(charHandle, charData); + emit service->characteristicRead(QLowEnergyCharacteristic(service, charHandle), value); + return S_OK; + }; + hr = readOp->put_Completed(Callback>( + readCompletedLambda).Get()); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not register characteristic read callback", + service, QLowEnergyService::CharacteristicReadError, return S_OK) + return S_OK; + }); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not run registration on Xaml thread", + service, QLowEnergyService::CharacteristicReadError, return) +} + +void QLowEnergyControllerPrivateWinRTNew::readDescriptor( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descHandle) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << descHandle; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::DescriptorReadError); + Q_UNIMPLEMENTED(); + return; + } + + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "in characteristic" << charHandle + << "cannot be found in service" << service->uuid; + service->setError(QLowEnergyService::DescriptorReadError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, descHandle, service, this]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + + // Get native descriptor + if (!charData.descriptorList.contains(descHandle)) + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "cannot be found in characteristic" << charHandle; + const QLowEnergyServicePrivate::DescData descData = charData.descriptorList.value(descHandle); + const QBluetoothUuid descUuid = descData.uuid; + if (descUuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + ComPtr> readOp; + HRESULT hr = characteristic->ReadClientCharacteristicConfigurationDescriptorAsync(&readOp); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not read client characteristic configuration", + service, QLowEnergyService::DescriptorReadError, return S_OK) + auto readCompletedLambda = [charHandle, descHandle, service] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "read operation failed"; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + ComPtr iValue; + HRESULT hr; + hr = op->GetResults(&iValue); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain result for descriptor", + service, QLowEnergyService::DescriptorReadError, return S_OK) + GattClientCharacteristicConfigurationDescriptorValue value; + hr = iValue->get_ClientCharacteristicConfigurationDescriptor(&value); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain value for descriptor", + service, QLowEnergyService::DescriptorReadError, return S_OK) + quint16 result = 0; + bool correct = false; + if (value & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + result |= QLowEnergyCharacteristic::Indicate; + correct = true; + } + if (value & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + result |= QLowEnergyCharacteristic::Notify; + correct = true; + } + if (value == GattClientCharacteristicConfigurationDescriptorValue_None) + correct = true; + if (!correct) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle + << "read operation failed. Obtained unexpected value."; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + QLowEnergyServicePrivate::DescData descData; + descData.uuid = QBluetoothUuid::ClientCharacteristicConfiguration; + descData.value = QByteArray(2, Qt::Uninitialized); + qToLittleEndian(result, descData.value.data()); + service->characteristicList[charHandle].descriptorList[descHandle] = descData; + emit service->descriptorRead(QLowEnergyDescriptor(service, charHandle, descHandle), + descData.value); + return S_OK; + }; + hr = readOp->put_Completed( + Callback>( + readCompletedLambda).Get()); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not register descriptor read callback", + service, QLowEnergyService::DescriptorReadError, return S_OK) + return S_OK; + } else { + ComPtr characteristic3; + HRESULT hr = characteristic.As(&characteristic3); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not cast characteristic", + service, QLowEnergyService::DescriptorReadError, return S_OK) + ComPtr> op; + hr = characteristic3->GetDescriptorsForUuidAsync(descData.uuid, &op); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain descriptor for uuid", + service, QLowEnergyService::DescriptorReadError, return S_OK) + ComPtr result; + hr = QWinRTFunctions::await(op, result.GetAddressOf(), QWinRTFunctions::ProcessMainThreadEvents, 5000); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not await descritpor read result", + service, QLowEnergyService::DescriptorReadError, return S_OK) + + GattCommunicationStatus commStatus; + hr = result->get_Status(&commStatus); + if (FAILED(hr) || commStatus != GattCommunicationStatus_Success) { + qErrnoWarning("Could not obtain list of descriptors"); + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + + ComPtr> descriptors; + hr = result->get_Descriptors(&descriptors); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain descriptor list", + service, QLowEnergyService::DescriptorReadError, return S_OK) + uint size; + hr = descriptors->get_Size(&size); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not await descritpor list's size", + service, QLowEnergyService::DescriptorReadError, return S_OK) + if (size == 0) { + qCWarning(QT_BT_WINRT) << "No descriptor with uuid" << descData.uuid << "was found."; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } else if (size > 1) { + qCWarning(QT_BT_WINRT) << "There is more than 1 descriptor with uuid" << descData.uuid; + } + + ComPtr descriptor; + hr = descriptors->GetAt(0, &descriptor); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain descritpor from list", + service, QLowEnergyService::DescriptorReadError, return S_OK) + ComPtr> readOp; + hr = descriptor->ReadValueWithCacheModeAsync(BluetoothCacheMode_Uncached, &readOp); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not read descriptor value", + service, QLowEnergyService::DescriptorReadError, return S_OK) + auto readCompletedLambda = [charHandle, descHandle, descUuid, service] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "read operation failed"; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + ComPtr descriptorValue; + HRESULT hr; + hr = op->GetResults(&descriptorValue); + if (FAILED(hr)) { + qCDebug(QT_BT_WINRT) << "Could not obtain result for descriptor" << descHandle; + service->setError(QLowEnergyService::DescriptorReadError); + return S_OK; + } + QLowEnergyServicePrivate::DescData descData; + descData.uuid = descUuid; + if (descData.uuid == QBluetoothUuid::CharacteristicUserDescription) + descData.value = byteArrayFromGattResult(descriptorValue, true); + else + descData.value = byteArrayFromGattResult(descriptorValue); + service->characteristicList[charHandle].descriptorList[descHandle] = descData; + emit service->descriptorRead(QLowEnergyDescriptor(service, charHandle, descHandle), + descData.value); + return S_OK; + }; + hr = readOp->put_Completed(Callback>( + readCompletedLambda).Get()); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not register descriptor read callback", + service, QLowEnergyService::DescriptorReadError, return S_OK) + return S_OK; + } + }); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not run registration on Xaml thread", + service, QLowEnergyService::DescriptorReadError, return) +} + +void QLowEnergyControllerPrivateWinRTNew::writeCharacteristic( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QByteArray &newValue, + QLowEnergyService::WriteMode mode) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << newValue << mode; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::CharacteristicWriteError); + Q_UNIMPLEMENTED(); + return; + } + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "cannot be found in service" + << service->uuid; + service->setError(QLowEnergyService::CharacteristicWriteError); + return; + } + + QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + const bool writeWithResponse = mode == QLowEnergyService::WriteWithResponse; + if (!(charData.properties & (writeWithResponse ? QLowEnergyCharacteristic::Write + : QLowEnergyCharacteristic::WriteNoResponse))) + qCDebug(QT_BT_WINRT) << "Write flag is not set for characteristic" << charHandle; + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charData, charHandle, this, service, newValue, + writeWithResponse]() { + ComPtr characteristic = getNativeCharacteristic(service->uuid, + charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + ComPtr bufferFactory; + HRESULT hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), + &bufferFactory); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain buffer factory", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + ComPtr buffer; + const quint32 length = quint32(newValue.length()); + hr = bufferFactory->Create(length, &buffer); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not create buffer", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + hr = buffer->put_Length(length); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not set buffer length", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + ComPtr byteAccess; + hr = buffer.As(&byteAccess); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not cast buffer", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + byte *bytes; + hr = byteAccess->Buffer(&bytes); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not set buffer", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + memcpy(bytes, newValue, length); + ComPtr> writeOp; + GattWriteOption option = writeWithResponse ? GattWriteOption_WriteWithResponse + : GattWriteOption_WriteWithoutResponse; + hr = characteristic->WriteValueWithOptionAsync(buffer.Get(), option, &writeOp); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could write characteristic", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + QPointer thisPtr(this); + auto writeCompletedLambda = [charData, charHandle, newValue, service, writeWithResponse, thisPtr] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation failed"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + if (hr == E_BLUETOOTH_ATT_INVALID_ATTRIBUTE_VALUE_LENGTH) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle + << "write operation was tried with invalid value length"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain characteristic write result", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Characteristic" << charHandle << "write operation failed"; + service->setError(QLowEnergyService::CharacteristicWriteError); + return S_OK; + } + // only update cache when property is readable. Otherwise it remains + // empty. + if (charData.properties & QLowEnergyCharacteristic::Read) + thisPtr->updateValueOfCharacteristic(charHandle, newValue, false); + if (writeWithResponse) + emit service->characteristicWritten(QLowEnergyCharacteristic(service, charHandle), + newValue); + return S_OK; + }; + hr = writeOp->put_Completed( + Callback>( + writeCompletedLambda).Get()); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not register characteristic write callback", + service, QLowEnergyService::CharacteristicWriteError, return S_OK) + return S_OK; + }); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not run registration on Xaml thread", + service, QLowEnergyService::CharacteristicWriteError, return) +} + +void QLowEnergyControllerPrivateWinRTNew::writeDescriptor( + const QSharedPointer service, + const QLowEnergyHandle charHandle, + const QLowEnergyHandle descHandle, + const QByteArray &newValue) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << service << charHandle << descHandle << newValue; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + Q_ASSERT(!service.isNull()); + if (role == QLowEnergyController::PeripheralRole) { + service->setError(QLowEnergyService::DescriptorWriteError); + Q_UNIMPLEMENTED(); + return; + } + + if (!service->characteristicList.contains(charHandle)) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "in characteristic" << charHandle + << "could not be found in service" << service->uuid; + service->setError(QLowEnergyService::DescriptorWriteError); + return; + } + + HRESULT hr; + hr = QEventDispatcherWinRT::runOnXamlThread([charHandle, descHandle, this, service, newValue]() { + const QLowEnergyServicePrivate::CharData charData = service->characteristicList.value(charHandle); + ComPtr characteristic = getNativeCharacteristic(service->uuid, charData.uuid); + if (!characteristic) { + qCDebug(QT_BT_WINRT) << "Could not obtain native characteristic" << charData.uuid + << "from service" << service->uuid; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + + // Get native descriptor + if (!charData.descriptorList.contains(descHandle)) + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "could not be found in Characteristic" + << charHandle; + + QLowEnergyServicePrivate::DescData descData = charData.descriptorList.value(descHandle); + if (descData.uuid == QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)) { + GattClientCharacteristicConfigurationDescriptorValue value; + quint16 intValue = qFromLittleEndian(newValue); + if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Indicate + && intValue & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + qCWarning(QT_BT_WINRT) << "Setting both Indicate and Notify is not supported on WinRT"; + value = GattClientCharacteristicConfigurationDescriptorValue( + (GattClientCharacteristicConfigurationDescriptorValue_Indicate + | GattClientCharacteristicConfigurationDescriptorValue_Notify)); + } else if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Indicate) { + value = GattClientCharacteristicConfigurationDescriptorValue_Indicate; + } else if (intValue & GattClientCharacteristicConfigurationDescriptorValue_Notify) { + value = GattClientCharacteristicConfigurationDescriptorValue_Notify; + } else if (intValue == 0) { + value = GattClientCharacteristicConfigurationDescriptorValue_None; + } else { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle + << "write operation failed: Invalid value"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + ComPtr> writeOp; + HRESULT hr = characteristic->WriteClientCharacteristicConfigurationDescriptorAsync(value, &writeOp); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not write client characteristic configuration", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + QPointer thisPtr(this); + auto writeCompletedLambda = [charHandle, descHandle, newValue, service, thisPtr] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain result for descriptor", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + if (result != GattCommunicationStatus_Success) { + qCWarning(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + thisPtr->updateValueOfDescriptor(charHandle, descHandle, newValue, false); + emit service->descriptorWritten(QLowEnergyDescriptor(service, charHandle, descHandle), + newValue); + return S_OK; + }; + hr = writeOp->put_Completed( + Callback>( + writeCompletedLambda).Get()); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not register descriptor write callback", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + } else { + ComPtr characteristic3; + HRESULT hr = characteristic.As(&characteristic3); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not cast characteristic", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + ComPtr> op; + hr = characteristic3->GetDescriptorsForUuidAsync(descData.uuid, &op); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain descriptor from Uuid", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + ComPtr result; + hr = QWinRTFunctions::await(op, result.GetAddressOf(), QWinRTFunctions::ProcessMainThreadEvents, 5000); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not await descriptor operation", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + GattCommunicationStatus commStatus; + hr = result->get_Status(&commStatus); + if (FAILED(hr) || commStatus != GattCommunicationStatus_Success) { + qCWarning(QT_BT_WINRT) << "Descriptor operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + ComPtr> descriptors; + hr = result->get_Descriptors(&descriptors); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain list of descriptors", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + uint size; + hr = descriptors->get_Size(&size); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain list of descriptors' size", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + if (size == 0) { + qCWarning(QT_BT_WINRT) << "No descriptor with uuid" << descData.uuid << "was found."; + return S_OK; + } else if (size > 1) { + qCWarning(QT_BT_WINRT) << "There is more than 1 descriptor with uuid" << descData.uuid; + } + ComPtr descriptor; + hr = descriptors->GetAt(0, &descriptor); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain descriptor", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + ComPtr bufferFactory; + hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), + &bufferFactory); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain buffer factory", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + ComPtr buffer; + const quint32 length = quint32(newValue.length()); + hr = bufferFactory->Create(length, &buffer); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not create buffer", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + hr = buffer->put_Length(length); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not set buffer length", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + ComPtr byteAccess; + hr = buffer.As(&byteAccess); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not cast buffer", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + byte *bytes; + hr = byteAccess->Buffer(&bytes); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not set buffer", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + memcpy(bytes, newValue, length); + ComPtr> writeOp; + hr = descriptor->WriteValueAsync(buffer.Get(), &writeOp); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not write descriptor value", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + QPointer thisPtr(this); + auto writeCompletedLambda = [charHandle, descHandle, newValue, service, thisPtr] + (IAsyncOperation *op, AsyncStatus status) + { + if (status == AsyncStatus::Canceled || status == AsyncStatus::Error) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + GattCommunicationStatus result; + HRESULT hr; + hr = op->GetResults(&result); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not obtain result for descriptor", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + if (result != GattCommunicationStatus_Success) { + qCDebug(QT_BT_WINRT) << "Descriptor" << descHandle << "write operation failed"; + service->setError(QLowEnergyService::DescriptorWriteError); + return S_OK; + } + thisPtr->updateValueOfDescriptor(charHandle, descHandle, newValue, false); + emit service->descriptorWritten(QLowEnergyDescriptor(service, charHandle, descHandle), + newValue); + return S_OK; + }; + hr = writeOp->put_Completed( + Callback>( + writeCompletedLambda).Get()); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not register descriptor write callback", + service, QLowEnergyService::DescriptorWriteError, return S_OK) + return S_OK; + } + return S_OK; + }); + CHECK_HR_AND_SET_SERVICE_ERROR(hr, "Could not run registration on Xaml thread", + service, QLowEnergyService::DescriptorWriteError, return) +} + + +void QLowEnergyControllerPrivateWinRTNew::addToGenericAttributeList(const QLowEnergyServiceData &, + QLowEnergyHandle) +{ + Q_UNIMPLEMENTED(); +} + +void QLowEnergyControllerPrivateWinRTNew::handleCharacteristicChanged( + quint16 charHandle, const QByteArray &data) +{ + qCDebug(QT_BT_WINRT) << __FUNCTION__ << charHandle << data; + qCDebug(QT_BT_WINRT_SERVICE_THREAD) << __FUNCTION__ << "Changing service pointer from thread" + << QThread::currentThread(); + QSharedPointer service = + serviceForHandle(charHandle); + if (service.isNull()) + return; + + qCDebug(QT_BT_WINRT) << "Characteristic change notification" << service->uuid + << charHandle << data.toHex(); + + QLowEnergyCharacteristic characteristic = characteristicForHandle(charHandle); + if (!characteristic.isValid()) { + qCWarning(QT_BT_WINRT) << "characteristicChanged: Cannot find characteristic"; + return; + } + + // only update cache when property is readable. Otherwise it remains + // empty. + if (characteristic.properties() & QLowEnergyCharacteristic::Read) + updateValueOfCharacteristic(characteristic.attributeHandle(), + data, false); + emit service->characteristicChanged(characteristic, data); +} + +void QLowEnergyControllerPrivateWinRTNew::handleServiceHandlerError(const QString &error) +{ + if (state != QLowEnergyController::DiscoveringState) + return; + + qCWarning(QT_BT_WINRT) << "Error while discovering services:" << error; + setState(QLowEnergyController::UnconnectedState); + setError(QLowEnergyController::ConnectionError); +} + +void QLowEnergyControllerPrivateWinRTNew::connectToPairedDevice() +{ + Q_Q(QLowEnergyController); + ComPtr device3; + HRESULT hr = mDevice.As(&device3); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not cast device", return) + ComPtr> deviceServicesOp; + while (!mAbortPending) { + hr = device3->GetGattServicesAsync(&deviceServicesOp); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain services", return) + ComPtr deviceServicesResult; + hr = QWinRTFunctions::await(deviceServicesOp, deviceServicesResult.GetAddressOf(), + QWinRTFunctions::ProcessThreadEvents, 5000); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not await services operation", return) + + GattCommunicationStatus commStatus; + hr = deviceServicesResult->get_Status(&commStatus); + if (FAILED(hr) || commStatus != GattCommunicationStatus_Success) { + qCWarning(QT_BT_WINRT()) << "Service operation failed"; + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + unregisterFromStatusChanges(); + return; + } + + ComPtr> deviceServices; + hr = deviceServicesResult->get_Services(&deviceServices); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain list of services", return) + uint serviceCount; + hr = deviceServices->get_Size(&serviceCount); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain service count", return) + + if (serviceCount == 0) { + qCWarning(QT_BT_WINRT()) << "Found devices without services"; + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + unregisterFromStatusChanges(); + return; + } + + // Windows automatically connects to the device as soon as a service value is read/written. + // Thus we read one value in order to establish the connection. + for (uint i = 0; i < serviceCount; ++i) { + ComPtr service; + hr = deviceServices->GetAt(i, &service); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain service", return); + ComPtr service3; + hr = service.As(&service3); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not cast service", return); + ComPtr> characteristicsOp; + hr = service3->GetCharacteristicsAsync(&characteristicsOp); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain characteristic", return); + ComPtr characteristicsResult; + hr = QWinRTFunctions::await(characteristicsOp, characteristicsResult.GetAddressOf(), + QWinRTFunctions::ProcessThreadEvents, 5000); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not await characteristic operation", return); + GattCommunicationStatus commStatus; + hr = characteristicsResult->get_Status(&commStatus); + if (FAILED(hr) || commStatus != GattCommunicationStatus_Success) { + qCWarning(QT_BT_WINRT) << "Characteristic operation failed"; + break; + } + ComPtr> characteristics; + hr = characteristicsResult->get_Characteristics(&characteristics); + if (hr == E_ACCESSDENIED) { + // Everything will work as expected up until this point if the manifest capabilties + // for bluetooth LE are not set. + qCWarning(QT_BT_WINRT) << "Could not obtain characteristic list. Please check your " + "manifest capabilities"; + setState(QLowEnergyController::UnconnectedState); + setError(QLowEnergyController::ConnectionError); + unregisterFromStatusChanges(); + return; + } + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain characteristic list", return); + uint characteristicsCount; + hr = characteristics->get_Size(&characteristicsCount); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain characteristic list's size", return); + for (uint j = 0; j < characteristicsCount; ++j) { + ComPtr characteristic; + hr = characteristics->GetAt(j, &characteristic); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain characteristic", return); + ComPtr> op; + GattCharacteristicProperties props; + hr = characteristic->get_CharacteristicProperties(&props); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain characteristic's properties", return); + if (!(props & GattCharacteristicProperties_Read)) + continue; + /* QZ rviola + hr = characteristic->ReadValueWithCacheModeAsync(BluetoothCacheMode::BluetoothCacheMode_Uncached, &op); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not read characteristic value", return); + ComPtr result; + hr = QWinRTFunctions::await(op, result.GetAddressOf(), QWinRTFunctions::ProcessThreadEvents, 500); + // E_ILLEGAL_METHOD_CALL will be the result for a device, that is not reachable at + // the moment. In this case we should jump back into the outer loop and keep trying. + if (hr == E_ILLEGAL_METHOD_CALL) + break; + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not await characteristic read", return); + ComPtr buffer; + hr = result->get_Value(&buffer); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain characteristic value", return); + if (!buffer) { + qCDebug(QT_BT_WINRT) << "Problem reading value"; + break; + } + */ + + setState(QLowEnergyController::ConnectedState); + emit q->connected(); + if (!registerForStatusChanges()) { + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + return; + } + return; + } + } + } +} + +void QLowEnergyControllerPrivateWinRTNew::connectToUnpairedDevice() +{ + if (!registerForStatusChanges()) { + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + return; + } + ComPtr device3; + HRESULT hr = mDevice.As(&device3); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not cast device", return) + ComPtr deviceServicesResult; + while (!mAbortPending) { + ComPtr> deviceServicesOp; + hr = device3->GetGattServicesAsync(&deviceServicesOp); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not obtain services", return) + hr = QWinRTFunctions::await(deviceServicesOp, deviceServicesResult.GetAddressOf(), + QWinRTFunctions::ProcessMainThreadEvents); + CHECK_FOR_DEVICE_CONNECTION_ERROR(hr, "Could not await services operation", return) + + GattCommunicationStatus commStatus; + hr = deviceServicesResult->get_Status(&commStatus); + if (commStatus == GattCommunicationStatus_Unreachable) + continue; + + if (FAILED(hr) || commStatus != GattCommunicationStatus_Success) { + qCWarning(QT_BT_WINRT()) << "Service operation failed"; + setError(QLowEnergyController::ConnectionError); + setState(QLowEnergyController::UnconnectedState); + unregisterFromStatusChanges(); + return; + } + + break; + } +} + +QT_END_NAMESPACE + +#include "qlowenergycontroller_winrt_new.moc" diff --git a/src/ChartFooter.qml b/src/ChartFooter.qml new file mode 100644 index 000000000..3b9fe2c14 --- /dev/null +++ b/src/ChartFooter.qml @@ -0,0 +1,25 @@ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 +import Qt.labs.settings 1.0 + +ColumnLayout { + anchors.fill: parent + Loader { + id: chartFooterLoader + sourceComponent: ChartFooterInnerJS + anchors.fill: parent + active: false + } + + Loader { + anchors.fill: parent + source: CHARTJS ? "ChartFooterInnerJS.qml":"ChartFooterInnerNoJS.qml" + onLoaded: { + if(CHARTJS) { + chartFooterLoader.active = true; + } + } + } +} diff --git a/src/ChartFooterInnerJS.qml b/src/ChartFooterInnerJS.qml new file mode 100644 index 000000000..32d7b5d14 --- /dev/null +++ b/src/ChartFooterInnerJS.qml @@ -0,0 +1,31 @@ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 +import Qt.labs.settings 1.0 +import QtWebView 1.1 + +ColumnLayout { + anchors.fill: parent + Settings { + id: settings + } + WebView { + id: webView + anchors.fill: parent + url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/chartjs/chartlive.htm" + visible: rootItem.chartFooterVisible + onLoadingChanged: { + if (loadRequest.errorString) { + console.error(loadRequest.errorString); + console.error("port " + settings.value("template_inner_QZWS_port")); + } + } + onVisibleChanged: { + console.log("onVisibleChanged" + visible) + if(visible === true) { + reload(); + } + } + } +} diff --git a/src/ChartFooterInnerNoJS.qml b/src/ChartFooterInnerNoJS.qml new file mode 100644 index 000000000..d459a84c9 --- /dev/null +++ b/src/ChartFooterInnerNoJS.qml @@ -0,0 +1,12 @@ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 +import Qt.labs.settings 1.0 + +ColumnLayout { + anchors.fill: parent + Settings { + id: settings + } +} diff --git a/src/Computrainer.cpp b/src/Computrainer.cpp index 8660b94db..7e567056b 100644 --- a/src/Computrainer.cpp +++ b/src/Computrainer.cpp @@ -380,7 +380,7 @@ void Computrainer::unpackTelemetry(int &ss1, int &ss2, int &ss3, int &buttons, i * READING TELEMETRY AND ISSUING CONTROL COMMANDS WHILST UPDATING * MEMBER VARIABLES AS TELEMETRY CHANGES ARE FOUND. * - * run() - bg thread continuosly reading/writing the device port + * run() - bg thread continuously reading/writing the device port * it is kicked off by start and then examines status to check * when it is time to pause or stop altogether. * ---------------------------------------------------------------------- */ @@ -632,6 +632,7 @@ void Computrainer::run() { /* time to shut up shop */ if (!(curstatus & CT_RUNNING)) { + qDebug() << "time to shut up shop"; // time to stop! closePort(); // need to release that file handle!! quit(0); @@ -639,12 +640,14 @@ void Computrainer::run() { } if ((curstatus & CT_PAUSED) && isDeviceOpen == true) { + qDebug() << "(curstatus & CT_PAUSED) && isDeviceOpen == true"; closePort(); isDeviceOpen = false; } else if (!(curstatus & CT_PAUSED) && (curstatus & CT_RUNNING) && isDeviceOpen == false) { - + qDebug() << "!(curstatus & CT_PAUSED) && (curstatus & CT_RUNNING) && isDeviceOpen == false"; if (openPort()) { + qDebug() << "quit(2)"; quit(2); return; // open failed! } @@ -653,6 +656,7 @@ void Computrainer::run() { // send first command to get computrainer ready prepareCommand(curmode, curmode == CT_ERGOMODE ? curload : curgradient); if (sendCommand(curmode) == -1) { + qDebug() << "quit(4)"; // send failed - ouch! closePort(); // need to release that file handle!! quit(4); @@ -671,6 +675,7 @@ void Computrainer::run() { prepareCommand(curmode, curmode == CT_ERGOMODE ? curload : curgradient); if (sendCommand(curmode) == -1) { + qDebug() << "quit(4)"; // send failed - ouch! closePort(); // need to release that file handle!! quit(4); @@ -744,9 +749,9 @@ int Computrainer::readMessage() { } #ifdef Q_OS_ANDROID - qDebug() << cleanFrame << QByteArray((const char*)buf, 7).toHex(' '); + qDebug() << cleanFrame << QByteArray((const char *)buf, 7).toHex(' '); - if(!cleanFrame) + if (!cleanFrame) return 0; #endif diff --git a/src/Computrainer.h b/src/Computrainer.h index 11f47900c..6cf2f4e2b 100644 --- a/src/Computrainer.h +++ b/src/Computrainer.h @@ -37,12 +37,9 @@ #include #ifdef WIN32 -#include -#endif +#include -#ifdef WIN32 #include -#include #else #include #include // unix!! @@ -145,7 +142,7 @@ class Computrainer : public QThread { double getLoad(); private: - void run(); // called by start to kick off the CT comtrol thread + void run() override; // called by start to kick off the CT comtrol thread // 56 bytes comprise of 8 7byte command messages, where // the last is the set load / gradient respectively diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/Info.plist b/src/ConnectIQ/iOS/ConnectIQ.xcframework/Info.plist new file mode 100644 index 000000000..1925319c5 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/Info.plist @@ -0,0 +1,41 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-armv7_arm64 + LibraryPath + ConnectIQ.framework + SupportedArchitectures + + armv7 + arm64 + + SupportedPlatform + ios + + + LibraryIdentifier + ios-i386_x86_64-simulator + LibraryPath + ConnectIQ.framework + SupportedArchitectures + + i386 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ConnectIQ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ConnectIQ new file mode 100755 index 000000000..cb695cffc Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ConnectIQ differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/ConnectIQ.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/ConnectIQ.h new file mode 100644 index 000000000..632ae87e7 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/ConnectIQ.h @@ -0,0 +1,237 @@ +// +// ConnectIQ.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import +#import "IQConstants.h" +#import "IQDevice.h" +#import "IQApp.h" + +// -------------------------------------------------------------------------------- +#pragma mark - PUBLIC TYPES +// -------------------------------------------------------------------------------- + +/// @brief SendMessage progress callback block +/// +/// @param sentBytes The number of bytes that have been successfully transferred +/// to the device so far for this connection. +/// @param totalBytes The total number of bytes to transfer for this connection. +typedef void (^IQSendMessageProgress)(uint32_t sentBytes, uint32_t totalBytes); + +/// @brief SendMessage completion callback block +/// +/// @param result The result of the SendMessage operation. +typedef void (^IQSendMessageCompletion)(IQSendMessageResult result); + +/// @brief Conforming to the IQUIOverrideDelegate protocol indicates that an +/// object handles one or more events triggered by the ConnectIQ SDK that +/// require user input. +@protocol IQUIOverrideDelegate +@optional +/// @brief Called by the ConnectIQ SDK when an action has been requested that +/// requires Garmin Connect Mobile to be installed. +/// +/// The receiver should choose whether or not to launch the Apple App +/// Store page for GCM, ideally by presenting the user with a choice. +/// +/// If the receiver of this message decides to install GCM, it must call +/// showAppStoreForConnectMobile. +- (void)needsToInstallConnectMobile; +@end + +/// @brief Conforming to the IQDeviceEventDelegate protocol indicates that an +/// object handles ConnectIQ device status events. +@protocol IQDeviceEventDelegate +@optional +/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has +/// changed. +/// +/// @param device The IQDevice whose status changed. +/// @param status The new status of the device. +- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status; +@end + +/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an +/// object handles messages from ConnectIQ apps on compatible devices. +@protocol IQAppMessageDelegate +@optional +/// @brief Called by the ConnectIQ SDK when a message is received from a device. +/// +/// @param message The message that was received. +/// @param app The device app that sent the message. +- (void)receivedMessage:(id)message fromApp:(IQApp *)app; +@end + +// -------------------------------------------------------------------------------- +#pragma mark - CLASS DEFINITION +// -------------------------------------------------------------------------------- + +/// @brief The root of the ConnectIQ SDK API. +@interface ConnectIQ : NSObject + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +// -------------------------------------------------------------------------------- +#pragma mark - SINGLETON ACCESS +// -------------------------------------------------------------------------------- + +/// @brief Exposes the single static instance of the ConnectIQ class. +/// +/// @return The single status instance of the ConnectIQ class. ++ (ConnectIQ *)sharedInstance; + +// -------------------------------------------------------------------------------- +#pragma mark - INITIALIZATION +// -------------------------------------------------------------------------------- + +/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for +/// its operation. +/// +/// @param urlScheme The URL scheme for this companion app. When Garmin Connect +/// Mobile is launched, it will return to the companion app by +/// launching a URL with this scheme. +/// @param delegate The delegate that the SDK will use for notifying the +/// companion app about events that require user input. If this +/// is nil, the SDK's default UI will be used. +- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id)delegate; + +// -------------------------------------------------------------------------------- +#pragma mark - EXTERNAL LAUNCHING +// -------------------------------------------------------------------------------- + +/// @brief Launches the Apple App Store page for the Garmin Connect Mobile app. +/// The companion app should only call this in response to a +/// needsToInstallConnectMobile event that gets triggered on the +/// IQUIOverrideDelegate. +- (void)showAppStoreForConnectMobile; + +/// @brief Launches Garmin Connect Mobile for the purpose of retrieving a list of +/// ConnectIQ-compatible devices. +/// +/// Once the user has chosen which ConnectIQ devices to share with the +/// companion app, GCM will return those devices to the companion app by +/// opening a URL with the scheme registered in +/// initializeWithUrlScheme:uiOverrideDelegate:. +/// +/// The companion app should handle this URL by passing it in to the +/// parseDeviceSelectionResponseFromURL: method to get the list of devices +/// that the user permitted the companion app to communicate with. +- (void)showConnectIQDeviceSelection; + +/// @brief Parses a URL opened from Garmin Connect Mobile into a list of devices. +/// +/// @param url The URL to parse. +/// +/// @return An array of IQDevice objects representing the ConnectIQ-compatible +/// devices that the user allowed GCM to share with the companion app. +/// +/// @seealso showConnectIQDeviceSelection +- (NSArray *)parseDeviceSelectionResponseFromURL:(NSURL *)url; + +/// @brief Launches Garmin Connect Mobile and shows the ConnectIQ app store page +/// for the given app. +/// +/// The companion app should call this if the user would like to manage +/// the app on the device, such as to install, upgrade, uninstall, or +/// modify settings. +/// +/// @param app The app to show the ConnectIQ app store page for. +- (void)showConnectIQStoreForApp:(IQApp *)app; + +// -------------------------------------------------------------------------------- +#pragma mark - DEVICE MANAGEMENT +// -------------------------------------------------------------------------------- + +/// @brief Registers an object as a listener for ConnectIQ device status events. +/// +/// A device may have multiple device event listeners if this method is +/// called more than once. +/// +/// @param device A device to listen for status events from. +/// @param delegate The listener which will receive status events for this device. +- (void)registerForDeviceEvents:(IQDevice *)device delegate:(id)delegate; + +/// @brief Unregisters a listener for a specific device. +/// +/// @param device The device to unregister the listener for. +/// @param delegate The listener to remove from the device. +- (void)unregisterForDeviceEvents:(IQDevice *)device delegate:(id)delegate; + +/// @brief Unregisters the specified listener from all devices for which it had +/// previously been registered. +/// +/// @param delegate The listener to unregister. +- (void)unregisterForAllDeviceEvents:(id)delegate; + +/// @brief Gets the current connection status of a device. +/// +/// The device must have been registered for event notifications by +/// calling registerForDeviceEvents:delegate: or this method will return +/// IQDeviceStatus_InvalidDevice. +/// +/// @param device The device to get the status for. +/// +/// @return The device's current connection status. +- (IQDeviceStatus)getDeviceStatus:(IQDevice *)device; + +// -------------------------------------------------------------------------------- +#pragma mark - APP MANAGEMENT +// -------------------------------------------------------------------------------- + +/// @brief Begins getting the status of an app on a device. This method returns +/// immediately. +/// +/// @param app The IQApp to get the status for. +/// @param completion The completion block that will be triggered when the device +/// status operation is complete. +- (void)getAppStatus:(IQApp *)app completion:(void(^)(IQAppStatus *appStatus))completion; + +/// @brief Registers an object as a listener for ConnectIQ messages from an app +/// on a device. +/// +/// An app may have multiple message listeners if this method is called +/// more than once. +/// +/// @param app The app to listen for messages from. +/// @param delegate The listener which will receive messages for this app. +- (void)registerForAppMessages:(IQApp *)app delegate:(id)delegate; + +/// @brief Unregisters a listener for a specific app. +/// +/// @param app The app to unregister a listener for. +/// @param delegate The listener to remove from the app. +- (void)unregisterForAppMessages:(IQApp *)app delegate:(id)delegate; + +/// @brief Unregisters all previously registered apps for a specific listener. +/// +/// @param delegate The listener to unregister. +- (void)unregisterForAllAppMessages:(id)delegate; + +/// @brief Begins sending a message to an app. This method returns immediately. +/// +/// @param message The message to send to the app. This message must be one of +/// the following types: NSString, NSNumber, NSNull, NSArray, +/// or NSDictionary. Arrays and dictionaries may be nested. +/// @param app The app to send the message to. +/// @param progress A progress block that will be triggered periodically +/// throughout the transfer. This is guaranteed to be triggered +/// at least once. +/// @param completion A completion block that will be triggered when the send +/// message operation is complete. +- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion; + +/// @brief Sends an open app request message request to the device. This method returns immediately. +/// +/// @param app The app to open. +/// @param completion A completion block that will be triggered when the send +/// message operation is complete. +- (void)openAppRequest:(IQApp *)app completion:(IQSendMessageCompletion)completion; + +// TODO *** Holding off on documenting this until this method actually works. +- (void)sendImage:(NSData *)bitmap toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQApp.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQApp.h new file mode 100644 index 000000000..a9dfe8c54 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQApp.h @@ -0,0 +1,34 @@ +// +// IQApp.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import +#import "IQDevice.h" +#import "IQAppStatus.h" + +/// @brief Represents an instance of a ConnectIQ app that is installed on a +/// Garmin device. +@interface IQApp : NSObject + +/// @brief The unique identifier for this app. +@property (nonatomic, readonly) NSUUID *uuid; + +/// @brief The unique identifier for this app in the store. +@property (nonatomic, readonly) NSUUID *storeUuid; + +/// @brief The device that this app is installed on. +@property (nonatomic, readonly) IQDevice *device; + +/// @brief Creates a new app instance. +/// +/// @param uuid The UUID of the app to create. +/// @param storeUuid The store UUID of the app to create. +/// @param device The device the app to create is installed on. +/// +/// @return A new IQApp instance with the appropriate values set. ++ (IQApp *)appWithUUID:(NSUUID *)uuid storeUuid:(NSUUID *)storeUuid device:(IQDevice *)device; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQAppStatus.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQAppStatus.h new file mode 100644 index 000000000..663641d15 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQAppStatus.h @@ -0,0 +1,20 @@ +// +// IQAppStatus.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import + +/// @brief Represents the current status of an app on a Garmin device. +@interface IQAppStatus : NSObject + +/// @brief YES if the app is installed on the device, NO if it isn't. +@property (nonatomic, readonly) BOOL isInstalled; + +/// @brief The version of the app that is currently installed on the device. If +/// the app is not installed, this value is unused. +@property (nonatomic, readonly) uint16_t version; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQConstants.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQConstants.h new file mode 100644 index 000000000..b9017759a --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQConstants.h @@ -0,0 +1,63 @@ +// +// IQConstants.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import + +/// @brief The current version of the ConnectIQ SDK. +extern int const IQSDKVersion; + +/// @brief The bundle identifier for the Garmin Connect Mobile app. +extern NSString * const IQGCMBundle; + +/// @brief The result of a SendMessage operation +typedef NS_ENUM(NSInteger, IQSendMessageResult){ + ///! @brief The message was sent successfully. + IQSendMessageResult_Success, + + /// @brief The message failed to send due to an unknown error. + IQSendMessageResult_Failure_Unknown, + + /// @brief The message failed to send. There was an error within the SDK or + /// on the device. + IQSendMessageResult_Failure_InternalError, + + /// @brief The message failed to send. The device is not available right now. + IQSendMessageResult_Failure_DeviceNotAvailable, + + /// @brief The message failed to send. The app is not installed on the + /// device. + IQSendMessageResult_Failure_AppNotFound, + + /// @brief The message failed to send. The device is busy and cannot receive + /// the message right now. + IQSendMessageResult_Failure_DeviceIsBusy, + + /// @brief The message failed to send. The message contained an unsupported + /// type. + IQSendMessageResult_Failure_UnsupportedType, + + /// @brief The message failed to send. The device does not have enough memory + /// to receive the message. + IQSendMessageResult_Failure_InsufficientMemory, + + /// @brief The message failed to send. The connection timed out while sending + /// the message. + IQSendMessageResult_Failure_Timeout, + + /// @brief The message failed to send and was retried, but could not complete + /// after a number of tries. + IQSendMessageResult_Failure_MaxRetries, + + /// @brief The message was received by the device but it chose not to display + /// a message prompt, ignoring the message. + IQSendMessageResult_Failure_PromptNotDisplayed, + + /// @brief The message was received by the device but the app to open + /// was already running on the device. + IQSendMessageResult_Failure_AppAlreadyRunning, +}; +NSString *NSStringFromSendMessageResult(IQSendMessageResult value); diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQDevice.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQDevice.h new file mode 100644 index 000000000..6842829f7 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Headers/IQDevice.h @@ -0,0 +1,61 @@ +// +// IQDevice.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import +#import + +/// @brief The current status of an IQDevice. +typedef NS_ENUM(NSInteger, IQDeviceStatus){ + /// @brief No device with this UUID has been registered for status events + /// the SDK. + IQDeviceStatus_InvalidDevice, + + /// @brief Bluetooth is either powered off or resetting. + IQDeviceStatus_BluetoothNotReady, + + /// @brief This device could not be found by iOS. Perhaps the user removed + /// the device? + IQDeviceStatus_NotFound, + + /// @brief The device is recognized by iOS, but it is not currently + /// connected. + IQDeviceStatus_NotConnected, + + /// @brief The device is connected and ready to communicate. + IQDeviceStatus_Connected, +}; + +/// @brief Represents a ConnectIQ-compatible Garmin device. +@interface IQDevice : NSObject + +/// @brief The unique identifier for this device. +@property (nonatomic, readonly) NSUUID *uuid; + +/// @brief The model name of the device provided by Garmin Connect Mobile. +@property (nonatomic, readonly) NSString *modelName; + +/// @brief The friendly name of the device, set by the user and provided by +/// Garmin Connect Mobile. +@property (nonatomic, readonly) NSString *friendlyName; + +/// @brief Creates a new device instance. +/// +/// @param uuid The UUID of the device to create. +/// @param modelName The model name of the device to create. +/// @param friendlyName The friendly name of the device to create. +/// +/// @return A new IQDevice instance with the appropriate values set. ++ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName; + +/// @brief Creates a new device instance by copying another device's values. +/// +/// @param device The device to copy values from. +/// +/// @return A new IQDevice instance with all values copied. +- (instancetype)initWithDevice:(IQDevice *)device; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Info.plist b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Info.plist new file mode 100644 index 000000000..87d927867 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Info.plist differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Modules/module.modulemap b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Modules/module.modulemap new file mode 100644 index 000000000..685a0721e --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module ConnectIQ { + umbrella header "ConnectIQ.h" + + export * + module * { export * } +} diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ar.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ar.lproj/IQLocalizable.strings new file mode 100644 index 000000000..772c7c199 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ar.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/cs.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/cs.lproj/IQLocalizable.strings new file mode 100644 index 000000000..294594b65 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/cs.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/da.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/da.lproj/IQLocalizable.strings new file mode 100644 index 000000000..9c7faad3e Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/da.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/de.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/de.lproj/IQLocalizable.strings new file mode 100644 index 000000000..cb4d87b1b Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/de.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/el.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/el.lproj/IQLocalizable.strings new file mode 100644 index 000000000..8f4e27056 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/el.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/en.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/en.lproj/IQLocalizable.strings new file mode 100644 index 000000000..8794262a9 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/en.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/es.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/es.lproj/IQLocalizable.strings new file mode 100644 index 000000000..247b71ac7 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/es.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/fi.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/fi.lproj/IQLocalizable.strings new file mode 100644 index 000000000..344d06c1b Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/fi.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/fr.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/fr.lproj/IQLocalizable.strings new file mode 100644 index 000000000..814e34f18 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/fr.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/he.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/he.lproj/IQLocalizable.strings new file mode 100644 index 000000000..b29a9b084 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/he.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/hr.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/hr.lproj/IQLocalizable.strings new file mode 100644 index 000000000..e10bd29af Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/hr.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/hu.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/hu.lproj/IQLocalizable.strings new file mode 100644 index 000000000..9c96de1e8 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/hu.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/id.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/id.lproj/IQLocalizable.strings new file mode 100644 index 000000000..eda1bc3d3 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/id.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/it.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/it.lproj/IQLocalizable.strings new file mode 100644 index 000000000..1bd95ac77 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/it.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ja.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ja.lproj/IQLocalizable.strings new file mode 100644 index 000000000..0f5b121c0 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ja.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ko.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ko.lproj/IQLocalizable.strings new file mode 100644 index 000000000..3f3749b0e Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ko.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ms.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ms.lproj/IQLocalizable.strings new file mode 100644 index 000000000..499c806b2 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ms.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/nb.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/nb.lproj/IQLocalizable.strings new file mode 100644 index 000000000..436c9f67d Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/nb.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/nl.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/nl.lproj/IQLocalizable.strings new file mode 100644 index 000000000..97d1c509c Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/nl.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pl.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pl.lproj/IQLocalizable.strings new file mode 100644 index 000000000..0f8f3d72b Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pl.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings new file mode 100644 index 000000000..6f9022878 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pt.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pt.lproj/IQLocalizable.strings new file mode 100644 index 000000000..ddc7d69a1 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/pt.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ru.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ru.lproj/IQLocalizable.strings new file mode 100644 index 000000000..ce83c0a22 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/ru.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/sk.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/sk.lproj/IQLocalizable.strings new file mode 100644 index 000000000..98b954bff Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/sk.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/sv.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/sv.lproj/IQLocalizable.strings new file mode 100644 index 000000000..316d6f897 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/sv.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/th.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/th.lproj/IQLocalizable.strings new file mode 100644 index 000000000..f98199499 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/th.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/tr.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/tr.lproj/IQLocalizable.strings new file mode 100644 index 000000000..c71eb4a00 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/tr.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings new file mode 100644 index 000000000..d630d3f0c Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings new file mode 100644 index 000000000..db0daa2c7 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-armv7_arm64/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ConnectIQ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ConnectIQ new file mode 100755 index 000000000..f86f0c158 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ConnectIQ differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/ConnectIQ.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/ConnectIQ.h new file mode 100644 index 000000000..632ae87e7 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/ConnectIQ.h @@ -0,0 +1,237 @@ +// +// ConnectIQ.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import +#import "IQConstants.h" +#import "IQDevice.h" +#import "IQApp.h" + +// -------------------------------------------------------------------------------- +#pragma mark - PUBLIC TYPES +// -------------------------------------------------------------------------------- + +/// @brief SendMessage progress callback block +/// +/// @param sentBytes The number of bytes that have been successfully transferred +/// to the device so far for this connection. +/// @param totalBytes The total number of bytes to transfer for this connection. +typedef void (^IQSendMessageProgress)(uint32_t sentBytes, uint32_t totalBytes); + +/// @brief SendMessage completion callback block +/// +/// @param result The result of the SendMessage operation. +typedef void (^IQSendMessageCompletion)(IQSendMessageResult result); + +/// @brief Conforming to the IQUIOverrideDelegate protocol indicates that an +/// object handles one or more events triggered by the ConnectIQ SDK that +/// require user input. +@protocol IQUIOverrideDelegate +@optional +/// @brief Called by the ConnectIQ SDK when an action has been requested that +/// requires Garmin Connect Mobile to be installed. +/// +/// The receiver should choose whether or not to launch the Apple App +/// Store page for GCM, ideally by presenting the user with a choice. +/// +/// If the receiver of this message decides to install GCM, it must call +/// showAppStoreForConnectMobile. +- (void)needsToInstallConnectMobile; +@end + +/// @brief Conforming to the IQDeviceEventDelegate protocol indicates that an +/// object handles ConnectIQ device status events. +@protocol IQDeviceEventDelegate +@optional +/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has +/// changed. +/// +/// @param device The IQDevice whose status changed. +/// @param status The new status of the device. +- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status; +@end + +/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an +/// object handles messages from ConnectIQ apps on compatible devices. +@protocol IQAppMessageDelegate +@optional +/// @brief Called by the ConnectIQ SDK when a message is received from a device. +/// +/// @param message The message that was received. +/// @param app The device app that sent the message. +- (void)receivedMessage:(id)message fromApp:(IQApp *)app; +@end + +// -------------------------------------------------------------------------------- +#pragma mark - CLASS DEFINITION +// -------------------------------------------------------------------------------- + +/// @brief The root of the ConnectIQ SDK API. +@interface ConnectIQ : NSObject + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +// -------------------------------------------------------------------------------- +#pragma mark - SINGLETON ACCESS +// -------------------------------------------------------------------------------- + +/// @brief Exposes the single static instance of the ConnectIQ class. +/// +/// @return The single status instance of the ConnectIQ class. ++ (ConnectIQ *)sharedInstance; + +// -------------------------------------------------------------------------------- +#pragma mark - INITIALIZATION +// -------------------------------------------------------------------------------- + +/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for +/// its operation. +/// +/// @param urlScheme The URL scheme for this companion app. When Garmin Connect +/// Mobile is launched, it will return to the companion app by +/// launching a URL with this scheme. +/// @param delegate The delegate that the SDK will use for notifying the +/// companion app about events that require user input. If this +/// is nil, the SDK's default UI will be used. +- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id)delegate; + +// -------------------------------------------------------------------------------- +#pragma mark - EXTERNAL LAUNCHING +// -------------------------------------------------------------------------------- + +/// @brief Launches the Apple App Store page for the Garmin Connect Mobile app. +/// The companion app should only call this in response to a +/// needsToInstallConnectMobile event that gets triggered on the +/// IQUIOverrideDelegate. +- (void)showAppStoreForConnectMobile; + +/// @brief Launches Garmin Connect Mobile for the purpose of retrieving a list of +/// ConnectIQ-compatible devices. +/// +/// Once the user has chosen which ConnectIQ devices to share with the +/// companion app, GCM will return those devices to the companion app by +/// opening a URL with the scheme registered in +/// initializeWithUrlScheme:uiOverrideDelegate:. +/// +/// The companion app should handle this URL by passing it in to the +/// parseDeviceSelectionResponseFromURL: method to get the list of devices +/// that the user permitted the companion app to communicate with. +- (void)showConnectIQDeviceSelection; + +/// @brief Parses a URL opened from Garmin Connect Mobile into a list of devices. +/// +/// @param url The URL to parse. +/// +/// @return An array of IQDevice objects representing the ConnectIQ-compatible +/// devices that the user allowed GCM to share with the companion app. +/// +/// @seealso showConnectIQDeviceSelection +- (NSArray *)parseDeviceSelectionResponseFromURL:(NSURL *)url; + +/// @brief Launches Garmin Connect Mobile and shows the ConnectIQ app store page +/// for the given app. +/// +/// The companion app should call this if the user would like to manage +/// the app on the device, such as to install, upgrade, uninstall, or +/// modify settings. +/// +/// @param app The app to show the ConnectIQ app store page for. +- (void)showConnectIQStoreForApp:(IQApp *)app; + +// -------------------------------------------------------------------------------- +#pragma mark - DEVICE MANAGEMENT +// -------------------------------------------------------------------------------- + +/// @brief Registers an object as a listener for ConnectIQ device status events. +/// +/// A device may have multiple device event listeners if this method is +/// called more than once. +/// +/// @param device A device to listen for status events from. +/// @param delegate The listener which will receive status events for this device. +- (void)registerForDeviceEvents:(IQDevice *)device delegate:(id)delegate; + +/// @brief Unregisters a listener for a specific device. +/// +/// @param device The device to unregister the listener for. +/// @param delegate The listener to remove from the device. +- (void)unregisterForDeviceEvents:(IQDevice *)device delegate:(id)delegate; + +/// @brief Unregisters the specified listener from all devices for which it had +/// previously been registered. +/// +/// @param delegate The listener to unregister. +- (void)unregisterForAllDeviceEvents:(id)delegate; + +/// @brief Gets the current connection status of a device. +/// +/// The device must have been registered for event notifications by +/// calling registerForDeviceEvents:delegate: or this method will return +/// IQDeviceStatus_InvalidDevice. +/// +/// @param device The device to get the status for. +/// +/// @return The device's current connection status. +- (IQDeviceStatus)getDeviceStatus:(IQDevice *)device; + +// -------------------------------------------------------------------------------- +#pragma mark - APP MANAGEMENT +// -------------------------------------------------------------------------------- + +/// @brief Begins getting the status of an app on a device. This method returns +/// immediately. +/// +/// @param app The IQApp to get the status for. +/// @param completion The completion block that will be triggered when the device +/// status operation is complete. +- (void)getAppStatus:(IQApp *)app completion:(void(^)(IQAppStatus *appStatus))completion; + +/// @brief Registers an object as a listener for ConnectIQ messages from an app +/// on a device. +/// +/// An app may have multiple message listeners if this method is called +/// more than once. +/// +/// @param app The app to listen for messages from. +/// @param delegate The listener which will receive messages for this app. +- (void)registerForAppMessages:(IQApp *)app delegate:(id)delegate; + +/// @brief Unregisters a listener for a specific app. +/// +/// @param app The app to unregister a listener for. +/// @param delegate The listener to remove from the app. +- (void)unregisterForAppMessages:(IQApp *)app delegate:(id)delegate; + +/// @brief Unregisters all previously registered apps for a specific listener. +/// +/// @param delegate The listener to unregister. +- (void)unregisterForAllAppMessages:(id)delegate; + +/// @brief Begins sending a message to an app. This method returns immediately. +/// +/// @param message The message to send to the app. This message must be one of +/// the following types: NSString, NSNumber, NSNull, NSArray, +/// or NSDictionary. Arrays and dictionaries may be nested. +/// @param app The app to send the message to. +/// @param progress A progress block that will be triggered periodically +/// throughout the transfer. This is guaranteed to be triggered +/// at least once. +/// @param completion A completion block that will be triggered when the send +/// message operation is complete. +- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion; + +/// @brief Sends an open app request message request to the device. This method returns immediately. +/// +/// @param app The app to open. +/// @param completion A completion block that will be triggered when the send +/// message operation is complete. +- (void)openAppRequest:(IQApp *)app completion:(IQSendMessageCompletion)completion; + +// TODO *** Holding off on documenting this until this method actually works. +- (void)sendImage:(NSData *)bitmap toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQApp.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQApp.h new file mode 100644 index 000000000..a9dfe8c54 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQApp.h @@ -0,0 +1,34 @@ +// +// IQApp.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import +#import "IQDevice.h" +#import "IQAppStatus.h" + +/// @brief Represents an instance of a ConnectIQ app that is installed on a +/// Garmin device. +@interface IQApp : NSObject + +/// @brief The unique identifier for this app. +@property (nonatomic, readonly) NSUUID *uuid; + +/// @brief The unique identifier for this app in the store. +@property (nonatomic, readonly) NSUUID *storeUuid; + +/// @brief The device that this app is installed on. +@property (nonatomic, readonly) IQDevice *device; + +/// @brief Creates a new app instance. +/// +/// @param uuid The UUID of the app to create. +/// @param storeUuid The store UUID of the app to create. +/// @param device The device the app to create is installed on. +/// +/// @return A new IQApp instance with the appropriate values set. ++ (IQApp *)appWithUUID:(NSUUID *)uuid storeUuid:(NSUUID *)storeUuid device:(IQDevice *)device; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQAppStatus.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQAppStatus.h new file mode 100644 index 000000000..663641d15 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQAppStatus.h @@ -0,0 +1,20 @@ +// +// IQAppStatus.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import + +/// @brief Represents the current status of an app on a Garmin device. +@interface IQAppStatus : NSObject + +/// @brief YES if the app is installed on the device, NO if it isn't. +@property (nonatomic, readonly) BOOL isInstalled; + +/// @brief The version of the app that is currently installed on the device. If +/// the app is not installed, this value is unused. +@property (nonatomic, readonly) uint16_t version; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQConstants.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQConstants.h new file mode 100644 index 000000000..b9017759a --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQConstants.h @@ -0,0 +1,63 @@ +// +// IQConstants.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import + +/// @brief The current version of the ConnectIQ SDK. +extern int const IQSDKVersion; + +/// @brief The bundle identifier for the Garmin Connect Mobile app. +extern NSString * const IQGCMBundle; + +/// @brief The result of a SendMessage operation +typedef NS_ENUM(NSInteger, IQSendMessageResult){ + ///! @brief The message was sent successfully. + IQSendMessageResult_Success, + + /// @brief The message failed to send due to an unknown error. + IQSendMessageResult_Failure_Unknown, + + /// @brief The message failed to send. There was an error within the SDK or + /// on the device. + IQSendMessageResult_Failure_InternalError, + + /// @brief The message failed to send. The device is not available right now. + IQSendMessageResult_Failure_DeviceNotAvailable, + + /// @brief The message failed to send. The app is not installed on the + /// device. + IQSendMessageResult_Failure_AppNotFound, + + /// @brief The message failed to send. The device is busy and cannot receive + /// the message right now. + IQSendMessageResult_Failure_DeviceIsBusy, + + /// @brief The message failed to send. The message contained an unsupported + /// type. + IQSendMessageResult_Failure_UnsupportedType, + + /// @brief The message failed to send. The device does not have enough memory + /// to receive the message. + IQSendMessageResult_Failure_InsufficientMemory, + + /// @brief The message failed to send. The connection timed out while sending + /// the message. + IQSendMessageResult_Failure_Timeout, + + /// @brief The message failed to send and was retried, but could not complete + /// after a number of tries. + IQSendMessageResult_Failure_MaxRetries, + + /// @brief The message was received by the device but it chose not to display + /// a message prompt, ignoring the message. + IQSendMessageResult_Failure_PromptNotDisplayed, + + /// @brief The message was received by the device but the app to open + /// was already running on the device. + IQSendMessageResult_Failure_AppAlreadyRunning, +}; +NSString *NSStringFromSendMessageResult(IQSendMessageResult value); diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQDevice.h b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQDevice.h new file mode 100644 index 000000000..6842829f7 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Headers/IQDevice.h @@ -0,0 +1,61 @@ +// +// IQDevice.h +// ConnectIQ +// +// Copyright (c) 2014 Garmin. All rights reserved. +// + +#import +#import + +/// @brief The current status of an IQDevice. +typedef NS_ENUM(NSInteger, IQDeviceStatus){ + /// @brief No device with this UUID has been registered for status events + /// the SDK. + IQDeviceStatus_InvalidDevice, + + /// @brief Bluetooth is either powered off or resetting. + IQDeviceStatus_BluetoothNotReady, + + /// @brief This device could not be found by iOS. Perhaps the user removed + /// the device? + IQDeviceStatus_NotFound, + + /// @brief The device is recognized by iOS, but it is not currently + /// connected. + IQDeviceStatus_NotConnected, + + /// @brief The device is connected and ready to communicate. + IQDeviceStatus_Connected, +}; + +/// @brief Represents a ConnectIQ-compatible Garmin device. +@interface IQDevice : NSObject + +/// @brief The unique identifier for this device. +@property (nonatomic, readonly) NSUUID *uuid; + +/// @brief The model name of the device provided by Garmin Connect Mobile. +@property (nonatomic, readonly) NSString *modelName; + +/// @brief The friendly name of the device, set by the user and provided by +/// Garmin Connect Mobile. +@property (nonatomic, readonly) NSString *friendlyName; + +/// @brief Creates a new device instance. +/// +/// @param uuid The UUID of the device to create. +/// @param modelName The model name of the device to create. +/// @param friendlyName The friendly name of the device to create. +/// +/// @return A new IQDevice instance with the appropriate values set. ++ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName; + +/// @brief Creates a new device instance by copying another device's values. +/// +/// @param device The device to copy values from. +/// +/// @return A new IQDevice instance with all values copied. +- (instancetype)initWithDevice:(IQDevice *)device; + +@end diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Info.plist b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Info.plist new file mode 100644 index 000000000..7d498ba76 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Info.plist differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Modules/module.modulemap b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Modules/module.modulemap new file mode 100644 index 000000000..685a0721e --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module ConnectIQ { + umbrella header "ConnectIQ.h" + + export * + module * { export * } +} diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/_CodeSignature/CodeResources b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/_CodeSignature/CodeResources new file mode 100644 index 000000000..ffec58498 --- /dev/null +++ b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/_CodeSignature/CodeResources @@ -0,0 +1,830 @@ + + + + + files + + Headers/ConnectIQ.h + + F1hICh90Ex4ADEjYLcSi0YPhrPA= + + Headers/IQApp.h + + R7+SmeArgBACIBWHRnEAugyFHKE= + + Headers/IQAppStatus.h + + WnybOSMMVqCKGns0rEz9C3EfQOg= + + Headers/IQConstants.h + + eI7keKSkaajUZACnuMhgtV1RuBA= + + Headers/IQDevice.h + + bl545C/cu0mw2KlRmzojKmHPom0= + + Info.plist + + sMY09qXRBL/m1OGNWejLjfNg04w= + + Modules/module.modulemap + + SSRVAtIAdFmowQqE4HzOpWYLubg= + + ar.lproj/IQLocalizable.strings + + hash + + 1CDTE/Qaf1Z/HuhSt9CUnwitv4M= + + optional + + + cs.lproj/IQLocalizable.strings + + hash + + /jkyQ77G2Xd9wy6QptBphGNbtCY= + + optional + + + da.lproj/IQLocalizable.strings + + hash + + FYi0wjOu/Hw//Qe96yqxSb9yClc= + + optional + + + de.lproj/IQLocalizable.strings + + hash + + MitzVbGhXhTLjPvw9vuWcQQa50Q= + + optional + + + el.lproj/IQLocalizable.strings + + hash + + n82gLcjjjHszaroTFeJUvSrrc0o= + + optional + + + en.lproj/IQLocalizable.strings + + hash + + hcxxLyrTI+aElXlPc5dwr7jdqwc= + + optional + + + es.lproj/IQLocalizable.strings + + hash + + ff8DVQtNhO8pF7HFnXjh8foHXbo= + + optional + + + fi.lproj/IQLocalizable.strings + + hash + + R9cr8yqJmu91Xz31tGyprGR3t/s= + + optional + + + fr.lproj/IQLocalizable.strings + + hash + + PwFmqFeRTcjdHmkXYrPzNVYoe5o= + + optional + + + he.lproj/IQLocalizable.strings + + hash + + /jPUgFtYbbyELG5DZ3Sjoi/If9w= + + optional + + + hr.lproj/IQLocalizable.strings + + hash + + H2GtdTeORRPCnogvpWY69Dg9uME= + + optional + + + hu.lproj/IQLocalizable.strings + + hash + + QIimMhNyYmqp4ZW01hfj554WAMg= + + optional + + + id.lproj/IQLocalizable.strings + + hash + + 2/54a0gkcVuk1I3m4ulDAXOLL5o= + + optional + + + it.lproj/IQLocalizable.strings + + hash + + hNIKYIcP/87e6g7AUP+zKRtJ52M= + + optional + + + ja.lproj/IQLocalizable.strings + + hash + + 0iU2PbJ/3xgXMZ20ffsqaWpxKWc= + + optional + + + ko.lproj/IQLocalizable.strings + + hash + + ERH8oHR9H9jMHjP0EAgaTtVhnX4= + + optional + + + ms.lproj/IQLocalizable.strings + + hash + + DkbQA2+v/qSgQWma/fg3647Bkqs= + + optional + + + nb.lproj/IQLocalizable.strings + + hash + + T3zFOvuvrJt5Vnmfqt2Mf/du8as= + + optional + + + nl.lproj/IQLocalizable.strings + + hash + + t9PD5JEbfoSLaQ7f8M2cLghOReI= + + optional + + + pl.lproj/IQLocalizable.strings + + hash + + wfTnhBccAm6JfwH/JkZKNRKTUAU= + + optional + + + pt-PT.lproj/IQLocalizable.strings + + hash + + 7yXkcZEpJ4UiRHAzhK+vw/Q857Y= + + optional + + + pt.lproj/IQLocalizable.strings + + hash + + tZPncsQs8weCDJa03AKLpijXSUw= + + optional + + + ru.lproj/IQLocalizable.strings + + hash + + Ct+byJ3rWeigvg0q6rB/kQaR+yE= + + optional + + + sk.lproj/IQLocalizable.strings + + hash + + 1yTM1nAsAYpSH7NrYU6/nFlqk5E= + + optional + + + sv.lproj/IQLocalizable.strings + + hash + + i84z6vuHLrFpO0qZ2V0zYjixIws= + + optional + + + th.lproj/IQLocalizable.strings + + hash + + oW5npy+pDJM1wUOgTkw9FY1Ave4= + + optional + + + tr.lproj/IQLocalizable.strings + + hash + + 76rD7PLrQMiT5YTlI8IjEFgsiU4= + + optional + + + zh-Hans.lproj/IQLocalizable.strings + + hash + + DNlMxUKypOvKArzi7ioJUiFfFXg= + + optional + + + zh-Hant.lproj/IQLocalizable.strings + + hash + + U6I+uL07KIv2b77w0c0glaJlhMg= + + optional + + + + files2 + + Headers/ConnectIQ.h + + hash + + F1hICh90Ex4ADEjYLcSi0YPhrPA= + + hash2 + + ABtgvHbvmly4QpZO/KmmrwYkL0N+AqV3gXdPVrseysY= + + + Headers/IQApp.h + + hash + + R7+SmeArgBACIBWHRnEAugyFHKE= + + hash2 + + X4vXt0sO9gxQNzQalIaLqMpSGNRC9ue2USDcfjBYkec= + + + Headers/IQAppStatus.h + + hash + + WnybOSMMVqCKGns0rEz9C3EfQOg= + + hash2 + + tg9qNXtTmFUvNoJtq7O/aEXBNngcGENVRhvxLJ8C/xo= + + + Headers/IQConstants.h + + hash + + eI7keKSkaajUZACnuMhgtV1RuBA= + + hash2 + + bqDpm8yikc2FIqaSUHcLqPY6TPXLlXSUo+Dl9NUYwmA= + + + Headers/IQDevice.h + + hash + + bl545C/cu0mw2KlRmzojKmHPom0= + + hash2 + + 4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA= + + + Modules/module.modulemap + + hash + + SSRVAtIAdFmowQqE4HzOpWYLubg= + + hash2 + + lQGjVO5Q0wfztjETCwDkwAkQ7nZInCgWdStnHL3o6Co= + + + ar.lproj/IQLocalizable.strings + + hash + + 1CDTE/Qaf1Z/HuhSt9CUnwitv4M= + + hash2 + + CWyQue2TCS0heGoGbN4ffetM2QZSk7lqgc2Wer2fgTg= + + optional + + + cs.lproj/IQLocalizable.strings + + hash + + /jkyQ77G2Xd9wy6QptBphGNbtCY= + + hash2 + + 1mSn+EYeYcTV1dArgHz7PkmZrV6mHWfnuG5aDa6Y87E= + + optional + + + da.lproj/IQLocalizable.strings + + hash + + FYi0wjOu/Hw//Qe96yqxSb9yClc= + + hash2 + + yLkvGzd+smkOjicvW/+Oe6wGGyirHS+/YfjuSzyVoMM= + + optional + + + de.lproj/IQLocalizable.strings + + hash + + MitzVbGhXhTLjPvw9vuWcQQa50Q= + + hash2 + + DFHv7MWBJmyAkOj993NmSFKbS2t8/vtSev603sBUtjI= + + optional + + + el.lproj/IQLocalizable.strings + + hash + + n82gLcjjjHszaroTFeJUvSrrc0o= + + hash2 + + i4FAK4mi+SgS6oZv8zM74kRZToakn49E8GD7FcJBLoQ= + + optional + + + en.lproj/IQLocalizable.strings + + hash + + hcxxLyrTI+aElXlPc5dwr7jdqwc= + + hash2 + + vmBi9DFJzFcG0OwaWKSDjgklNi407U8u2pz3EnEENN4= + + optional + + + es.lproj/IQLocalizable.strings + + hash + + ff8DVQtNhO8pF7HFnXjh8foHXbo= + + hash2 + + z6RjynaWjrRKHmv4sLirc4eXwKOtQdylzj5+TiHpaTc= + + optional + + + fi.lproj/IQLocalizable.strings + + hash + + R9cr8yqJmu91Xz31tGyprGR3t/s= + + hash2 + + 6BI0iPRVWaP63/XFdjLBz6z7DsvvuOoaEAS+mYzrx8E= + + optional + + + fr.lproj/IQLocalizable.strings + + hash + + PwFmqFeRTcjdHmkXYrPzNVYoe5o= + + hash2 + + geXjZzXre2CRiALecPFBGz4JSJA7MbkDnB4qrEMKNwk= + + optional + + + he.lproj/IQLocalizable.strings + + hash + + /jPUgFtYbbyELG5DZ3Sjoi/If9w= + + hash2 + + 47mcrSx16SFjWPIiN7guCAG0va8NiJ6I5s45tSVEHlY= + + optional + + + hr.lproj/IQLocalizable.strings + + hash + + H2GtdTeORRPCnogvpWY69Dg9uME= + + hash2 + + 4bQvygPax6VBpoFlyS5by1N6otnDMliHu+bWsDaWSQc= + + optional + + + hu.lproj/IQLocalizable.strings + + hash + + QIimMhNyYmqp4ZW01hfj554WAMg= + + hash2 + + 0m2fIyz26vh3RlUqqSXvoNTLovxIixrUyJoL/IDSoVk= + + optional + + + id.lproj/IQLocalizable.strings + + hash + + 2/54a0gkcVuk1I3m4ulDAXOLL5o= + + hash2 + + hQf9SrG7d8aVWsXIbCIxkKEJjbnW1FLvS+MbOI1VtHQ= + + optional + + + it.lproj/IQLocalizable.strings + + hash + + hNIKYIcP/87e6g7AUP+zKRtJ52M= + + hash2 + + XAbEWX6cicDxGzxGgSx3DhF4rjUHX4LV+dO0X3rUEqc= + + optional + + + ja.lproj/IQLocalizable.strings + + hash + + 0iU2PbJ/3xgXMZ20ffsqaWpxKWc= + + hash2 + + YOqOvZq0WEN4DCoSwc0lcTSRc4C812DqzjIsaid1SHg= + + optional + + + ko.lproj/IQLocalizable.strings + + hash + + ERH8oHR9H9jMHjP0EAgaTtVhnX4= + + hash2 + + WJyaRCWn1KqmcDeajRnC41MdNrlpbI+1JbPkXhbKrKY= + + optional + + + ms.lproj/IQLocalizable.strings + + hash + + DkbQA2+v/qSgQWma/fg3647Bkqs= + + hash2 + + gztYxa4Hn58HkKmcUIZI1jCz44IETZeMsqrpZSKxJvc= + + optional + + + nb.lproj/IQLocalizable.strings + + hash + + T3zFOvuvrJt5Vnmfqt2Mf/du8as= + + hash2 + + Oy6UOwSN+/xPIrthAEvzV8PEn27kfsHpMMLU5w1rww0= + + optional + + + nl.lproj/IQLocalizable.strings + + hash + + t9PD5JEbfoSLaQ7f8M2cLghOReI= + + hash2 + + XbijhSaZgmsW59Vo9ZEbhDuUQH18fHizWKzsLosiM0o= + + optional + + + pl.lproj/IQLocalizable.strings + + hash + + wfTnhBccAm6JfwH/JkZKNRKTUAU= + + hash2 + + MQYgqA+Hl03JJ261Q19K5Lt64kSTBP+pfpD+jOVE3AU= + + optional + + + pt-PT.lproj/IQLocalizable.strings + + hash + + 7yXkcZEpJ4UiRHAzhK+vw/Q857Y= + + hash2 + + seINq3QazVameLGOW+pIAtGWLa6NDl5XWRtqnObxywo= + + optional + + + pt.lproj/IQLocalizable.strings + + hash + + tZPncsQs8weCDJa03AKLpijXSUw= + + hash2 + + GnzdqEuQwORzVCih99bwr79UHIyzXm+zuN5b9m1NrKY= + + optional + + + ru.lproj/IQLocalizable.strings + + hash + + Ct+byJ3rWeigvg0q6rB/kQaR+yE= + + hash2 + + yCN9s/JXYqsMNZ1icaH4hUwyMQ1NtxOmV6sIAtRd9pc= + + optional + + + sk.lproj/IQLocalizable.strings + + hash + + 1yTM1nAsAYpSH7NrYU6/nFlqk5E= + + hash2 + + OFHDtkGLLSfTuSx8GOTycKDCKOKmX0Wh2QG1CHhRz3I= + + optional + + + sv.lproj/IQLocalizable.strings + + hash + + i84z6vuHLrFpO0qZ2V0zYjixIws= + + hash2 + + a3Gk+3USOT5uundOXrNCgnbcD0rDo2lkCO7b7+zg2Is= + + optional + + + th.lproj/IQLocalizable.strings + + hash + + oW5npy+pDJM1wUOgTkw9FY1Ave4= + + hash2 + + qxGqAqRMwm0/dMd0W7DUsvbWb9x65GT+3d1zOQEql1w= + + optional + + + tr.lproj/IQLocalizable.strings + + hash + + 76rD7PLrQMiT5YTlI8IjEFgsiU4= + + hash2 + + Y6TnKQmqO/TAx+0KYqRRG6UOz7I/gM1YmbUwgSfZSQU= + + optional + + + zh-Hans.lproj/IQLocalizable.strings + + hash + + DNlMxUKypOvKArzi7ioJUiFfFXg= + + hash2 + + BI3m4MTMHuPI4sQKPGeQnxIlBJJrXwgVuR7Ho1Q5o6Y= + + optional + + + zh-Hant.lproj/IQLocalizable.strings + + hash + + U6I+uL07KIv2b77w0c0glaJlhMg= + + hash2 + + 14dQnjX3pEz2Um4J/fOdQDRe/LSuXxqkg1hEkO8E5ys= + + optional + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ar.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ar.lproj/IQLocalizable.strings new file mode 100644 index 000000000..772c7c199 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ar.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/cs.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/cs.lproj/IQLocalizable.strings new file mode 100644 index 000000000..294594b65 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/cs.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/da.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/da.lproj/IQLocalizable.strings new file mode 100644 index 000000000..9c7faad3e Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/da.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/de.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/de.lproj/IQLocalizable.strings new file mode 100644 index 000000000..cb4d87b1b Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/de.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/el.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/el.lproj/IQLocalizable.strings new file mode 100644 index 000000000..8f4e27056 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/el.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/en.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/en.lproj/IQLocalizable.strings new file mode 100644 index 000000000..8794262a9 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/en.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/es.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/es.lproj/IQLocalizable.strings new file mode 100644 index 000000000..247b71ac7 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/es.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/fi.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/fi.lproj/IQLocalizable.strings new file mode 100644 index 000000000..344d06c1b Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/fi.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/fr.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/fr.lproj/IQLocalizable.strings new file mode 100644 index 000000000..814e34f18 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/fr.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/he.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/he.lproj/IQLocalizable.strings new file mode 100644 index 000000000..b29a9b084 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/he.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/hr.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/hr.lproj/IQLocalizable.strings new file mode 100644 index 000000000..e10bd29af Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/hr.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/hu.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/hu.lproj/IQLocalizable.strings new file mode 100644 index 000000000..9c96de1e8 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/hu.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/id.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/id.lproj/IQLocalizable.strings new file mode 100644 index 000000000..eda1bc3d3 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/id.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/it.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/it.lproj/IQLocalizable.strings new file mode 100644 index 000000000..1bd95ac77 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/it.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ja.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ja.lproj/IQLocalizable.strings new file mode 100644 index 000000000..0f5b121c0 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ja.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ko.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ko.lproj/IQLocalizable.strings new file mode 100644 index 000000000..3f3749b0e Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ko.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ms.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ms.lproj/IQLocalizable.strings new file mode 100644 index 000000000..499c806b2 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ms.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/nb.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/nb.lproj/IQLocalizable.strings new file mode 100644 index 000000000..436c9f67d Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/nb.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/nl.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/nl.lproj/IQLocalizable.strings new file mode 100644 index 000000000..97d1c509c Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/nl.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pl.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pl.lproj/IQLocalizable.strings new file mode 100644 index 000000000..0f8f3d72b Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pl.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings new file mode 100644 index 000000000..6f9022878 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pt.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pt.lproj/IQLocalizable.strings new file mode 100644 index 000000000..ddc7d69a1 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/pt.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ru.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ru.lproj/IQLocalizable.strings new file mode 100644 index 000000000..ce83c0a22 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/ru.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/sk.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/sk.lproj/IQLocalizable.strings new file mode 100644 index 000000000..98b954bff Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/sk.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/sv.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/sv.lproj/IQLocalizable.strings new file mode 100644 index 000000000..316d6f897 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/sv.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/th.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/th.lproj/IQLocalizable.strings new file mode 100644 index 000000000..f98199499 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/th.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/tr.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/tr.lproj/IQLocalizable.strings new file mode 100644 index 000000000..c71eb4a00 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/tr.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings new file mode 100644 index 000000000..d630d3f0c Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings differ diff --git a/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings new file mode 100644 index 000000000..db0daa2c7 Binary files /dev/null and b/src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-i386_x86_64-simulator/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings differ diff --git a/src/Home.qml b/src/Home.qml index 61d46ef21..176481783 100644 --- a/src/Home.qml +++ b/src/Home.qml @@ -9,6 +9,12 @@ import QtMultimedia 5.15 HomeForm{ objectName: "home" + background: Rectangle { + anchors.fill: parent + width: parent.fill + height: parent.fill + color: settings.theme_background_color + } signal start_clicked; signal stop_clicked; signal lap_clicked; @@ -21,6 +27,12 @@ HomeForm{ Settings { id: settings property real ui_zoom: 100.0 + property bool theme_tile_icon_enabled: true + property string theme_tile_background_color: "#303030" + property string theme_background_color: "#303030" + property bool theme_tile_shadow_enabled: true + property string theme_tile_shadow_color: "#9C27B0" + property int theme_tile_secondline_textsize: 12 } MessageDialog { @@ -154,26 +166,27 @@ HomeForm{ height: 123 * settings.ui_zoom / 100 radius: 3 border.width: 1 - border.color: "purple" - color: Material.backgroundColor + border.color: (settings.theme_tile_shadow_enabled ? settings.theme_tile_shadow_color : settings.theme_tile_background_color) + color: settings.theme_tile_background_color id: rect } DropShadow { + visible: settings.theme_tile_shadow_enabled anchors.fill: rect cached: true horizontalOffset: 3 verticalOffset: 3 radius: 8.0 samples: 16 - color: Material.color(Material.Purple) + color: settings.theme_tile_shadow_color source: rect } Timer { id: toggleIconTimer interval: 500; running: true; repeat: true - onTriggered: { if(identificator === "inclination" && rootItem.autoInclinationEnabled()) myIcon.visible = !myIcon.visible; else myIcon.visible = true; } + onTriggered: { if(identificator === "inclination" && rootItem.autoInclinationEnabled()) myIcon.visible = !myIcon.visible; else myIcon.visible = settings.theme_tile_icon_enabled && !largeButton; } } Image { @@ -185,7 +198,7 @@ HomeForm{ width: 48 * settings.ui_zoom / 100 height: 48 * settings.ui_zoom / 100 source: icon - visible: !largeButton + visible: settings.theme_tile_icon_enabled && !largeButton } Text { objectName: "value" @@ -212,7 +225,7 @@ HomeForm{ } text: secondLine horizontalAlignment: Text.AlignHCenter - font.pointSize: 12 * settings.ui_zoom / 100 + font.pointSize: settings.theme_tile_secondline_textsize * settings.ui_zoom / 100 font.bold: false visible: !largeButton } @@ -278,57 +291,75 @@ HomeForm{ } footer: - Rectangle { - objectName: "footerrectangle" - visible: rootItem.videoVisible - anchors.top: gridView.bottom + Item { width: parent.width - height: parent.height / 2 + height: (rootItem.chartFooterVisible ? parent.height / 4 : parent.height / 2) + anchors.top: gridView.bottom + visible: rootItem.chartFooterVisible || rootItem.videoVisible - Timer { - id: pauseTimer - interval: 1000; running: true; repeat: true - onTriggered: { if(visible == true) { (rootItem.currentSpeed > 0 ? - videoPlaybackHalf.play() : - videoPlaybackHalf.pause()) } } + Rectangle { + id: chartFooterRectangle + visible: rootItem.chartFooterVisible + anchors.fill: parent + ChartFooter { + anchors.fill: parent + visible: rootItem.chartFooterVisible + } } - onVisibleChanged: { - if(visible === true) { - console.log("mediaPlayer onCompleted: " + rootItem.videoPath) - console.log("videoRate: " + rootItem.videoRate) - videoPlaybackHalf.source = rootItem.videoPath - //videoPlaybackHalf.playbackRate = rootItem.videoRate - - videoPlaybackHalf.seek(rootItem.videoPosition) - videoPlaybackHalf.play() - videoPlaybackHalf.muted = true - } else { - videoPlaybackHalf.stop() + Rectangle { + objectName: "footerrectangle" + visible: rootItem.videoVisible + anchors.fill: parent + // Removed Timer, Play/Pause/Resume is now done via Homeform.cpp + /* + Timer { + id: pauseTimer + interval: 1000; running: true; repeat: true + onTriggered: { if(visible == true) { (rootItem.currentSpeed > 0 ? + videoPlaybackHalf.play() : + videoPlaybackHalf.pause()) } } } + */ + + onVisibleChanged: { + if(visible === true) { + console.log("mediaPlayer onCompleted: " + rootItem.videoPath) + console.log("videoRate: " + rootItem.videoRate) + videoPlaybackHalf.source = rootItem.videoPath + //videoPlaybackHalf.playbackRate = rootItem.videoRate + + videoPlaybackHalf.seek(rootItem.videoPosition) + videoPlaybackHalf.play() + videoPlaybackHalf.muted = rootItem.currentCoordinateValid + } else { + videoPlaybackHalf.stop() + } - } + } - MediaPlayer { - id: videoPlaybackHalf - objectName: "videoplaybackhalf" - autoPlay: false - playbackRate: rootItem.videoRate + MediaPlayer { + id: videoPlaybackHalf + objectName: "videoplaybackhalf" + autoPlay: false + playbackRate: rootItem.videoRate - onError: { - if (videoPlaybackHalf.NoError !== error) { - console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString) + onError: { + if (videoPlaybackHalf.NoError !== error) { + console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString) + } } + } - } + VideoOutput { + id:videoPlayer + anchors.fill: parent + source: videoPlaybackHalf + } + } - VideoOutput { - id:videoPlayer - anchors.fill: parent - source: videoPlaybackHalf - } - } + } MouseArea { property int currentId: -1 // Original position in model diff --git a/src/HomeForm.ui.qml b/src/HomeForm.ui.qml index d112a5d4a..39d85f299 100644 --- a/src/HomeForm.ui.qml +++ b/src/HomeForm.ui.qml @@ -2,6 +2,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.5 import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.12 +import Qt.labs.settings 1.0 Page { @@ -13,6 +14,13 @@ Page { property alias lap: lap property alias row: row + Settings { + id: settings + property real ui_zoom: 100.0 + property bool theme_tile_icon_enabled: true + property string theme_background_color: "#303030" + } + Item { width: parent.width height: rootItem.topBarHeight @@ -30,7 +38,7 @@ Page { Rectangle { width: 50 height: row.height - color: Material.backgroundColor + color: settings.theme_background_color Column { id: column anchors.horizontalCenter: parent.horizontalCenter @@ -42,7 +50,7 @@ Page { Rectangle { width: 50 height: row.height - color: Material.backgroundColor + color: settings.theme_background_color Image { anchors.verticalCenter: parent.verticalCenter @@ -73,7 +81,7 @@ Page { Rectangle { width: 120 height: row.height - color: Material.backgroundColor + color: settings.theme_background_color RoundButton { icon.source: rootItem.startIcon icon.height: row.height - 54 @@ -95,7 +103,7 @@ Page { Rectangle { width: 120 height: row.height - color: Material.backgroundColor + color: settings.theme_background_color RoundButton { icon.source: rootItem.stopIcon @@ -119,7 +127,7 @@ Page { id: item2 width: 50 height: row.height - color: Material.backgroundColor + color: settings.theme_background_color RoundButton { anchors.verticalCenter: parent.verticalCenter id: lap diff --git a/src/SwagBagView.qml b/src/SwagBagView.qml index 0b7b5aa11..3da0b4254 100644 --- a/src/SwagBagView.qml +++ b/src/SwagBagView.qml @@ -93,7 +93,7 @@ Item { onLinkActivated: Qt.openUrlExternally(link) } - /*Button { + Button { id: restoreButton anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter @@ -101,7 +101,8 @@ Item { text: "Restore Purchases" onClicked: { console.log("restoring..."); + toast.show("Restoring..."); iapStore.restorePurchases(); } - }*/ + } } diff --git a/src/Toast.qml b/src/Toast.qml new file mode 100644 index 000000000..0900915ba --- /dev/null +++ b/src/Toast.qml @@ -0,0 +1,96 @@ +import QtQuick 2.0 + +/** + * adapted from StackOverflow: + * http://stackoverflow.com/questions/26879266/make-toast-in-android-by-qml + */ + +/** + * @brief An Android-like timed message text in a box that self-destroys when finished if desired + */ +Rectangle { + + /** + * Public + */ + + /** + * @brief Shows this Toast + * + * @param {string} text Text to show + * @param {real} duration Duration to show in milliseconds, defaults to 3000 + */ + function show(text, duration) { + message.text = text; + if (typeof duration !== "undefined") { // checks if parameter was passed + time = Math.max(duration, 2 * fadeTime); + } + else { + time = defaultTime; + } + animation.start(); + } + + /** + * Private + */ + + id: root + + readonly property real defaultTime: 3000 + property real time: defaultTime + readonly property real fadeTime: 300 + + property real margin: 10 + + anchors { + left: (parent != null) ? parent.left : undefined + right: (parent != null) ? parent.right : undefined + margins: margin + } + + height: message.height + margin + radius: margin + + opacity: 0 + color: "#222222" + + Text { + id: message + color: "white" + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: margin / 2 + } + } + + SequentialAnimation on opacity { + id: animation + running: false + + + NumberAnimation { + to: .9 + duration: fadeTime + } + + PauseAnimation { + duration: time - 2 * fadeTime + } + + NumberAnimation { + to: 0 + duration: fadeTime + } + + onRunningChanged: { + if (!running) { ++ toastManager.model.remove(index); + } + } + } +} diff --git a/src/ToastManager.qml b/src/ToastManager.qml new file mode 100644 index 000000000..c8f020c35 --- /dev/null +++ b/src/ToastManager.qml @@ -0,0 +1,56 @@ +import QtQuick 2.0 + +/** + * adapted from StackOverflow: + * http://stackoverflow.com/questions/26879266/make-toast-in-android-by-qml + * @brief Manager that creates Toasts dynamically + */ +ListView { + /** + * Public + */ + + /** + * @brief Shows a Toast + * + * @param {string} text Text to show + * @param {real} duration Duration to show in milliseconds, defaults to 3000 + */ + function show(text, duration) { + model.insert(0, {text: text, duration: duration}); + } + + /** + * Private + */ + + id: root + + z: Infinity + spacing: 5 + anchors.fill: parent + anchors.bottomMargin: 10 + verticalLayoutDirection: ListView.BottomToTop + + interactive: false + + displaced: Transition { + NumberAnimation { + properties: "y" + easing.type: Easing.InOutQuad + } + } + + delegate: Toast { + Component.onCompleted: { + if (typeof duration === "undefined") { + show(text); + } + else { + show(text, duration); + } + } + } + + model: ListModel {id: model} +} diff --git a/src/TrainingProgramsList.qml b/src/TrainingProgramsList.qml index 4938397b8..5e49e71d4 100644 --- a/src/TrainingProgramsList.qml +++ b/src/TrainingProgramsList.qml @@ -102,9 +102,9 @@ ColumnLayout { clip: true Text { id: fileTextBox - color: (!folderModel.isFolder(index)?Material.color(Material.Grey):Material.color(Material.Orange)) + color: (!folderModel.isFolder(index)?Material.color(Material.Grey):Material.color(Material.Orange)) font.pixelSize: Qt.application.font.pixelSize * 1.6 - text: fileName.substring(0, fileName.length-4) + text: (!folderModel.isFolder(index)?fileName.substring(0, fileName.length-4):fileName) NumberAnimation on x { Component.onCompleted: { if(fileName.length > 30) { diff --git a/src/activiotreadmill.cpp b/src/activiotreadmill.cpp index 4294447c2..7f823d760 100644 --- a/src/activiotreadmill.cpp +++ b/src/activiotreadmill.cpp @@ -1,7 +1,9 @@ #include "activiotreadmill.h" +#include "virtualbike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include @@ -54,10 +56,15 @@ void activiotreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac return; } - gattCommunicationChannelService->writeCharacteristic(characteristic, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(characteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -74,59 +81,59 @@ void activiotreadmill::forceSpeed(double requestSpeed) { writeSpeed[1] = (requestSpeed * 10); writeSpeed[5] += writeSpeed[1]; - if(!settings.value(QZSettings::fitfiu_mc_v460, QZSettings::default_fitfiu_mc_v460).toBool()) + if (!settings.value(QZSettings::fitfiu_mc_v460, QZSettings::default_fitfiu_mc_v460).toBool() && + !settings.value(QZSettings::zero_zt2500_treadmill, QZSettings::default_zero_zt2500_treadmill).toBool()) writeSpeed[6] = writeSpeed[1] + 1; else { - switch(writeSpeed[1] & 0x0F) { - case 0x00: - writeSpeed[6] = writeSpeed[1] + 5; - break; - case 0x01: - writeSpeed[6] = writeSpeed[1] + 3; - break; - case 0x02: - writeSpeed[6] = writeSpeed[1] + 1; - break; - case 0x03: - writeSpeed[6] = writeSpeed[1] - 1; - break; - case 0x04: - writeSpeed[6] = writeSpeed[1] + 5; - break; - case 0x05: - writeSpeed[6] = writeSpeed[1] + 3; - break; - case 0x06: - writeSpeed[6] = writeSpeed[1] + 1; - break; - case 0x07: - writeSpeed[6] = writeSpeed[1] - 1; - break; - case 0x08: - writeSpeed[6] = writeSpeed[1] + 5; - break; - case 0x09: - writeSpeed[6] = writeSpeed[1] + 3; - break; - case 0x0A: - writeSpeed[6] = writeSpeed[1] + 1; - break; - case 0x0B: - writeSpeed[6] = writeSpeed[1] - 1; - break; - case 0x0C: - writeSpeed[6] = writeSpeed[1] + 5; - break; - case 0x0D: - writeSpeed[6] = writeSpeed[1] + 3; - break; - case 0x0E: - writeSpeed[6] = writeSpeed[1] + 1; - break; - case 0x0F: - writeSpeed[6] = writeSpeed[1] - 1; - break; - + switch (writeSpeed[1] & 0x0F) { + case 0x00: + writeSpeed[6] = writeSpeed[1] + 5; + break; + case 0x01: + writeSpeed[6] = writeSpeed[1] + 3; + break; + case 0x02: + writeSpeed[6] = writeSpeed[1] + 1; + break; + case 0x03: + writeSpeed[6] = writeSpeed[1] - 1; + break; + case 0x04: + writeSpeed[6] = writeSpeed[1] + 5; + break; + case 0x05: + writeSpeed[6] = writeSpeed[1] + 3; + break; + case 0x06: + writeSpeed[6] = writeSpeed[1] + 1; + break; + case 0x07: + writeSpeed[6] = writeSpeed[1] - 1; + break; + case 0x08: + writeSpeed[6] = writeSpeed[1] + 5; + break; + case 0x09: + writeSpeed[6] = writeSpeed[1] + 3; + break; + case 0x0A: + writeSpeed[6] = writeSpeed[1] + 1; + break; + case 0x0B: + writeSpeed[6] = writeSpeed[1] - 1; + break; + case 0x0C: + writeSpeed[6] = writeSpeed[1] + 5; + break; + case 0x0D: + writeSpeed[6] = writeSpeed[1] + 3; + break; + case 0x0E: + writeSpeed[6] = writeSpeed[1] + 1; + break; + case 0x0F: + writeSpeed[6] = writeSpeed[1] - 1; + break; } } @@ -167,21 +174,26 @@ void activiotreadmill::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstInit && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &activiotreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &activiotreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &activiotreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstInit = 1; } @@ -201,7 +213,7 @@ void activiotreadmill::update() { requestSpeed = -1; } if (requestInclination != -100) { - if(requestInclination < 0) + if (requestInclination < 0) requestInclination = 0; if (requestInclination != currentInclination().value() && requestInclination >= 0 && requestInclination <= 15) { @@ -297,10 +309,10 @@ void activiotreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha double speed = GetSpeedFromPacket(value); double incline = 1.0; // "fitfiu_mc_v460" has 1.0 fixed inclination - if(!settings.value(QZSettings::fitfiu_mc_v460, QZSettings::default_fitfiu_mc_v460).toBool()) + if (!settings.value(QZSettings::fitfiu_mc_v460, QZSettings::default_fitfiu_mc_v460).toBool()) incline = GetInclinationFromPacket(value); - // double kcal = GetKcalFromPacket(value); - // double distance = GetDistanceFromPacket(value); + // double kcal = GetKcalFromPacket(value); + // double distance = GetDistanceFromPacket(value); #ifdef Q_OS_ANDROID if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) @@ -312,17 +324,7 @@ void activiotreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha uint8_t heart = 0; if (heart == 0) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else Heart = heart; @@ -332,7 +334,8 @@ void activiotreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha if (!firstCharacteristicChanged) { if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) KCal += - ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) * + ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo( @@ -518,7 +521,9 @@ void activiotreadmill::btinit(bool startTape) { uint8_t initData2[] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x26, 0x03}; writeCharacteristic(gattWrite2Characteristic, initData1, sizeof(initData1), QStringLiteral("init"), false, false); - writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("init"), false, true); + + // starts the tape + // writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("init"), false, true); if (startTape) { } @@ -675,8 +680,4 @@ bool activiotreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *activiotreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *activiotreadmill::VirtualDevice() { return VirtualTreadMill(); } - void activiotreadmill::searchingStop() { searchStopped = true; } diff --git a/src/activiotreadmill.h b/src/activiotreadmill.h index 9e4c3b502..5acb3e085 100644 --- a/src/activiotreadmill.h +++ b/src/activiotreadmill.h @@ -28,8 +28,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -41,11 +39,8 @@ class activiotreadmill : public treadmill { public: activiotreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - - void *VirtualTreadMill(); - void *VirtualDevice(); + bool connected() override; + double minStepInclination() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -67,8 +62,6 @@ class activiotreadmill : public treadmill { bool firstCharacteristicChanged = true; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index 84e2df5a9..4d3ee80fa 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -1,5 +1,5 @@ - + @@ -15,6 +15,12 @@ + + + + @@ -73,6 +79,19 @@ android:name=".ForegroundService" android:enabled="true" android:exported="true"> + + + + + + + diff --git a/src/android/build.gradle b/src/android/build.gradle index 876dd794a..842589948 100644 --- a/src/android/build.gradle +++ b/src/android/build.gradle @@ -38,14 +38,15 @@ dependencies { def appcompat_version = "1.3.1" implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation "com.android.billingclient:billing:4.0.0" + implementation "com.android.billingclient:billing:5.0.0" implementation 'com.android.support:appcompat-v7:28.0.0' implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation 'com.github.mik3y:usb-serial-for-android:3.4.6' + implementation 'com.github.mik3y:usb-serial-for-android:v3.4.6' androidTestImplementation "com.android.support:support-annotations:28.0.0" + implementation 'com.google.android.gms:play-services-wearable:+' } android { @@ -100,4 +101,24 @@ android { minSdkVersion = 21 targetSdkVersion = 33 } + +tasks.all { task -> + if (task.name == 'compileDebugJavaWithJavac' && amazon == "1") { + task.dependsOn copyArm64Directory + task.dependsOn copyArm32Directory + } +} +} + + +task copyArm64Directory(type: Copy) { + from "libs/arm64-v8a/" + include '*arm64-v8a.so' + into "libs/armeabi-v7a/" +} + +task copyArm32Directory(type: Copy) { + from "libs/armeabi-v7a/" + include '*armeabi-v7a.so' + into "libs/arm64-v8a/" } diff --git a/src/android/libs/android_antlib_4-14-0.jar b/src/android/libs/android_antlib_4-14-0.jar deleted file mode 100644 index c61fd100e..000000000 Binary files a/src/android/libs/android_antlib_4-14-0.jar and /dev/null differ diff --git a/src/android/libs/android_antlib_4-16-0.aar b/src/android/libs/android_antlib_4-16-0.aar new file mode 100755 index 000000000..f562c3446 Binary files /dev/null and b/src/android/libs/android_antlib_4-16-0.aar differ diff --git a/src/android/libs/connectiq-mobile-sdk-android-1.5.aar b/src/android/libs/connectiq-mobile-sdk-android-1.5.aar new file mode 100644 index 000000000..bb5f4ef54 Binary files /dev/null and b/src/android/libs/connectiq-mobile-sdk-android-1.5.aar differ diff --git a/src/android/res/xml/device_filter.xml b/src/android/res/xml/device_filter.xml new file mode 100644 index 000000000..64c8b6de2 --- /dev/null +++ b/src/android/res/xml/device_filter.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/src/Ant.java b/src/android/src/Ant.java index cf0240b42..15f2b815f 100644 --- a/src/android/src/Ant.java +++ b/src/android/src/Ant.java @@ -35,11 +35,13 @@ public class Ant { static boolean speedRequest = false; static boolean heartRequest = false; static boolean garminKey = false; + static boolean treadmill = false; - public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey) { + public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill) { Log.v(TAG, "antStart"); speedRequest = SpeedRequest; heartRequest = HeartRequest; + treadmill = Treadmill; garminKey = GarminKey; activity = a; diff --git a/src/android/src/CSafeRowerUSBHID.java b/src/android/src/CSafeRowerUSBHID.java new file mode 100644 index 000000000..c3c79c006 --- /dev/null +++ b/src/android/src/CSafeRowerUSBHID.java @@ -0,0 +1,71 @@ +package org.cagnulen.qdomyoszwift; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Deque; +import java.util.EnumSet; +import java.util.concurrent.Callable; +import java.util.ArrayList; +import java.util.List; +import java.nio.charset.StandardCharsets; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.util.Log; + +public class CSafeRowerUSBHID { + + static HidBridge hidBridge; + static byte[] receiveData; + static int lastReadLen = 0; + + public static void open(Context context) { + Log.d("QZ","CSafeRowerUSBHID open"); + hidBridge = new HidBridge(context, 0x0002, 0x17A4); + boolean ret = hidBridge.OpenDevice(); + Log.d("QZ","hidBridge.OpenDevice " + ret); + if(ret == false) { + hidBridge = new HidBridge(context, 0x0001, 0x17A4); + ret = hidBridge.OpenDevice(); + Log.d("QZ","hidBridge.OpenDevice " + ret); + } + hidBridge.StartReadingThread(); + Log.d("QZ","hidBridge.StartReadingThread"); + } + + public static void write (byte[] bytes) { + Log.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1)); + hidBridge.WriteData(bytes); + } + + public static int readLen() { + return lastReadLen; + } + + public static byte[] read() { + if(hidBridge.IsThereAnyReceivedData()) { + receiveData = hidBridge.GetReceivedDataFromQueue(); + lastReadLen = receiveData.length; + Log.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1)); + return receiveData; + } else { + Log.d("QZ","CSafeRowerUSBHID empty data"); + lastReadLen = 0; + return null; + } + } +} diff --git a/src/android/src/ChannelService.java b/src/android/src/ChannelService.java index 3b277c5bd..1cdeea263 100644 --- a/src/android/src/ChannelService.java +++ b/src/android/src/ChannelService.java @@ -50,6 +50,7 @@ public class ChannelService extends Service { HeartChannelController heartChannelController = null; PowerChannelController powerChannelController = null; SpeedChannelController speedChannelController = null; + SDMChannelController sdmChannelController = null; private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() { @Override @@ -104,6 +105,9 @@ void setSpeed(double speed) { if (null != speedChannelController) { speedChannelController.speed = speed; } + if (null != sdmChannelController) { + sdmChannelController.speed = speed; + } } void setPower(int power) { @@ -119,6 +123,9 @@ void setCadence(int cadence) { if (null != speedChannelController) { speedChannelController.cadence = cadence; } + if (null != sdmChannelController) { + sdmChannelController.cadence = cadence; + } } int getHeart() { @@ -141,8 +148,12 @@ public void openAllChannels() throws ChannelNotAvailableException { heartChannelController = new HeartChannelController(acquireChannel()); if (Ant.speedRequest) { - powerChannelController = new PowerChannelController(acquireChannel()); - speedChannelController = new SpeedChannelController(acquireChannel()); + if(Ant.treadmill) { + sdmChannelController = new SDMChannelController(acquireChannel()); + } else { + powerChannelController = new PowerChannelController(acquireChannel()); + speedChannelController = new SpeedChannelController(acquireChannel()); + } } } @@ -153,9 +164,12 @@ private void closeAllChannels() { powerChannelController.close(); if (speedChannelController != null) speedChannelController.close(); + if (sdmChannelController != null) + sdmChannelController.close(); heartChannelController = null; powerChannelController = null; speedChannelController = null; + sdmChannelController = null; } AntChannel acquireChannel() throws ChannelNotAvailableException { @@ -199,12 +213,15 @@ public IBinder onBind(Intent arg0) { private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive"); if (AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) { boolean update = false; // Retrieving the data contained in the intent int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0); boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false); + Log.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse); + if (mAllowAddChannel) { // Was a acquire channel allowed // If no channels available AND legacy interface is not in use, disallow acquiring of channels diff --git a/src/android/src/Garmin.java b/src/android/src/Garmin.java new file mode 100644 index 000000000..d289b4156 --- /dev/null +++ b/src/android/src/Garmin.java @@ -0,0 +1,283 @@ +package org.cagnulen.qdomyoszwift; + +import android.app.ActivityManager; +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; +import android.os.Looper; +import android.os.Handler; +import android.util.Log; +import com.garmin.android.connectiq.ConnectIQ; +import com.garmin.android.connectiq.ConnectIQAdbStrategy; +import com.garmin.android.connectiq.IQApp; +import com.garmin.android.connectiq.IQDevice; +import com.garmin.android.connectiq.exception.InvalidStateException; +import com.garmin.android.connectiq.exception.ServiceUnavailableException; +import android.content.BroadcastReceiver; +import android.content.ContextWrapper; +import android.content.IntentFilter; +import android.widget.Toast; + +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; + +public class Garmin { + + private static IQDevice deviceCache; + + public static Boolean connectIqReady = false; + private static Boolean connectIqInitializing = false; + + private static ConnectIQ connectIQ; + private static Context context; + + private static final String TAG = "CIQManager: "; + + private static Integer HR = 0; + private static Integer FootCad = 0; + + public static int getHR() { + Log.d(TAG, "getHR " + HR); + return HR; + } + + public static int getFootCad() { + Log.d(TAG, "getFootCad " + FootCad); + return FootCad; + } + + public static void init(Context c) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + connectIQ = ConnectIQ.getInstance(c, ConnectIQ.IQConnectType.WIRELESS); + + // init a wrapped SDK with fix for "Cannot cast to Long" issue viz https://forums.garmin.com/forum/developers/connect-iq/connect-iq-bug-reports/158068-?p=1278464#post1278464 + context = initializeConnectIQWrapped(c, connectIQ, false, new ConnectIQ.ConnectIQListener() { + + @Override + public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) { + Log.e(TAG, errStatus.toString()); + connectIqReady = false; + } + + @Override + public void onSdkReady() { + connectIqInitializing = false; + connectIqReady = true; + Log.i(TAG, " onSdkReady"); + + registerWatchMessagesReceiver(); + registerDeviceStatusReceiver(); + isWatchAppAvailable(); + } + + @Override + public void onSdkShutDown() { + connectIqInitializing = false; + connectIqReady = false; + } + }); + } + }); + } + + @Nullable + public static IQDevice getDevice() { + return getDevice(connectIQ); + } + + @Nullable + private static IQDevice getDevice(ConnectIQ connectIQ) { + try { + List devices = connectIQ.getConnectedDevices(); + if (devices != null && devices.size() > 0) { + Log.v(TAG, "getDevice connected: " + devices.get(0).toString() ); + deviceCache = devices.get(0); + return deviceCache; + } else { + return deviceCache; + } + } catch (InvalidStateException e) { + Log.e(TAG, e.toString()); + } catch (ServiceUnavailableException e) { + Log.e(TAG, e.toString()); + } + return null; + } + + // Should rewrite to use connectiq.getApplicationInfo() with callback (maybe wrap in RxJava) + public static IQApp getApp() { + return new IQApp("feec8674-2795-4e03-a283-0b69a0a291e3"); + } + + public void onOpenAppOnWatch(ConnectIQ.IQOpenApplicationListener listener) throws InvalidStateException, ServiceUnavailableException { + if (getDevice() == null) { return; } + + if (getDevice().getFriendlyName().contains("vívoactive3")) { + //Notifications.showCannotStartFromPhoneNotification(context); + } else { + connectIQ.openApplication(getDevice(), getApp(), listener); + } + } + + public void sendMessageToWatch(String message, ConnectIQ.IQSendMessageListener listener) throws InvalidStateException, ServiceUnavailableException { + connectIQ.sendMessage(getDevice(), getApp(), message, listener); + } + + private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) { + if (connectIQ instanceof ConnectIQAdbStrategy) { + connectIQ.initialize(context, autoUI, listener); + return context; + } + Context wrappedContext = new ContextWrapper(context) { + private HashMap receiverToWrapper = new HashMap<>(); + + @Override + public Intent registerReceiver(final BroadcastReceiver receiver, IntentFilter filter) { + BroadcastReceiver wrappedRecv = new IQMessageReceiverWrapper(receiver); + synchronized (receiverToWrapper) { + receiverToWrapper.put(receiver, wrappedRecv); + } + return super.registerReceiver(wrappedRecv, filter); + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + BroadcastReceiver wrappedReceiver = null; + synchronized (receiverToWrapper) { + wrappedReceiver = receiverToWrapper.get(receiver); + receiverToWrapper.remove(receiver); + } + if (wrappedReceiver != null) super.unregisterReceiver(wrappedReceiver); + } + }; + connectIQ.initialize(wrappedContext, autoUI, listener); + return wrappedContext; + } + + private static void isWatchAppAvailable() { + try { + connectIQ.getApplicationInfo("feec8674-2795-4e03-a283-0b69a0a291e3", getDevice(), new ConnectIQ.IQApplicationInfoListener() { + + @Override + public void onApplicationInfoReceived(IQApp app) { + Log.d(TAG, "App installed."); + } + + @Override + public void onApplicationNotInstalled(String applicationId) { + if (getDevice() != null) { + Toast.makeText(context, "App not installed on your Garmin watch", Toast.LENGTH_LONG).show(); + Log.d(TAG, "watch app not installed."); + } + } + }); + } catch (InvalidStateException e) { + Log.e(TAG, e.toString()); + } catch (ServiceUnavailableException e) { + Log.e(TAG, e.toString()); + } + } + + private static void registerDeviceStatusReceiver() { + Log.d(TAG, "registerDeviceStatusReceiver"); + IQDevice device = getDevice(); + try { + if (device != null) { + connectIQ.registerForDeviceEvents(device, new ConnectIQ.IQDeviceEventListener() { + @Override + public void onDeviceStatusChanged(IQDevice device, IQDevice.IQDeviceStatus newStatus) { + Log.d(TAG, "Device status changed, now " + newStatus); + } + }); + } + } catch (InvalidStateException e) { + e.printStackTrace(); + } + } + + private static void registerWatchMessagesReceiver(){ + Log.d(TAG, "registerWatchMessageReceiver"); + IQDevice device = getDevice(); + try { + if (device != null) { + connectIQ.registerForAppEvents(device, getApp(), new ConnectIQ.IQApplicationEventListener() { + @Override + public void onMessageReceived(IQDevice device, IQApp app, List message, ConnectIQ.IQMessageStatus status) { + if (status == ConnectIQ.IQMessageStatus.SUCCESS) { + //MessageHandler.getInstance().handleMessageFromWatchUsingCIQ(message, status, context); + Log.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0)); + String var[] = message.toArray()[0].toString().split(","); + HR = Integer.parseInt(var[0].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]); + if(var.length > 1) { + FootCad = Integer.parseInt(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]); + } + Log.d(TAG, "HR " + HR); + Log.d(TAG, "FootCad " + FootCad); + } else { + Log.d(TAG, "onMessageReceived error, status: " + status.toString()); + } + } + }); + } else { + Log.d(TAG, "registerWatchMessagesReceiver: No device found."); + } + } catch (InvalidStateException e) { + Log.e(TAG, e.toString()); + } + } + + public void shutdown(Context applicationContext) { + connectIqReady = false; + unregisterApp(connectIQ); + + try { + if (context != null) { + Log.d(TAG, "Shutting down with wrapped context"); + connectIQ.shutdown(context); + } else { + Log.d(TAG, "Shutting down without wrapped context"); + connectIQ.shutdown(applicationContext); + } + } catch (InvalidStateException e) { + // This is usually because the SDK was already shut down so no worries. + Log.e(TAG, "This is usually because the SDK was already shut down so no worries.", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, e.toString()); + } catch (RuntimeException e) { + Log.e(TAG, e.toString()); + } + } + + private void unregisterApp(ConnectIQ connectIQ) { + try { + if (connectIQ != null) { + IQDevice device = getDevice(); + if (device != null) { + connectIQ.unregisterForApplicationEvents(device, getApp()); + connectIQ.unregisterForDeviceEvents(device); + } + } + } catch (InvalidStateException e) { + Log.e(TAG, e.toString()); + } catch (IllegalArgumentException e) { + Log.e(TAG, e.toString()); + } catch (RuntimeException e) { + Log.e(TAG, e.toString()); + } + } +} diff --git a/src/android/src/HidBridge.java b/src/android/src/HidBridge.java new file mode 100644 index 000000000..63c5cf661 --- /dev/null +++ b/src/android/src/HidBridge.java @@ -0,0 +1,356 @@ +package org.cagnulen.qdomyoszwift; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.util.Log; +import android.os.Build; + +/** + * This class is used for talking to hid of the dongle, connecting, disconnencting and enumerating the devices. + * @author gai + */ +public class HidBridge { + private Context _context; + private int _productId; + private int _vendorId; + + // Can be used for debugging. + @SuppressWarnings("unused") + private static final String ACTION_USB_PERMISSION = + "org.cagnulen.qdomyoszwift.app.testhid.USB_PERMISSION"; + + // Locker object that is responsible for locking read/write thread. + private Object _locker = new Object(); + private Thread _readingThread = null; + private String _deviceName; + + private UsbManager _usbManager; + private UsbDevice _usbDevice; + + // The queue that contains the read data. + private Queue _receivedQueue; + + /** + * Creates a hid bridge to the dongle. Should be created once. + * @param context is the UI context of Android. + * @param productId of the device. + * @param vendorId of the device. + */ + public HidBridge(Context context, int productId, int vendorId) { + _context = context; + _productId = productId; + _vendorId = vendorId; + _receivedQueue = new LinkedList(); + } + + /** + * Searches for the device and opens it if successful + * @return true, if connection was successful + */ + public boolean OpenDevice() { + _usbManager = (UsbManager) _context.getSystemService(Context.USB_SERVICE); + + HashMap deviceList = _usbManager.getDeviceList(); + + Iterator deviceIterator = deviceList.values().iterator(); + _usbDevice = null; + + // Iterate all the available devices and find ours. + while(deviceIterator.hasNext()){ + UsbDevice device = deviceIterator.next(); + if (device.getProductId() == _productId && device.getVendorId() == _vendorId) { + _usbDevice = device; + _deviceName = _usbDevice.getDeviceName(); + } + } + + if (_usbDevice == null) { + Log("Cannot find the device. Did you forgot to plug it?"); + Log(String.format("\t I search for VendorId: %s and ProductId: %s", _vendorId, _productId)); + return false; + } + + // Create and intent and request a permission. + int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0; + PendingIntent mPermissionIntent = PendingIntent.getBroadcast(_context, 0, new Intent(ACTION_USB_PERMISSION), flags); + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + _context.registerReceiver(mUsbReceiver, filter); + + _usbManager.requestPermission(_usbDevice, mPermissionIntent); + Log("Found the device"); + return true; + } + + /** + * Closes the reading thread of the device. + */ + public void CloseTheDevice() { + StopReadingThread(); + } + + /** + * Starts the thread that continuously reads the data from the device. + * Should be called in order to be able to talk with the device. + */ + public void StartReadingThread() { + if (_readingThread == null) { + _readingThread = new Thread(readerReceiver); + _readingThread.start(); + } else { + Log("Reading thread already started"); + } + } + + /** + * Stops the thread that continuously reads the data from the device. + * If it is stopped - talking to the device would be impossible. + */ + @SuppressWarnings("deprecation") + public void StopReadingThread() { + if (_readingThread != null) { + // Just kill the thread. It is better to do that fast if we need that asap. + _readingThread.stop(); + _readingThread = null; + } else { + Log("No reading thread to stop"); + } + } + + /** + * Write data to the usb hid. Data is written as-is, so calling method is responsible for adding header data. + * @param bytes is the data to be written. + * @return true if succeed. + */ + public boolean WriteData(byte[] bytes) { + try + { + // Lock that is common for read/write methods. + synchronized (_locker) { + UsbInterface writeIntf = _usbDevice.getInterface(0); + UsbEndpoint writeEp = writeIntf.getEndpoint(1); + UsbDeviceConnection writeConnection = _usbManager.openDevice(_usbDevice); + + // Lock the usb interface. + writeConnection.claimInterface(writeIntf, true); + + // Write the data as a bulk transfer with defined data length. + int r = writeConnection.bulkTransfer(writeEp, bytes, bytes.length, 0); + if (r != -1) { + Log(String.format("Written %s bytes to the dongle. Data written: %s", r, composeString(bytes))); + } else { + Log("Error happened while writing data. No ACK"); + } + + // Release the usb interface. + writeConnection.releaseInterface(writeIntf); + writeConnection.close(); + } + + } catch(NullPointerException e) + { + Log("Error happened while writing. Could not connect to the device or interface is busy?"); + Log.e("HidBridge", Log.getStackTraceString(e)); + return false; + } + return true; + } + + /** + * @return true if there are any data in the queue to be read. + */ + public boolean IsThereAnyReceivedData() { + synchronized(_locker) { + return !_receivedQueue.isEmpty(); + } + } + + /** + * Queue the data from the read queue. + * @return queued data. + */ + public byte[] GetReceivedDataFromQueue() { + synchronized(_locker) { + return _receivedQueue.poll(); + } + } + + // The thread that continuously receives data from the dongle and put it to the queue. + private Runnable readerReceiver = new Runnable() { + public void run() { + if (_usbDevice == null) { + Log("No device to read from"); + return; + } + + UsbEndpoint readEp; + UsbDeviceConnection readConnection = null; + UsbInterface readIntf = null; + boolean readerStartedMsgWasShown = false; + + // We will continuously ask for the data from the device and store it in the queue. + while (true) { + // Lock that is common for read/write methods. + synchronized (_locker) { + try + { + if (_usbDevice == null) { + OpenDevice(); + Log("No device. Recheking in 10 sec..."); + + Sleep(10000); + continue; + } + + readIntf = _usbDevice.getInterface(0); + readEp = readIntf.getEndpoint(0); + if (!_usbManager.getDeviceList().containsKey(_deviceName)) { + Log("Failed to connect to the device. Retrying to acquire it."); + OpenDevice(); + if (!_usbManager.getDeviceList().containsKey(_deviceName)) { + Log("No device. Recheking in 10 sec..."); + + Sleep(10000); + continue; + } + } + + try + { + + readConnection = _usbManager.openDevice(_usbDevice); + + if (readConnection == null) { + Log("Cannot start reader because the user didn't gave me permissions or the device is not present. Retrying in 2 sec..."); + Sleep(2000); + continue; + } + + // Claim and lock the interface in the android system. + readConnection.claimInterface(readIntf, true); + } + catch (SecurityException e) { + Log("Cannot start reader because the user didn't gave me permissions. Retrying in 2 sec..."); + + Sleep(2000); + continue; + } + + // Show the reader started message once. + if (!readerStartedMsgWasShown) { + Log("!!! Reader was started !!!"); + readerStartedMsgWasShown = true; + } + + // Read the data as a bulk transfer with the size = MaxPacketSize + int packetSize = readEp.getMaxPacketSize(); + byte[] bytes = new byte[packetSize]; + int r = readConnection.bulkTransfer(readEp, bytes, packetSize, 50); + if (r >= 0) { + byte[] trancatedBytes = new byte[r]; // Truncate bytes in the honor of r + + int i=0; + for (byte b : bytes) { + if(i < r) { + trancatedBytes[i] = b; + } else { + Log(String.format("Buffer overflow %s %s %s", r, i, packetSize)); + } + i++; + } + + _receivedQueue.add(trancatedBytes); // Store received data + Log(String.format("Message received of lengths %s and content: %s", r, composeString(bytes))); + } + + // Release the interface lock. + readConnection.releaseInterface(readIntf); + readConnection.close(); + } + + catch (NullPointerException e) { + Log("Error happened while reading. No device or the connection is busy"); + Log.e("HidBridge", Log.getStackTraceString(e)); + } + catch (ThreadDeath e) { + if (readConnection != null) { + readConnection.releaseInterface(readIntf); + readConnection.close(); + } + + throw e; + } + } + + // Sleep for 10 ms to pause, so other thread can write data or anything. + // As both read and write data methods lock each other - they cannot be run in parallel. + // Looks like Android is not so smart in planning the threads, so we need to give it a small time + // to switch the thread context. + Sleep(10); + } + } + }; + + private void Sleep(int milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + synchronized (this) { + UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + if(device != null){ + //call method to set up device communication + } + } + else { + Log.d("TAG", "permission denied for the device " + device); + } + } + } + } + }; + + /** + * Logs the message from HidBridge. + * @param message to log. + */ + private void Log(String message) { + Log.d("HidBridge", message); + } + + /** + * Composes a string from byte array. + */ + private String composeString(byte[] bytes) { + StringBuilder builder = new StringBuilder(); + for (byte b: bytes) { + builder.append(b); + builder.append(" "); + } + + return builder.toString(); + } +} diff --git a/src/android/src/IQMessageReceiverWrapper.java b/src/android/src/IQMessageReceiverWrapper.java new file mode 100644 index 000000000..86e8801b1 --- /dev/null +++ b/src/android/src/IQMessageReceiverWrapper.java @@ -0,0 +1,53 @@ +package org.cagnulen.qdomyoszwift; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.garmin.android.connectiq.IQDevice; + +import java.nio.BufferUnderflowException; + +import android.util.Log; + +public class IQMessageReceiverWrapper extends BroadcastReceiver { + private final BroadcastReceiver receiver; + private static String TAG = "IQMessageReceiverWrapper: "; + + public IQMessageReceiverWrapper(BroadcastReceiver receiver) { + this.receiver = receiver; + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive intent " + intent.getAction()); + if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) { + replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE"); + } else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) { + replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE"); + } else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) { + replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE"); + } + + try { + receiver.onReceive(context, intent); + } catch (IllegalArgumentException | BufferUnderflowException e) { + Log.d(TAG, e.toString()); + } + } + + private static void replaceIQDeviceById(Intent intent, String extraName) { + try { + IQDevice device = intent.getParcelableExtra(extraName); + if (device != null) { +// Logger.logDebug("Replacing " + device.describeContents() + " " + device.getFriendlyName() + " by " + device.getDeviceIdentifier() ); + intent.putExtra(extraName, device.getDeviceIdentifier()); + } + } catch (ClassCastException e) { + Log.d(TAG, e.toString()); + // It's already a long, i.e. on the simulator. + } + } + + +} diff --git a/src/android/src/PowerChannelController.java b/src/android/src/PowerChannelController.java index 4b3ad0c48..ba898f52f 100644 --- a/src/android/src/PowerChannelController.java +++ b/src/android/src/PowerChannelController.java @@ -30,6 +30,11 @@ import com.dsi.ant.message.fromant.MessageFromAntType; import com.dsi.ant.message.ipc.AntMessageParcel; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; + import java.util.Random; public class PowerChannelController { @@ -88,6 +93,7 @@ boolean openChannel() { mIsOpen = true; Log.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID); + } catch (RemoteException e) { channelError(e); } catch (AntCommandFailedException e) { @@ -164,6 +170,7 @@ public class ChannelEventCallback implements IAntChannelEventHandler { int cnt = 0; int eventCount = 0; int cumulativePower = 0; + Timer carousalTimer = null; @Override public void onChannelDeath() { @@ -177,6 +184,36 @@ public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel an Log.d(TAG, "Message Type: " + messageType); byte[] payload = new byte[8]; + if(carousalTimer == null) { + carousalTimer = new Timer(); // At this line a new Thread will be created + carousalTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + Log.d(TAG, "Tx Unsollicited"); + byte[] payload = new byte[8]; + eventCount = (eventCount + 1) & 0xFF; + cumulativePower = (cumulativePower + power) & 0xFFFF; + payload[0] = (byte) 0x10; + payload[1] = (byte) eventCount; + payload[2] = (byte) 0xFF; + payload[3] = (byte) cadence; + payload[4] = (byte) ((cumulativePower) & 0xFF); + payload[5] = (byte) ((cumulativePower >> 8) & 0xFF); + payload[6] = (byte) ((power) & 0xFF); + payload[7] = (byte) ((power >> 8) & 0xFF); + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + } + }, 0, 1000); // delay + } + // Switching on message type to handle different types of messages switch (messageType) { // If data message, construct from parcel and update channel data @@ -205,6 +242,26 @@ public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel an } catch (RemoteException e) { channelError(e); } + } else { + eventCount = (eventCount + 1) & 0xFF; + cumulativePower = (cumulativePower + power) & 0xFFFF; + payload[0] = (byte) 0x10; + payload[1] = (byte) eventCount; + payload[2] = (byte) 0xFF; + payload[3] = (byte) cadence; + payload[4] = (byte) ((cumulativePower) & 0xFF); + payload[5] = (byte) ((cumulativePower >> 8) & 0xFF); + payload[6] = (byte) ((power) & 0xFF); + payload[7] = (byte) ((power >> 8) & 0xFF); + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } } break; case CHANNEL_EVENT: diff --git a/src/android/src/SDMChannelController.java b/src/android/src/SDMChannelController.java new file mode 100644 index 000000000..6507061da --- /dev/null +++ b/src/android/src/SDMChannelController.java @@ -0,0 +1,308 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.cagnulen.qdomyoszwift; + +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import com.dsi.ant.channel.AntChannel; +import com.dsi.ant.channel.AntCommandFailedException; +import com.dsi.ant.channel.IAntChannelEventHandler; +import com.dsi.ant.message.ChannelId; +import com.dsi.ant.message.ChannelType; +import com.dsi.ant.message.EventCode; +import com.dsi.ant.message.fromant.ChannelEventMessage; +import com.dsi.ant.message.fromant.MessageFromAntType; +import com.dsi.ant.message.ipc.AntMessageParcel; + +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; + + +import java.util.Random; + +public class SDMChannelController { + // The device type and transmission type to be part of the channel ID message + private static final int CHANNEL_SPEED_DEVICE_TYPE = 0x7C; + private static final int CHANNEL_SPEED_TRANSMISSION_TYPE = 1; + + // The period and frequency values the channel will be configured to + private static final int CHANNEL_SPEED_PERIOD = 8134; // 1 Hz + private static final int CHANNEL_SPEED_FREQUENCY = 57; + + private static final String TAG = SDMChannelController.class.getSimpleName(); + public static final int SPEED_SENSOR_ID = 0x9e3d4b99; + + private static final double MILLISECOND_TO_1_1024_CONVERSION = 0.9765625; + + private AntChannel mAntChannel; + + private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback(); + + private boolean mIsOpen; + double speed = 0.0; + int cadence = 0; + byte stride_count = 0; + + public SDMChannelController(AntChannel antChannel) { + mAntChannel = antChannel; + openChannel(); + } + + boolean openChannel() { + if (null != mAntChannel) { + if (mIsOpen) { + Log.w(TAG, "Channel was already open"); + } else { + // Channel ID message contains device number, type and transmission type. In + // order for master (TX) channels and slave (RX) channels to connect, they + // must have the same channel ID, or wildcard (0) is used. + ChannelId channelId = new ChannelId(SPEED_SENSOR_ID & 0xFFFF, + CHANNEL_SPEED_DEVICE_TYPE, CHANNEL_SPEED_TRANSMISSION_TYPE); + + try { + // Setting the channel event handler so that we can receive messages from ANT + mAntChannel.setChannelEventHandler(mChannelEventCallback); + + // Performs channel assignment by assigning the type to the channel. Additional + // features (such as, background scanning and frequency agility) can be enabled + // by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment). + mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER); + + /* + * Configures the channel ID, messaging period and rf frequency after assigning, + * then opening the channel. + * + * For any additional ANT features such as proximity search or background scanning, refer to + * the ANT Protocol Doc found at: + * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ + */ + mAntChannel.setChannelId(channelId); + mAntChannel.setPeriod(CHANNEL_SPEED_PERIOD); + mAntChannel.setRfFrequency(CHANNEL_SPEED_FREQUENCY); + mAntChannel.open(); + mIsOpen = true; + + Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID); + } catch (RemoteException e) { + channelError(e); + } catch (AntCommandFailedException e) { + // This will release, and therefore unassign if required + channelError("Open failed", e); + } + } + } else { + Log.w(TAG, "No channel available"); + } + + return mIsOpen; + } + + void channelError(RemoteException e) { + String logString = "Remote service communication failed."; + + Log.e(TAG, logString); + } + + void channelError(String error, AntCommandFailedException e) { + StringBuilder logString; + + if (e.getResponseMessage() != null) { + String initiatingMessageId = "0x" + Integer.toHexString( + e.getResponseMessage().getInitiatingMessageId()); + String rawResponseCode = "0x" + Integer.toHexString( + e.getResponseMessage().getRawResponseCode()); + + logString = new StringBuilder(error) + .append(". Command ") + .append(initiatingMessageId) + .append(" failed with code ") + .append(rawResponseCode); + } else { + String attemptedMessageId = "0x" + Integer.toHexString( + e.getAttemptedMessageType().getMessageId()); + String failureReason = e.getFailureReason().toString(); + + logString = new StringBuilder(error) + .append(". Command ") + .append(attemptedMessageId) + .append(" failed with reason ") + .append(failureReason); + } + + Log.e(TAG, logString.toString()); + + mAntChannel.release(); + + Log.e(TAG, "ANT Command Failed"); + } + + public void close() { + // TODO kill all our resources + if (null != mAntChannel) { + mIsOpen = false; + + // Releasing the channel to make it available for others. + // After releasing, the AntChannel instance cannot be reused. + mAntChannel.release(); + mAntChannel = null; + } + + Log.e(TAG, "Channel Closed"); + } + + /** + * Implements the Channel Event Handler Interface so that messages can be + * received and channel death events can be handled. + */ + public class ChannelEventCallback implements IAntChannelEventHandler { + long lastTime = 0; + double totalWay = 0.0; + double totalRotations = 0.0; + long lastSpeedEventTime = 0; + long lastCadenceEventTime = 0; + long elapsedMillis = 0; + int rotations; + int rev; + double wheel = 0.1; + Timer carousalTimer = null; + + @Override + public void onChannelDeath() { + // Display channel death message when channel dies + Log.e(TAG, "Channel Death"); + } + + @Override + public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) { + Log.d(TAG, "Rx: " + antParcel); + Log.d(TAG, "Message Type: " + messageType); + + if(carousalTimer == null) { + carousalTimer = new Timer(); // At this line a new Thread will be created + carousalTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + Log.d(TAG, "Tx Unsollicited"); + long realtimeMillis = SystemClock.elapsedRealtime(); + double speedM_s = speed / 3.6; + long deltaTime = (realtimeMillis - lastTime); + lastTime = realtimeMillis; + + byte[] payload = new byte[8]; + + payload[0] = (byte) 0x01; + payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF); + payload[2] = (byte) ((lastTime % 256000) / 1000); + payload[3] = (byte) 0x00; + payload[4] = (byte) speedM_s; + payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0)); + payload[6] = (byte) stride_count++; // bad but it works on zwift + payload[7] = (byte) ((double)deltaTime * 0.03125); + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + } + }, 0, 250); // delay + } + + // Switching on message type to handle different types of messages + switch (messageType) { + // If data message, construct from parcel and update channel data + case BROADCAST_DATA: + // Rx Data + //updateData(new BroadcastDataMessage(antParcel).getPayload()); + break; + case ACKNOWLEDGED_DATA: + // Rx Data + //updateData(new AcknowledgedDataMessage(antParcel).getPayload()); + break; + case CHANNEL_EVENT: + // Constructing channel event message from parcel + ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel); + EventCode code = eventMessage.getEventCode(); + Log.d(TAG, "Event Code: " + code); + + // Switching on event code to handle the different types of channel events + switch (code) { + case TX: + long realtimeMillis = SystemClock.elapsedRealtime(); + double speedM_s = speed / 3.6; + long deltaTime = (realtimeMillis - lastTime); + // in case the treadmill doesn't provide cadence, I have to force it. ANT+ requires cadence + lastTime = realtimeMillis; + + byte[] payload = new byte[8]; + + payload[0] = (byte) 0x01; + payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF); + payload[2] = (byte) ((lastTime % 256000) / 1000); + payload[3] = (byte) 0x00; + payload[4] = (byte) speedM_s; + payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0)); + payload[6] = (byte) stride_count; + payload[7] = (byte) ((double)deltaTime * 0.03125); + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case CHANNEL_COLLISION: + break; + case RX_SEARCH_TIMEOUT: + // TODO May want to keep searching + Log.e(TAG, "No Device Found"); + break; + case CHANNEL_CLOSED: + case RX_FAIL: + case RX_FAIL_GO_TO_SEARCH: + case TRANSFER_RX_FAILED: + case TRANSFER_TX_COMPLETED: + case TRANSFER_TX_FAILED: + case TRANSFER_TX_START: + case UNKNOWN: + // TODO More complex communication will need to handle these events + break; + } + break; + case ANT_VERSION: + case BURST_TRANSFER_DATA: + case CAPABILITIES: + case CHANNEL_ID: + case CHANNEL_RESPONSE: + case CHANNEL_STATUS: + case SERIAL_NUMBER: + case OTHER: + // TODO More complex communication will need to handle these message types + break; + } + } + } +} diff --git a/src/android/src/ScreenCaptureService.java b/src/android/src/ScreenCaptureService.java index 3d9fde212..f2f94a4a6 100644 --- a/src/android/src/ScreenCaptureService.java +++ b/src/android/src/ScreenCaptureService.java @@ -65,18 +65,33 @@ public class ScreenCaptureService extends Service { private int mDensity; private int mWidth; private int mHeight; + private static int mWidthImage; + private static int mHeightImage; private int mRotation; private OrientationChangeCallback mOrientationChangeCallback; private TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS); private static String lastText = ""; + private static String lastTextExtended = ""; private static boolean isRunning = false; public static String getLastText() { return lastText; } + public static String getLastTextExtended() { + return lastTextExtended; + } + + public static int getImageWidth() { + return mWidthImage; + } + + public static int getImageHeight() { + return mHeightImage; + } + public static Intent getStartIntent(Context context, int resultCode, Intent data) { Intent intent = new Intent(context, ScreenCaptureService.class); intent.putExtra(ACTION, START); @@ -122,6 +137,8 @@ public void onImageAvailable(ImageReader reader) { isRunning = true; // create bitmap + mWidthImage = mWidth + rowPadding / pixelStride; + mHeightImage = mHeight; final Bitmap bitmap = Bitmap.createBitmap(mWidth + rowPadding / pixelStride, mHeight, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(buffer); /* @@ -151,12 +168,13 @@ public void onSuccess(Text result) { String resultText = result.getText(); lastText = resultText; - /* + lastTextExtended = ""; for (Text.TextBlock block : result.getTextBlocks()) { String blockText = block.getText(); Point[] blockCornerPoints = block.getCornerPoints(); Rect blockFrame = block.getBoundingBox(); - for (Text.Line line : block.getLines()) { + lastTextExtended = lastTextExtended + blockText + "$$" + blockFrame.toString() + "§§"; + /*for (Text.Line line : block.getLines()) { String lineText = line.getText(); Point[] lineCornerPoints = line.getCornerPoints(); Rect lineFrame = line.getBoundingBox(); @@ -170,8 +188,8 @@ public void onSuccess(Text result) { Rect symbolFrame = symbol.getBoundingBox(); } } - } - }*/ + }*/ + } bitmap.recycle(); isRunning = false; } diff --git a/src/android/src/SpeedChannelController.java b/src/android/src/SpeedChannelController.java index f602fb185..9c2a060cd 100644 --- a/src/android/src/SpeedChannelController.java +++ b/src/android/src/SpeedChannelController.java @@ -29,6 +29,12 @@ import com.dsi.ant.message.fromant.MessageFromAntType; import com.dsi.ant.message.ipc.AntMessageParcel; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; + + import java.util.Random; public class SpeedChannelController { @@ -174,6 +180,7 @@ public class ChannelEventCallback implements IAntChannelEventHandler { int rotations; int rev; double wheel = 0.1; + Timer carousalTimer = null; @Override public void onChannelDeath() { @@ -186,6 +193,50 @@ public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel an Log.d(TAG, "Rx: " + antParcel); Log.d(TAG, "Message Type: " + messageType); + if(carousalTimer == null) { + carousalTimer = new Timer(); // At this line a new Thread will be created + carousalTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + Log.d(TAG, "Tx Unsollicited"); + long realtimeMillis = SystemClock.elapsedRealtime(); + + if (lastTime != 0) { + elapsedMillis = realtimeMillis - lastTime; + totalWay += speed * elapsedMillis / 3_600L; + totalRotations += (double) cadence * elapsedMillis / 60_000L; + rev = (int) (totalWay / wheel); + rotations = (int) totalRotations; + lastCadenceEventTime = realtimeMillis - (long) ((totalRotations - rotations) / cadence * 60_000); + lastSpeedEventTime = realtimeMillis - (long) ((totalWay - (rev * wheel)) / speed * 3_600); + } + lastTime = realtimeMillis; + + byte[] payload = new byte[8]; + + int lastCadenceEventTime1024 = (int) ((double) lastCadenceEventTime / MILLISECOND_TO_1_1024_CONVERSION); + int lastSpeedEventTime1024 = (int) ((double) lastSpeedEventTime / MILLISECOND_TO_1_1024_CONVERSION); + payload[0] = (byte) (lastCadenceEventTime1024 & 0xFF); + payload[1] = (byte) ((lastCadenceEventTime1024 >> 8) & 0xFF); + payload[2] = (byte) (rotations & 0xFF); + payload[3] = (byte) ((rotations >> 8) & 0xFF); + payload[4] = (byte) (lastSpeedEventTime1024 & 0xFF); + payload[5] = (byte) ((lastSpeedEventTime1024 >> 8) & 0xFF); + payload[6] = (byte) (rev & 0xFF); + payload[7] = (byte) ((rev >> 8) & 0xFF); + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + } + }, 0, 500); // delay + } + // Switching on message type to handle different types of messages switch (messageType) { // If data message, construct from parcel and update channel data diff --git a/src/android/src/WearableController.java b/src/android/src/WearableController.java new file mode 100644 index 000000000..3ce3561c3 --- /dev/null +++ b/src/android/src/WearableController.java @@ -0,0 +1,42 @@ +package org.cagnulen.qdomyoszwift; + +import android.app.ActivityManager; +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; +import android.os.Looper; +import android.os.Handler; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +public class WearableController { + static Context _context; + static Intent _intent = null; + + public static void start(Context context) { + _context = context; + + if(_intent == null) + _intent = new Intent(context, WearableMessageListenerService.class); + // FloatingWindowGFG service is started + context.startService(_intent); + Log.v("WearableController", "started"); + } + + public static int getHeart() { + return WearableMessageListenerService.getHeart(); + } +} diff --git a/src/android/src/WearableMessageListenerService.java b/src/android/src/WearableMessageListenerService.java new file mode 100644 index 000000000..94ed6a51b --- /dev/null +++ b/src/android/src/WearableMessageListenerService.java @@ -0,0 +1,131 @@ +package org.cagnulen.qdomyoszwift; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.wearable.MessageClient; +import com.google.android.gms.wearable.DataClient; +import com.google.android.gms.wearable.DataEvent; +import com.google.android.gms.wearable.DataEventBuffer; +import com.google.android.gms.wearable.MessageEvent; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.wearable.DataItemBuffer; +import com.google.android.gms.wearable.DataMap; +import android.util.Log; +import android.os.Bundle; +import com.google.android.gms.common.api.Status; +import java.io.InputStream; + +public class WearableMessageListenerService extends Service implements + MessageClient.OnMessageReceivedListener, GoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener,DataClient.OnDataChangedListener { + + private GoogleApiClient googleApiClient; + private MessageClient mWearableClient; + private String TAG = "WearableMessageListenerService"; + private static int heart_rate = 0; + + @Override + public void onCreate() { + super.onCreate(); + Log.v("WearableMessageListenerService","onCreate"); + } + + public static int getHeart() { + return heart_rate; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // Your service logic here + + googleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks (this) + .addOnConnectionFailedListener(this) + .build(); + + googleApiClient.connect(); + + // Register the MessageClient.OnMessageReceivedListener + mWearableClient = Wearable.getMessageClient(this); + mWearableClient.addListener(this); + Wearable.getDataClient(this).addListener(this); + + Log.v("WearableMessageListenerService","onStartCommand"); + + // Return START_STICKY to restart the service if it's killed by the system + return START_STICKY; + } + + @Override + public void onDataChanged(DataEventBuffer dataEvents) { + for (DataEvent event : dataEvents) { + if (event.getType() == DataEvent.TYPE_DELETED) { + Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri()); + } else if (event.getType() == DataEvent.TYPE_CHANGED) { + Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath()); + if(event.getDataItem().getUri().getPath().equals("/qz")) { + new Thread(new Runnable() { + @Override + public void run() { + DataItemBuffer result = Wearable.DataApi.getDataItems(googleApiClient).await(); + if (result.getStatus().isSuccess()) { + if (result.getCount() == 1) { + heart_rate = DataMap.fromByteArray(result.get(0).getData()) + .getInt("heart_rate", 0); + } else { + Log.e(TAG, "Unexpected number of DataItems found.\n" + + "\tExpected: 1\n" + + "\tActual: " + result.getCount()); + } + } else if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onHandleIntent: failed to get current alarm state"); + } + Log.d(TAG, "Heart: " + heart_rate); + } + }).start(); + } + } + } + } + + + @Override + public void onConnected(Bundle bundle) { + Log.v("WearableMessageListenerService","onConnected"); + } + + @Override + public void onConnectionSuspended(int i) { + Log.v("WearableMessageListenerService","onConnectionSuspended"); + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + Log.v("WearableMessageListenerService","onConnectionFailed"); + } + + @Override + public void onMessageReceived(final MessageEvent messageEvent) { + String path = messageEvent.getPath(); + byte[] data = messageEvent.getData(); + + // Handle the received message data here + String messageData = new String(data); // Assuming it's a simple string message + + Log.v("Wearable", path); + Log.v("Wearable", messageData); + + // You can then perform actions or update data in your service based on the received message + } + + @Override + public IBinder onBind(Intent intent) { + // This service does not support binding + return null; + } +} diff --git a/src/android/src/com/cgutman/adblib/AdbConnection.java b/src/android/src/com/cgutman/adblib/AdbConnection.java index bbdc2f51a..fa77906fe 100644 --- a/src/android/src/com/cgutman/adblib/AdbConnection.java +++ b/src/android/src/com/cgutman/adblib/AdbConnection.java @@ -87,7 +87,7 @@ private AdbConnection() /** * Creates a AdbConnection object associated with the socket and * crypto object specified. - * @param socket The socket that the connection will use for communcation. + * @param socket The socket that the connection will use for communication. * @param crypto The crypto object that stores the key pair for authentication. * @return A new AdbConnection object. * @throws IOException If there is a socket error diff --git a/src/android/src/com/cgutman/adblib/AdbProtocol.java b/src/android/src/com/cgutman/adblib/AdbProtocol.java index 02741ad21..6d41257f8 100644 --- a/src/android/src/com/cgutman/adblib/AdbProtocol.java +++ b/src/android/src/com/cgutman/adblib/AdbProtocol.java @@ -58,7 +58,7 @@ public class AdbProtocol { * processed successfully. */ public static final int CMD_OKAY = 0x59414b4f; - /** CLSE is the close stream message. It it sent to close an + /** CLSE is the close stream message. It is sent to close an * existing stream on the target device. */ public static final int CMD_CLSE = 0x45534c43; diff --git a/src/android/src/com/cgutman/androidremotedebugger/service/ShellService.java b/src/android/src/com/cgutman/androidremotedebugger/service/ShellService.java index f7ec86ac8..dd31d4ace 100644 --- a/src/android/src/com/cgutman/androidremotedebugger/service/ShellService.java +++ b/src/android/src/com/cgutman/androidremotedebugger/service/ShellService.java @@ -87,7 +87,7 @@ public IBinder onBind(Intent arg0) { @Override public boolean onUnbind(Intent intent) { - /* Stop the the service if no connections remain */ + /* Stop the service if no connections remain */ if (currentConnectionMap.isEmpty()) { stopSelf(); } @@ -245,7 +245,7 @@ private synchronized void addNewConnection(DeviceConnection devConn) { private synchronized void removeConnection(DeviceConnection devConn) { currentConnectionMap.remove(getConnectionString(devConn)); - /* Stop the the service if no connections remain */ + /* Stop the service if no connections remain */ if (currentConnectionMap.isEmpty()) { stopSelf(); } diff --git a/src/androidadblog.h b/src/androidadblog.h index 0f95da6bf..b6973f516 100644 --- a/src/androidadblog.h +++ b/src/androidadblog.h @@ -12,7 +12,7 @@ class androidadblog : public QThread public: explicit androidadblog(); - void run(); + void run() override; private: void runAdbTailCommand(QString command); diff --git a/src/apexbike.cpp b/src/apexbike.cpp index b381356ac..98a753a9d 100644 --- a/src/apexbike.cpp +++ b/src/apexbike.cpp @@ -52,11 +52,15 @@ void apexbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -182,16 +186,7 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } @@ -267,7 +262,7 @@ void apexbike::stateChanged(QLowEnergyService::ServiceState state) { &apexbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -292,10 +287,12 @@ void apexbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&apexbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &apexbike::changeInclination); + + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -389,36 +386,7 @@ bool apexbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *apexbike::VirtualBike() { return virtualBike; } - -void *apexbike::VirtualDevice() { return VirtualBike(); } - -uint16_t apexbike::watts() { - QSettings settings; - double watt; - if (currentCadence().value() == 0) { - return 0; - } - if (Heart.value() > 0) { - int avgP = ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() * - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()) - - (settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble() * - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble())) / - (settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble() - - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble()) + - (Heart.value() * - ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() - - settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble()) / - (settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble() - - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()))); - if (Speed.value() > 0) { - watt = avgP; - } else { - watt = 0; - } - } - return watt; -} +uint16_t apexbike::watts() { return wattFromHR(true); } void apexbike::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; diff --git a/src/apexbike.h b/src/apexbike.h index 4bb736b6f..c553e5dc2 100644 --- a/src/apexbike.h +++ b/src/apexbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,10 +36,7 @@ class apexbike : public bike { Q_OBJECT public: apexbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: const resistance_t max_resistance = 32; @@ -50,10 +46,9 @@ class apexbike : public bike { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/bhfitnesselliptical.cpp b/src/bhfitnesselliptical.cpp index a4ecfff2d..69775ca87 100644 --- a/src/bhfitnesselliptical.cpp +++ b/src/bhfitnesselliptical.cpp @@ -1,6 +1,5 @@ #include "bhfitnesselliptical.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualtreadmill.h" #include #include @@ -10,9 +9,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -46,10 +46,15 @@ void bhfitnesselliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, c timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -98,6 +103,7 @@ void bhfitnesselliptical::update() { } if (requestResistance != currentResistance().value()) { + virtualbike *virtualBike = dynamic_cast(this->VirtualDevice()); if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) { emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); forceResistance(requestResistance); @@ -324,16 +330,7 @@ void bhfitnesselliptical::characteristicChanged(const QLowEnergyCharacteristic & if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && (!Flags.heartRate || Heart.value() == 0 || disable_hr_frommachinery)) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS @@ -438,7 +435,7 @@ void bhfitnesselliptical::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -446,7 +443,7 @@ void bhfitnesselliptical::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - if (!virtualTreadmill && !virtualBike) { + if (!this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -455,19 +452,22 @@ void bhfitnesselliptical::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &bhfitnesselliptical::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &bhfitnesselliptical::changeInclinationRequested); + + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &bhfitnesselliptical::changeInclinationRequested); connect(virtualBike, &virtualbike::changeInclination, this, &bhfitnesselliptical::changeInclination); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &bhfitnesselliptical::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } } } @@ -494,7 +494,12 @@ void bhfitnesselliptical::ftmsCharacteristicChanged(const QLowEnergyCharacterist Resistance = (slope / 33) + default_resistance; } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, b); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray(b); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); } } @@ -599,10 +604,6 @@ bool bhfitnesselliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *bhfitnesselliptical::VirtualTreadmill() { return virtualTreadmill; } - -void *bhfitnesselliptical::VirtualDevice() { return VirtualTreadmill(); } - uint16_t bhfitnesselliptical::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/bhfitnesselliptical.h b/src/bhfitnesselliptical.h index 63287bf39..0cfcea4b2 100644 --- a/src/bhfitnesselliptical.h +++ b/src/bhfitnesselliptical.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,10 +37,7 @@ class bhfitnesselliptical : public elliptical { public: bhfitnesselliptical(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, @@ -52,9 +47,6 @@ class bhfitnesselliptical : public elliptical { void forceResistance(resistance_t requestResistance); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; - QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; QLowEnergyService *gattFTMSService = nullptr; diff --git a/src/bike.cpp b/src/bike.cpp index e75d1d3f9..4c7a85c13 100644 --- a/src/bike.cpp +++ b/src/bike.cpp @@ -5,10 +5,25 @@ bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); } +virtualbike *bike::VirtualBike() { return dynamic_cast(this->VirtualDevice()); } + void bike::changeResistance(resistance_t resistance) { + QSettings settings; + double zwift_erg_resistance_up = + settings.value(QZSettings::zwift_erg_resistance_up, QZSettings::default_zwift_erg_resistance_up).toDouble(); + double zwift_erg_resistance_down = + settings.value(QZSettings::zwift_erg_resistance_down, QZSettings::default_zwift_erg_resistance_down).toDouble(); + lastRawRequestedResistanceValue = resistance; if (autoResistanceEnable) { double v = (resistance * m_difficult) + gears(); + if ((double)v > zwift_erg_resistance_up) { + qDebug() << "zwift_erg_resistance_up filter enabled!"; + v = (resistance_t)zwift_erg_resistance_up; + } else if ((double)v < zwift_erg_resistance_down) { + qDebug() << "zwift_erg_resistance_down filter enabled!"; + v = (resistance_t)zwift_erg_resistance_down; + } requestResistance = v; emit resistanceChanged(requestResistance); } @@ -36,16 +51,24 @@ void bike::changeRequestedPelotonResistance(int8_t resistance) { RequestedPeloto void bike::changeCadence(int16_t cadence) { RequestedCadence = cadence; } void bike::changePower(int32_t power) { - RequestedPower = power; + RequestedPower = power; // in order to paint in any case the request power on the charts + + if (!autoResistanceEnable) { + qDebug() << QStringLiteral("changePower ignored because auto resistance is disabled"); + return; + } + requestPower = power; // used by some bikes that have ERG mode builtin QSettings settings; - bool force_resistance = settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance).toBool(); - // bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); //Not used anywhere in code - double erg_filter_upper = settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble(); - double erg_filter_lower = settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble(); - double zwift_erg_resistance_up = settings.value(QZSettings::zwift_erg_resistance_up, QZSettings::default_zwift_erg_resistance_up).toDouble(); - double zwift_erg_resistance_down = settings.value(QZSettings::zwift_erg_resistance_down, QZSettings::default_zwift_erg_resistance_down).toDouble(); - + bool force_resistance = + settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance) + .toBool(); + // bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); //Not used + // anywhere in code + double erg_filter_upper = + settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble(); + double erg_filter_lower = + settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble(); double deltaDown = wattsMetric().value() - ((double)power); double deltaUp = ((double)power) - wattsMetric().value(); qDebug() << QStringLiteral("filter ") + QString::number(deltaUp) + " " + QString::number(deltaDown) + " " + @@ -53,19 +76,12 @@ void bike::changePower(int32_t power) { if (!ergModeSupported && force_resistance /*&& erg_mode*/ && (deltaUp > erg_filter_upper || deltaDown > erg_filter_lower)) { resistance_t r = (resistance_t)resistanceFromPowerRequest(power); - if ((double)r > zwift_erg_resistance_up) { - qDebug() << "zwift_erg_resistance_up filter enabled!"; - r = (resistance_t)zwift_erg_resistance_up; - } else if ((double)r < zwift_erg_resistance_down) { - qDebug() << "zwift_erg_resistance_down filter enabled!"; - r = (resistance_t)zwift_erg_resistance_down; - } changeResistance(r); // resistance start from 1 } } -int8_t bike::gears() { return m_gears; } -void bike::setGears(int8_t gears) { +double bike::gears() { return m_gears; } +void bike::setGears(double gears) { QSettings settings; qDebug() << "setGears" << gears; m_gears = gears; @@ -238,8 +254,42 @@ uint8_t bike::metrics_override_heartrate() { return qRound(RequestedPower.value()); } else if (!setting.compare(QStringLiteral("Watt/Kg"))) { return qRound(wattKg().value()); + } else if (!setting.compare(QStringLiteral("Target Cadence"))) { + return qRound(RequestedCadence.value()); } return qRound(currentHeart().value()); } bool bike::inclinationAvailableByHardware() { return false; } + +uint16_t bike::wattFromHR(bool useSpeedAndCadence) { + QSettings settings; + double watt = 0; + if (currentCadence().value() == 0 && useSpeedAndCadence == true) { + return 0; + } + if (Heart.value() > 0) { + int avgP = ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() * + settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()) - + (settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble() * + settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble())) / + (settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble() - + settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble()) + + (Heart.value() * + ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() - + settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble()) / + (settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble() - + settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()))); + if (Speed.value() > 0 || useSpeedAndCadence == false) { + if (avgP < 50) { + avgP = 50; + } + watt = avgP; + } else { + watt = 0; + } + } else { + watt = currentCadence().value() * 1.2; // random value cloned from Zwift when HR is not available + } + return watt; +} diff --git a/src/bike.h b/src/bike.h index 099c76c7c..8c64bd53e 100644 --- a/src/bike.h +++ b/src/bike.h @@ -2,6 +2,7 @@ #define BIKE_H #include "bluetoothdevice.h" +#include "virtualbike.h" #include class bike : public bluetoothdevice { @@ -10,31 +11,33 @@ class bike : public bluetoothdevice { public: bike(); + + virtualbike *VirtualBike(); + metric lastRequestedResistance(); metric lastRequestedPelotonResistance(); metric lastRequestedCadence(); metric lastRequestedPower(); - virtual metric currentResistance(); - virtual uint8_t fanSpeed(); - virtual double currentCrankRevolutions(); - virtual uint16_t lastCrankEventTime(); - virtual bool connected(); + metric currentResistance() override; + uint8_t fanSpeed() override; + double currentCrankRevolutions() override; + uint16_t lastCrankEventTime() override; + bool connected() override; virtual uint16_t watts(); virtual resistance_t pelotonToBikeResistance(int pelotonResistance); virtual resistance_t resistanceFromPowerRequest(uint16_t power); virtual uint16_t powerFromResistanceRequest(resistance_t requestResistance); virtual bool ergManagedBySS2K() { return false; } - bluetoothdevice::BLUETOOTH_TYPE deviceType(); + bluetoothdevice::BLUETOOTH_TYPE deviceType() override; metric pelotonResistance(); - void clearStats(); - void setLap(); - void setPaused(bool p); - uint8_t metrics_override_heartrate(); - void setGears(int8_t d); - int8_t gears(); - void setSpeedLimit(double speed) {m_speedLimit = speed;} - double speedLimit() {return m_speedLimit;} - + void clearStats() override; + void setLap() override; + void setPaused(bool p) override; + uint8_t metrics_override_heartrate() override; + void setGears(double d); + double gears(); + void setSpeedLimit(double speed) { m_speedLimit = speed; } + double speedLimit() { return m_speedLimit; } /** * @brief currentSteeringAngle Gets a metric object to get or set the current steering angle @@ -43,15 +46,16 @@ class bike : public bluetoothdevice { */ metric currentSteeringAngle() { return m_steeringAngle; } virtual bool inclinationAvailableByHardware(); + bool ergModeSupportedAvailableByHardware() { return ergModeSupported; } public Q_SLOTS: - virtual void changeResistance(resistance_t res); + void changeResistance(resistance_t res) override; virtual void changeCadence(int16_t cad); - virtual void changePower(int32_t power); + void changePower(int32_t power) override; virtual void changeRequestedPelotonResistance(int8_t resistance); - virtual void cadenceSensor(uint8_t cadence); - virtual void powerSensor(uint16_t power); - virtual void changeInclination(double grade, double percentage); + void cadenceSensor(uint8_t cadence) override; + void powerSensor(uint16_t power) override; + void changeInclination(double grade, double percentage) override; virtual void changeSteeringAngle(double angle) { m_steeringAngle = angle; } virtual void resistanceFromFTMSAccessory(resistance_t res) { Q_UNUSED(res); } @@ -74,7 +78,7 @@ class bike : public bluetoothdevice { bool ergModeSupported = false; // if a bike has this mode supported, when from the virtual bike there is a power // request there is no need to translate in resistance levels - int8_t m_gears = 0; + double m_gears = 0; resistance_t lastRawRequestedResistanceValue = -1; uint16_t LastCrankEventTime = 0; double CrankRevs = 0; @@ -84,6 +88,8 @@ class bike : public bluetoothdevice { metric m_steeringAngle; double m_speedLimit = 0; + + uint16_t wattFromHR(bool useSpeedAndCadence); }; #endif // BIKE_H diff --git a/src/bkoolbike.cpp b/src/bkoolbike.cpp new file mode 100644 index 000000000..da6b4e3af --- /dev/null +++ b/src/bkoolbike.cpp @@ -0,0 +1,757 @@ +#include "bkoolbike.h" +#include "virtualbike.h" +#include +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#include +#endif + +#include + +using namespace std::chrono_literals; + +bkoolbike::bkoolbike(bool noWriteResistance, bool noHeartService) { + m_watt.setType(metric::METRIC_WATT); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + initDone = false; + connect(refresh, &QTimer::timeout, this, &bkoolbike::update); + refresh->start(200ms); +} + +void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response) { + QEventLoop loop; + QTimer timeout; + + if (!gattCustomService) { + qDebug() << QStringLiteral("ERROR: no gattCustomService!"); + return; + } + + if (wait_for_response) { + connect(gattCustomService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } else { + connect(gattCustomService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer); + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + + QStringLiteral(" // ") + info); + } + + loop.exec(); +} + +void bkoolbike::changePower(int32_t power) { + RequestedPower = power; + /* + if (power < 0) + power = 0; + uint8_t p[] = {0xa4, 0x09, 0x4e, 0x05, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x02, 0x00}; + p[10] = (uint8_t)((power * 4) & 0xFF); + p[11] = (uint8_t)((power * 4) >> 8); + for (uint8_t i = 0; i < sizeof(p) - 1; i++) { + p[12] ^= p[i]; // the last byte is a sort of a checksum + } + + writeCharacteristic(p, sizeof(p), QStringLiteral("changePower"), false, false);*/ + + qDebug() << QStringLiteral("Changepower not implemented"); +} + +void bkoolbike::forceInclination(double inclination) { + // TODO: inclination for bikes need to be managed on virtual bike interface + // Inclination = inclination; + + // this bike doesn't provide resistance, so i will put at the same value of the inclination #659 + QSettings settings; + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + if (tacx_neo2_peloton) + Resistance = inclination * 10; + else + Resistance = inclination; + + inclination += 200; + inclination = inclination * 100; + uint8_t inc[] = {0x33, 0x25, 0xff, 0xff, 0xff, 0x6d, 0x4f, 0xa0}; + inc[5] = (uint8_t)(((uint16_t)inclination) & 0xFF); + inc[6] = (uint8_t)(((uint16_t)inclination) >> 8); + + writeCharacteristic(inc, sizeof(inc), QStringLiteral("changeInclination"), false, false); +} + +void bkoolbike::update() { + if (m_control->state() == QLowEnergyController::UnconnectedState) { + emit disconnected(); + return; + } + + if (initRequest) { + initRequest = false; + QSettings settings; + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + if (tacx_neo2_peloton) + requestInclination = 0; + + uint8_t init1[] = {0x30, 0x25, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}; + uint8_t init2[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x1e, 0x7f, 0x00}; + uint8_t init3[] = {0x33, 0x25, 0xff, 0xff, 0xff, 0x20, 0x4e, 0x00}; + uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21}; + uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21}; + writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, false); + writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, false); + writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false); + writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true); + writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true); + + } else if (bluetoothDevice.isValid() && + m_control->state() == QLowEnergyController::DiscoveredState //&& + // gattCommunicationChannelService && + // gattWriteCharacteristic.isValid() && + // gattNotify1Characteristic.isValid() && + /*initDone*/) { + update_metrics(false, watts()); + + // updating the treadmill console every second + if (sec1Update++ == (500 / refresh->interval())) { + sec1Update = 0; + // updateDisplay(elapsed); + } + + if (requestResistance != -1) { + if (requestResistance != currentResistance().value() || lastGearValue != gears()) { + emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); + auto virtualBike = this->VirtualBike(); + if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && + (requestPower == 0 || requestPower == -1)) { + requestInclination = requestResistance / 10.0; + } + // forceResistance(requestResistance);; + } + lastGearValue = gears(); + requestResistance = -1; + } + if (requestInclination != -100) { + emit debug(QStringLiteral("writing inclination ") + QString::number(requestInclination)); + forceInclination(requestInclination + gears()); // since this bike doesn't have the concept of resistance, + // i'm using the gears in the inclination + requestInclination = -100; + } + + if (requestPower != -1) { + changePower(requestPower); + requestPower = -1; + } + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit bikeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + } +} + +void bkoolbike::powerPacketReceived(const QByteArray &b) { + Q_UNUSED(b) + /* + if(gattPowerService) + writeCharacteristic((uint8_t*)b.constData(), b.length(), "powerPacketReceived bridge", false, false); + else + qDebug() << "no power service found" << b; + */ +} + +void bkoolbike::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void bkoolbike::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length(); + Q_UNUSED(characteristic); + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + + qDebug() << QStringLiteral(" << char ") << characteristic.uuid(); + emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); + + if (characteristic.uuid() == QBluetoothUuid((quint16)0x2A5B)) { + lastPacket = newValue; + + uint8_t index = 1; + + if (newValue.at(0) == 0x02 && newValue.length() < 4) { + emit debug(QStringLiteral("Crank revolution data present with wrong bytes ") + + QString::number(newValue.length())); + return; + } else if (newValue.at(0) == 0x01 && newValue.length() < 6) { + emit debug(QStringLiteral("Wheel revolution data present with wrong bytes ") + + QString::number(newValue.length())); + return; + } else if (newValue.at(0) == 0x00) { + emit debug(QStringLiteral("Cadence sensor notification without datas ") + + QString::number(newValue.length())); + return; + } + + if (newValue.at(0) == 0x02) { + CrankRevsRead = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + } else if (newValue.at(0) == 0x03) { + index += 6; + CrankRevsRead = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + } else { + return; + // CrankRevsRead = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) | + // ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8) + // | (uint32_t)((uint8_t)newValue.at(index))); + } + if (newValue.at(0) == 0x01) { + index += 4; + } else { + index += 2; + } + uint16_t LastCrankEventTimeRead = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + + int16_t deltaT = LastCrankEventTimeRead - oldLastCrankEventTime; + if (deltaT < 0) { + deltaT = LastCrankEventTimeRead + 65535 - oldLastCrankEventTime; + } + + if (CrankRevsRead != oldCrankRevs && deltaT) { + double cadence = (((double)CrankRevsRead - (double)oldCrankRevs) / (double)deltaT) * 1024.0 * 60.0; + if (cadence >= 0 && cadence < 255) { + Cadence = cadence; + } + lastGoodCadence = QDateTime::currentDateTime(); + } else if (lastGoodCadence.msecsTo(QDateTime::currentDateTime()) > 2000) { + Cadence = 0; + } + + oldLastCrankEventTime = LastCrankEventTimeRead; + oldCrankRevs = CrankRevsRead; + + Speed = Cadence.value() * + settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio) + .toDouble(); + + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + + // Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | + // (uint16_t)((uint8_t)newValue.at(index)))); debug("Current Resistance: " + + // QString::number(Resistance.value())); + + if (tacx_neo2_peloton) { + m_pelotonResistance = bikeResistanceToPeloton(Resistance.value()); + } else { + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + m_pelotonResistance = + (((sqrt(pow(br, 2.0) - 4.0 * ar * + (cr - (m_watt.value() * 132.0 / + (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + Resistance = m_pelotonResistance; + } + emit resistanceRead(Resistance.value()); + + if (watts()) + KCal += + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + emit debug(QStringLiteral("Current CrankRevsRead: ") + QString::number(CrankRevsRead)); + emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + } else if (characteristic.uuid() == QBluetoothUuid::HeartRateMeasurement) { + if (newValue.length() > 1) { + Heart = newValue[1]; + } + + emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value())); + } else if (characteristic.uuid() == QBluetoothUuid::CyclingPowerMeasurement) { + uint16_t flags = (((uint16_t)((uint8_t)newValue.at(1)) << 8) | (uint16_t)((uint8_t)newValue.at(0))); + bool cadence_present = false; + bool wheel_revs = false; + uint16_t time_division = 1024; + uint8_t index = 4; + + if (newValue.length() > 3) { + m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2))); + } + + emit powerChanged(m_watt.value()); + emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value())); + + if ((flags & 0x1) == 0x01) // Pedal Power Balance Present + { + index += 1; + } + if ((flags & 0x2) == 0x02) // Pedal Power Balance Reference + { + } + if ((flags & 0x4) == 0x04) // Accumulated Torque Present + { + index += 2; + } + if ((flags & 0x8) == 0x08) // Accumulated Torque Source + { + } + + if ((flags & 0x10) == 0x10) // Wheel Revolution Data Present + { + cadence_present = true; + wheel_revs = true; + time_division = 2048; + } else if ((flags & 0x20) == 0x20) // Crank Revolution Data Present + { + cadence_present = true; + } + + if (cadence_present) { + if (!wheel_revs) { + CrankRevs = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + index += 2; + } else { + CrankRevs = + (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) | + ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | + ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index))); + index += 4; + } + LastCrankEventTime = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + + int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime; + if (deltaT < 0) { + deltaT = LastCrankEventTime + time_division - oldLastCrankEventTime; + } + + if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) { + if (CrankRevs != oldCrankRevs && deltaT) { + double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * time_division * 60; + if (cadence >= 0) { + Cadence = cadence; + } + lastGoodCadence = QDateTime::currentDateTime(); + } else if (lastGoodCadence.msecsTo(QDateTime::currentDateTime()) > 2000) { + Cadence = 0; + } + } + + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); + + oldLastCrankEventTime = LastCrankEventTime; + oldCrankRevs = CrankRevs; + + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = Cadence.value() * settings + .value(QZSettings::cadence_sensor_speed_ratio, + QZSettings::default_cadence_sensor_speed_ratio) + .toDouble(); + } else { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); + + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + + // if we change this, also change the wattsFromResistance function. We can create a standard function in + // order to have all the costants in one place (I WANT MORE TIME!!!) + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + double res = + (((sqrt(pow(br, 2.0) - 4.0 * ar * + (cr - (m_watt.value() * 132.0 / + (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + + if (isnan(res)) { + if (Cadence.value() > 0) { + // let's keep the last good value + } else { + m_pelotonResistance = 0; + } + } else { + m_pelotonResistance = res; + } + + qDebug() << QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value()); + + if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance) + .toBool()) + Resistance = pelotonToBikeResistance(m_pelotonResistance.value()); + else + Resistance = m_pelotonResistance; + emit resistanceRead(Resistance.value()); + qDebug() << QStringLiteral("Current Resistance Calculated: ") + QString::number(Resistance.value()); + + if (watts()) + KCal += + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight + // in kg * 3.5) / 200 ) / 60 + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + } + } + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + Heart = (uint8_t)KeepAwakeHelper::heart(); + debug("Current Heart: " + QString::number(Heart.value())); + } +#endif + if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && Heart.value() == 0) { + update_hr_from_external(); + } + + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)currentHeart().value()); + } +#endif +#endif + + emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs)); + emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); + + if (m_control->error() != QLowEnergyController::NoError) + qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); +} + +void bkoolbike::stateChanged(QLowEnergyService::ServiceState state) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state(); + if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) { + qDebug() << QStringLiteral("not all services discovered"); + return; + } + } + + qDebug() << QStringLiteral("all services discovered!"); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + if (s->state() == QLowEnergyService::ServiceDiscovered) { + // establish hook into notifications + connect(s, &QLowEnergyService::characteristicChanged, this, &bkoolbike::characteristicChanged); + connect(s, &QLowEnergyService::characteristicWritten, this, &bkoolbike::characteristicWritten); + connect(s, &QLowEnergyService::characteristicRead, this, &bkoolbike::characteristicRead); + connect(s, SIGNAL(error(QLowEnergyService::ServiceError)), this, + SLOT(errorService(QLowEnergyService::ServiceError))); + connect(s, &QLowEnergyService::descriptorWritten, this, &bkoolbike::descriptorWritten); + connect(s, &QLowEnergyService::descriptorRead, this, &bkoolbike::descriptorRead); + + qDebug() << s->serviceUuid() << QStringLiteral("connected!"); + + auto characteristics = s->characteristics(); + for (const QLowEnergyCharacteristic &c : characteristics) { + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle() + << QStringLiteral("properties") << c.properties(); + auto descriptors = c.descriptors(); + for (const QLowEnergyDescriptor &d : descriptors) { + qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); + } + + if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) { + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Indicate) == + QLowEnergyCharacteristic::Indicate) { + QByteArray descriptor; + descriptor.append((char)0x02); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Read) == QLowEnergyCharacteristic::Read) { + // s->readCharacteristic(c); + // qDebug() << s->serviceUuid() << c.uuid() << "reading!"; + } + + if (c.properties() & QLowEnergyCharacteristic::Write && + c.uuid() == QBluetoothUuid::CyclingPowerControlPoint) { + qDebug() << QStringLiteral("CyclingPowerControlPoint found"); + gattWriteCharControlPointId = c; + gattPowerService = s; + } else if (/*c.properties() & QLowEnergyCharacteristic::Write &&*/ + c.uuid() == QBluetoothUuid(QStringLiteral("f03ee002-4910-473c-be46-960948c2f59c"))) { + qDebug() << QStringLiteral("CustomChar found"); + gattWriteCharCustomId = c; + gattCustomService = s; + } + } + } + } + + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + QSettings settings; + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + if (virtual_device_enabled) { + emit debug(QStringLiteral("creating virtual bike interface...")); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, 4, 1); + connect(virtualBike, &virtualbike::changeInclination, this, &bkoolbike::changeInclination); + // connect(virtualBike, &virtualbike::powerPacketReceived, this, &bkoolbike::powerPacketReceived); + // connect(virtualBike, &virtualbike::debug, this, &bkoolbike::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + firstStateChanged = 1; + // ******************************************************************************************************** +} + +void bkoolbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ')); + + initRequest = true; + emit connectedAndDiscovered(); +} + +void bkoolbike::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + qDebug() << QStringLiteral("descriptorRead ") << descriptor.name() << descriptor.uuid() << newValue.toHex(' '); +} + +void bkoolbike::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + Q_UNUSED(characteristic); + emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' ')); +} + +void bkoolbike::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + qDebug() << QStringLiteral("characteristicRead ") << characteristic.uuid() << newValue.toHex(' '); +} + +void bkoolbike::serviceScanDone(void) { + emit debug(QStringLiteral("serviceScanDone")); + +#ifdef Q_OS_ANDROID + QLowEnergyConnectionParameters c; + c.setIntervalRange(24, 40); + c.setLatency(0); + c.setSupervisionTimeout(420); + m_control->requestConnectionUpdate(c); +#endif + + auto services = m_control->services(); + for (const QBluetoothUuid &s : services) { + gattCommunicationChannelService.append(m_control->createServiceObject(s)); + connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, + &bkoolbike::stateChanged); + gattCommunicationChannelService.constLast()->discoverDetails(); + } +} + +void bkoolbike::errorService(QLowEnergyService::ServiceError err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("bkoolbike::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void bkoolbike::error(QLowEnergyController::Error err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("bkoolbike::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void bkoolbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + + device.address().toString() + ')'); + { + bluetoothDevice = device; + + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); + connect(m_control, &QLowEnergyController::serviceDiscovered, this, &bkoolbike::serviceDiscovered); + connect(m_control, &QLowEnergyController::discoveryFinished, this, &bkoolbike::serviceScanDone); + connect(m_control, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error))); + connect(m_control, &QLowEnergyController::stateChanged, this, &bkoolbike::controllerStateChanged); + + connect(m_control, + static_cast(&QLowEnergyController::error), + this, [this](QLowEnergyController::Error error) { + Q_UNUSED(error); + Q_UNUSED(this); + emit debug(QStringLiteral("Cannot connect to remote device.")); + emit disconnected(); + }); + connect(m_control, &QLowEnergyController::connected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("Controller connected. Search services...")); + m_control->discoverServices(); + }); + connect(m_control, &QLowEnergyController::disconnected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("LowEnergy controller disconnected")); + emit disconnected(); + }); + + // Connect + m_control->connectToDevice(); + return; + } +} + +bool bkoolbike::connected() { + if (!m_control) { + return false; + } + return m_control->state() == QLowEnergyController::DiscoveredState; +} + +uint16_t bkoolbike::watts() { + if (currentCadence().value() == 0) { + return 0; + } + + return m_watt.value(); +} + +void bkoolbike::controllerStateChanged(QLowEnergyController::ControllerState state) { + qDebug() << QStringLiteral("controllerStateChanged") << state; + if (state == QLowEnergyController::UnconnectedState && m_control) { + qDebug() << QStringLiteral("trying to connect back again..."); + initDone = false; + m_control->connectToDevice(); + } +} + +resistance_t bkoolbike::pelotonToBikeResistance(int pelotonResistance) { + for (resistance_t i = 0; i < max_resistance; i++) { + if (bikeResistanceToPeloton(i) <= pelotonResistance && bikeResistanceToPeloton(i + 1) >= pelotonResistance) { + return i; + } + } + if (pelotonResistance < bikeResistanceToPeloton(1)) + return 0; + else + return max_resistance; +} + +double bkoolbike::bikeResistanceToPeloton(double resistance) { + QSettings settings; + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + + if (tacx_neo2_peloton) { + return (resistance * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + } else { + return resistance; + } +} diff --git a/src/bkoolbike.h b/src/bkoolbike.h new file mode 100644 index 000000000..ccca2db91 --- /dev/null +++ b/src/bkoolbike.h @@ -0,0 +1,110 @@ +#ifndef BKOOLBIKE_H +#define BKOOLBIKE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include + +#include "bike.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class bkoolbike : public bike { + Q_OBJECT + public: + bkoolbike(bool noWriteResistance, bool noHeartService); + void changePower(int32_t power) override; + bool connected() override; + resistance_t pelotonToBikeResistance(int pelotonResistance); + + private: + void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, + bool wait_for_response = false); + void startDiscover(); + void forceInclination(double inclination); + uint16_t watts() override; + double bikeResistanceToPeloton(double resistance); + + QTimer *refresh; + + const int max_resistance = 100; + + QList gattCommunicationChannelService; + QLowEnergyCharacteristic gattWriteCharControlPointId; + QLowEnergyCharacteristic gattWriteCharCustomId; + QLowEnergyService *gattPowerService = nullptr; + QLowEnergyService *gattCustomService; + // QLowEnergyCharacteristic gattNotify1Characteristic; + + uint8_t sec1Update = 0; + QByteArray lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastGoodCadence = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + + uint16_t oldLastCrankEventTime = 0; + uint16_t oldCrankRevs = 0; + uint16_t CrankRevsRead = 0; + + double lastGearValue = -1; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + + private slots: + + void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void stateChanged(QLowEnergyService::ServiceState state); + void controllerStateChanged(QLowEnergyController::ControllerState state); + + void serviceDiscovered(const QBluetoothUuid &gatt); + void serviceScanDone(void); + void update(); + void error(QLowEnergyController::Error err); + void errorService(QLowEnergyService::ServiceError); + + void powerPacketReceived(const QByteArray &b); +}; + +#endif // BKOOLBIKE_H diff --git a/src/bluetooth.cpp b/src/bluetooth.cpp index 98c505c61..2cfb52515 100644 --- a/src/bluetooth.cpp +++ b/src/bluetooth.cpp @@ -15,11 +15,11 @@ bluetooth::bluetooth(const discoveryoptions &options) : bluetooth(options.logs, options.deviceName, options.noWriteResistance, options.noHeartService, options.pollDeviceTime, options.noConsole, options.testResistance, options.bikeResistanceOffset, - options.bikeResistanceGain, options.createTemplateManagers, options.startDiscovery) {} + options.bikeResistanceGain, options.startDiscovery) {} bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistance, bool noHeartService, uint32_t pollDeviceTime, bool noConsole, bool testResistance, uint8_t bikeResistanceOffset, - double bikeResistanceGain, bool createTemplateManagers, bool startDiscovery) { + double bikeResistanceGain, bool startDiscovery) { QSettings settings; QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); filterDevice = deviceName; @@ -33,23 +33,6 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc this->bikeResistanceOffset = bikeResistanceOffset; this->useDiscovery = startDiscovery; - this->createTemplateManagers = createTemplateManagers; - - QString innerId = QStringLiteral("inner"); - QString sKey = QStringLiteral("template_") + innerId + QStringLiteral("_" TEMPLATE_PRIVATE_WEBSERVER_ID "_"); - if (this->createTemplateManagers) { - QString path = homeform::getWritableAppDir() + QStringLiteral("QZTemplates"); - this->userTemplateManager = TemplateInfoSenderBuilder::getInstance( - QStringLiteral("user"), QStringList({path, QStringLiteral(":/templates/")}), this); - - settings.setValue(sKey + QStringLiteral("enabled"), true); - settings.setValue(sKey + QStringLiteral("type"), TEMPLATE_TYPE_WEBSERVER); - settings.setValue(sKey + QStringLiteral("port"), 0); - this->innerTemplateManager = - TemplateInfoSenderBuilder::getInstance(innerId, QStringList({QStringLiteral(":/inner_templates/")}), this); - } else { - settings.setValue(sKey + QStringLiteral("enabled"), false); - } QString nordictrack_2950_ip = settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString(); @@ -62,12 +45,14 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(pelotonBike); + // this signal is not associated to anything in this moment, since the homeform is not loaded yet + this->signalBluetoothDeviceConnected(pelotonBike); } #ifdef TEST schwinnIC4Bike = (schwinnic4bike *)new bike(); - this->startTemplateManagers(schwinnIC4Bike); + // this signal is not associated to anything in this moment, since the homeform is not loaded yet + this->signalBluetoothDeviceConnected(schwinnIC4Bike); connectedAndDiscovered(); return; #endif @@ -104,38 +89,6 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc #ifndef Q_OS_WIN discoveryAgent->setLowEnergyDiscoveryTimeout(10000); #endif - -#ifdef Q_OS_IOS - // Schwinn bikes on iOS allows to be connected to several instances, so in this way - // QZ will remember the address and will try to connect to it - QString b = settings.value(QZSettings::bluetooth_lastdevice_name, QZSettings::default_bluetooth_lastdevice_name) - .toString(); - qDebug() << "last device name (IC BIKE workaround)" << b; - if (!b.compare(settings.value(QZSettings::filter_device, QZSettings::default_filter_device).toString()) && - (b.toUpper().startsWith("IC BIKE") || b.toUpper().startsWith("C7-"))) { - - this->stopDiscovery(); - schwinnIC4Bike = new schwinnic4bike(noWriteResistance, noHeartService); - // stateFileRead(); - QBluetoothDeviceInfo bt; - bt.setDeviceUuid(QBluetoothUuid( - settings - .value(QZSettings::bluetooth_lastdevice_address, QZSettings::default_bluetooth_lastdevice_address) - .toString())); - // set name method doesn't exist - emit(deviceConnected(bt)); - connect(schwinnIC4Bike, SIGNAL(connectedAndDiscovered()), this, SLOT(connectedAndDiscovered())); - // connect(echelonConnectSport, SIGNAL(disconnected()), this, SLOT(restart())); - connect(schwinnIC4Bike, SIGNAL(debug(QString)), this, SLOT(debug(QString))); - // connect(echelonConnectSport, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); - // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); - qDebug() << "UUID" << bt.deviceUuid(); - schwinnIC4Bike->deviceDiscovered(bt); - this->startTemplateManagers(schwinnIC4Bike); - qDebug() << "connecting directly"; - } -#endif - this->startDiscovery(); } } @@ -148,30 +101,19 @@ bluetooth::~bluetooth() { }*/ } -void bluetooth::startTemplateManagers(bluetoothdevice *b) { - if (this->createTemplateManagers) { - this->userTemplateManager->start(b); - this->innerTemplateManager->start(b); - } -} - -void bluetooth::stopTemplateManagers() { - if (this->createTemplateManagers) { - this->userTemplateManager->stop(); - this->innerTemplateManager->stop(); - } -} +void bluetooth::signalBluetoothDeviceConnected(bluetoothdevice *b) { emit this->bluetoothDeviceConnected(b); } void bluetooth::finished() { debug(QStringLiteral("BTLE scanning finished")); QSettings settings; -#ifdef Q_OS_WIN QString nordictrack_2950_ip = settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString(); + QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); // wifi devices on windows - if (!nordictrack_2950_ip.isEmpty()) { + if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty()) { // faking a bluetooth device + qDebug() << "faking a bluetooth device for nordictrack_2950_ip"; deviceDiscovered(QBluetoothDeviceInfo()); } @@ -179,7 +121,6 @@ void bluetooth::finished() { qDebug() << QStringLiteral("bluetooth::finished but discoveryAgent is not active"); return; } -#endif QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); @@ -241,8 +182,10 @@ void bluetooth::startDiscovery() { .toBool(); bool trx_route_key = settings.value(QZSettings::trx_route_key, QZSettings::default_trx_route_key).toBool(); bool bh_spada_2 = settings.value(QZSettings::bh_spada_2, QZSettings::default_bh_spada_2).toBool(); + bool iconcept_elliptical = + settings.value(QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical).toBool(); - if (!trx_route_key && !bh_spada_2 && !technogym_myrun_treadmill_experimental) { + if (!trx_route_key && !bh_spada_2 && !technogym_myrun_treadmill_experimental && !iconcept_elliptical) { #endif discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); #ifndef Q_OS_IOS @@ -451,6 +394,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool(); bool fakedevice_elliptical = settings.value(QZSettings::fakedevice_elliptical, QZSettings::default_fakedevice_elliptical).toBool(); + bool fakedevice_rower = settings.value(QZSettings::fakedevice_rower, QZSettings::default_fakedevice_rower).toBool(); bool fakedevice_treadmill = settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool(); bool pafers_treadmill = settings.value(QZSettings::pafers_treadmill, QZSettings::default_pafers_treadmill).toBool(); @@ -462,12 +406,26 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); QString computrainerSerialPort = settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString(); + QString csaferowerSerialPort = settings.value(QZSettings::csafe_rower, QZSettings::default_csafe_rower).toString(); bool manufacturerDeviceFound = false; bool ss2k_peloton = settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool(); bool pafers_treadmill_bh_iboxster_plus = settings .value(QZSettings::pafers_treadmill_bh_iboxster_plus, QZSettings::default_pafers_treadmill_bh_iboxster_plus) .toBool(); + bool gem_module_inclination = + settings.value(QZSettings::gem_module_inclination, QZSettings::default_gem_module_inclination).toBool(); + bool iconcept_elliptical = + settings.value(QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical).toBool(); + bool horizon_treadmill_force_ftms = + settings.value(QZSettings::horizon_treadmill_force_ftms, QZSettings::default_horizon_treadmill_force_ftms) + .toBool(); + bool sole_inclination = + settings.value(QZSettings::sole_treadmill_inclination, QZSettings::default_sole_treadmill_inclination).toBool(); + QString ftms_rower = settings.value(QZSettings::ftms_rower, QZSettings::default_ftms_rower).toString(); + QString ftms_bike = settings.value(QZSettings::ftms_bike, QZSettings::default_ftms_bike).toString(); + QString ftms_treadmill = settings.value(QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill).toString(); + bool saris_trainer = settings.value(QZSettings::saris_trainer, QZSettings::default_saris_trainer).toBool(); if (!heartRateBeltFound) { @@ -501,6 +459,37 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { eliteSterzoSmartFound = eliteSterzoSmartAvaiable(); } +#ifdef Q_OS_IOS + // Schwinn bikes on iOS allows to be connected to several instances, so in this way + // QZ will remember the address and will try to connect to it + QString b = + settings.value(QZSettings::bluetooth_lastdevice_name, QZSettings::default_bluetooth_lastdevice_name).toString(); + qDebug() << "last device name (IC BIKE workaround)" << b; + if (!schwinnIC4Bike && + !b.compare(settings.value(QZSettings::filter_device, QZSettings::default_filter_device).toString()) && + (b.toUpper().startsWith("IC BIKE") || b.toUpper().startsWith("C7-"))) { + + this->stopDiscovery(); + schwinnIC4Bike = new schwinnic4bike(noWriteResistance, noHeartService); + // stateFileRead(); + QBluetoothDeviceInfo bt; + bt.setDeviceUuid(QBluetoothUuid( + settings.value(QZSettings::bluetooth_lastdevice_address, QZSettings::default_bluetooth_lastdevice_address) + .toString())); + // set name method doesn't exist + emit(deviceConnected(bt)); + connect(schwinnIC4Bike, SIGNAL(connectedAndDiscovered()), this, SLOT(connectedAndDiscovered())); + // connect(echelonConnectSport, SIGNAL(disconnected()), this, SLOT(restart())); + connect(schwinnIC4Bike, SIGNAL(debug(QString)), this, SLOT(debug(QString))); + // connect(echelonConnectSport, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); + // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); + qDebug() << "UUID" << bt.deviceUuid(); + schwinnIC4Bike->deviceDiscovered(bt); + this->signalBluetoothDeviceConnected(schwinnIC4Bike); + qDebug() << "connecting directly"; + } +#endif + QVector ids = device.manufacturerIds(); qDebug() << "manufacturerData"; foreach (quint16 id, ids) { @@ -594,7 +583,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, m3iBike, &m3ibike::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(m3iBike); + this->signalBluetoothDeviceConnected(m3iBike); } } else if (fake_bike && !fakeBike) { this->stopDiscovery(); @@ -608,7 +597,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(fakeBike); + this->signalBluetoothDeviceConnected(fakeBike); } else if (fakedevice_elliptical && !fakeElliptical) { this->stopDiscovery(); fakeElliptical = new fakeelliptical(noWriteResistance, noHeartService, false); @@ -622,7 +611,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(fakeElliptical); + this->signalBluetoothDeviceConnected(fakeElliptical); + } else if (fakedevice_rower && !fakeRower) { + this->stopDiscovery(); + fakeRower = new fakerower(noWriteResistance, noHeartService, false); + emit deviceConnected(b); + connect(fakeRower, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); + connect(fakeRower, &fakerower::inclinationChanged, this, &bluetooth::inclinationChanged); + // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); + // connect(this, SIGNAL(searchingStop()), fakeBike, SLOT(searchingStop())); //NOTE: Commented due to + // #358 + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + this->signalBluetoothDeviceConnected(fakeRower); } else if (fakedevice_treadmill && !fakeTreadmill) { this->stopDiscovery(); fakeTreadmill = new faketreadmill(noWriteResistance, noHeartService, false); @@ -636,7 +638,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(fakeTreadmill); + this->signalBluetoothDeviceConnected(fakeTreadmill); } else if (!proformtdf4ip.isEmpty() && !proformWifiBike) { this->stopDiscovery(); @@ -652,7 +654,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(proformWifiBike); + this->signalBluetoothDeviceConnected(proformWifiBike); #ifndef Q_OS_IOS } else if (!computrainerSerialPort.isEmpty() && !computrainerBike) { this->stopDiscovery(); @@ -668,7 +670,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(computrainerBike); + this->signalBluetoothDeviceConnected(computrainerBike); + } else if (!csaferowerSerialPort.isEmpty() && !csafeRower) { + this->stopDiscovery(); + csafeRower = new csaferower(noWriteResistance, noHeartService, false); + emit deviceConnected(b); + connect(csafeRower, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); + // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); + connect(csafeRower, &csaferower::debug, this, &bluetooth::debug); + csafeRower->deviceDiscovered(b); + // connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358 + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + this->signalBluetoothDeviceConnected(csafeRower); #endif } else if (!proformtreadmillip.isEmpty() && !proformWifiTreadmill) { this->stopDiscovery(); @@ -684,7 +699,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(proformWifiTreadmill); + this->signalBluetoothDeviceConnected(proformWifiTreadmill); } else if (!nordictrack_2950_ip.isEmpty() && !nordictrackifitadbTreadmill) { this->stopDiscovery(); nordictrackifitadbTreadmill = new nordictrackifitadbtreadmill(noWriteResistance, noHeartService); @@ -697,10 +712,11 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(nordictrackifitadbTreadmill); + this->signalBluetoothDeviceConnected(nordictrackifitadbTreadmill); } else if (!tdf_10_ip.isEmpty() && !nordictrackifitadbBike) { this->stopDiscovery(); - nordictrackifitadbBike = new nordictrackifitadbbike(noWriteResistance, noHeartService); + nordictrackifitadbBike = new nordictrackifitadbbike(noWriteResistance, noHeartService, + bikeResistanceOffset, bikeResistanceGain); emit deviceConnected(b); connect(nordictrackifitadbBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); @@ -710,7 +726,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(nordictrackifitadbBike); + this->signalBluetoothDeviceConnected(nordictrackifitadbBike); } else if (((csc_as_bike && b.name().startsWith(cscName)) || b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) && !cscBike && filter) { @@ -726,7 +742,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(cscBike); + this->signalBluetoothDeviceConnected(cscBike); } else if (power_as_bike && b.name().startsWith(powerSensorName) && !powerBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -740,7 +756,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(powerBike); + this->signalBluetoothDeviceConnected(powerBike); } else if ((((power_as_treadmill && b.name().startsWith(powerSensorName))) || b.name().toUpper().startsWith(QStringLiteral("ZWIFT RUNPOD"))) && !powerTreadmill && filter) { @@ -757,7 +773,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(powerTreadmill); + this->signalBluetoothDeviceConnected(powerTreadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("DOMYOS-ROW")) && !b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && filter) { this->setLastBluetoothDevice(b); @@ -774,7 +790,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(domyosRower); + this->signalBluetoothDeviceConnected(domyosRower); } else if (b.name().startsWith(QStringLiteral("Domyos-Bike")) && !b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) { this->setLastBluetoothDevice(b); @@ -790,7 +806,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(domyosBike); + this->signalBluetoothDeviceConnected(domyosBike); } else if (b.name().startsWith(QStringLiteral("Domyos-EL")) && !b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosElliptical && filter) { this->setLastBluetoothDevice(b); @@ -807,7 +823,23 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(domyosElliptical); + this->signalBluetoothDeviceConnected(domyosElliptical); + } else if (b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) && !ypooElliptical && filter) { + this->setLastBluetoothDevice(b); + this->stopDiscovery(); + ypooElliptical = + new ypooelliptical(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + emit deviceConnected(b); + connect(ypooElliptical, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(domyosElliptical, SIGNAL(disconnected()), this, SLOT(restart())); + connect(ypooElliptical, &ypooelliptical::debug, this, &bluetooth::debug); + ypooElliptical->deviceDiscovered(b); + // connect(this, &bluetooth::searchingStop, ypooElliptical, &ypooelliptical::searchingStop); + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + this->signalBluetoothDeviceConnected(ypooElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("NAUTILUS E"))) && !nautilusElliptical && // NAUTILUS E616 filter) { @@ -824,7 +856,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, nautilusElliptical, &nautiluselliptical::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(nautilusElliptical); + this->signalBluetoothDeviceConnected(nautilusElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("NAUTILUS B"))) && !nautilusBike && filter) { // NAUTILUS B628 this->setLastBluetoothDevice(b); @@ -840,7 +872,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, nautilusBike, &nautilusbike::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(nautilusBike); + this->signalBluetoothDeviceConnected(nautilusBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("I_FS"))) && !proformElliptical && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -854,7 +886,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(this, &bluetooth::searchingStop, proformElliptical, &proformelliptical::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(proformElliptical); + this->signalBluetoothDeviceConnected(proformElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("I_EL"))) && !nordictrackElliptical && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -869,7 +901,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(this, &bluetooth::searchingStop, proformElliptical, &proformelliptical::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(nordictrackElliptical); + this->signalBluetoothDeviceConnected(nordictrackElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("I_VE"))) && !proformEllipticalTrainer && filter) { this->setLastBluetoothDevice(b); @@ -886,7 +918,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // &proformellipticaltrainer::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(proformEllipticalTrainer); + this->signalBluetoothDeviceConnected(proformEllipticalTrainer); } else if ((b.name().toUpper().startsWith(QStringLiteral("I_RW"))) && !proformRower && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -900,7 +932,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(this, &bluetooth::searchingStop, proformElliptical, &proformelliptical::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(proformRower); + this->signalBluetoothDeviceConnected(proformRower); } else if ((b.name().toUpper().startsWith(QStringLiteral("B01_"))) && !bhFitnessElliptical && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -915,7 +947,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(this, &bluetooth::searchingStop, bhFitnessElliptical, &bhfitnesselliptical::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(bhFitnessElliptical); + this->signalBluetoothDeviceConnected(bhFitnessElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("E95S")) || b.name().toUpper().startsWith(QStringLiteral("E25")) || b.name().toUpper().startsWith(QStringLiteral("E35")) || @@ -938,7 +970,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, soleElliptical, &soleelliptical::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(soleElliptical); + this->signalBluetoothDeviceConnected(soleElliptical); } else if (b.name().startsWith(QStringLiteral("Domyos")) && !b.name().startsWith(QStringLiteral("DomyosBr")) && !domyos && !domyosElliptical && !domyosBike && !domyosRower && filter) { @@ -958,7 +990,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, domyos, &domyostreadmill::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(domyos); + this->signalBluetoothDeviceConnected(domyos); } else if (( // Xiaomi k12 pro treadmill KS-ST-K12PRO b.name().toUpper().startsWith(QStringLiteral("KS-ST-K12PRO")) || @@ -970,6 +1002,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { b.name().toUpper().startsWith(QStringLiteral("KS-X21")) || b.name().toUpper().startsWith(QStringLiteral("KS-HDSC-X21C")) || b.name().toUpper().startsWith(QStringLiteral("KS-HDSY-X21C")) || + b.name().toUpper().startsWith(QStringLiteral("KS-NACH-X21C")) || b.name().toUpper().startsWith(QStringLiteral("KS-NGCH-X21C"))) && !kingsmithR2Treadmill && filter) { this->setLastBluetoothDevice(b); @@ -990,13 +1023,16 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, kingsmithR2Treadmill, &kingsmithr2treadmill::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(kingsmithR2Treadmill); + this->signalBluetoothDeviceConnected(kingsmithR2Treadmill); } else if ((b.name().toUpper().startsWith(QStringLiteral("R1 PRO")) || b.name().toUpper().startsWith(QStringLiteral("KINGSMITH")) || b.name().toUpper().startsWith(QStringLiteral("DYNAMAX")) || + b.name().toUpper().startsWith(QStringLiteral("WALKINGPAD")) || !b.name().toUpper().compare(QStringLiteral("RE")) || // just "RE" + b.name().toUpper().startsWith(QStringLiteral("KS-H")) || + b.name().toUpper().startsWith(QStringLiteral("KS-BLC")) || // Walkingpad C2 #1672 b.name().toUpper().startsWith( - QStringLiteral("KS-H"))) && // Treadmill KingSmith WalkingPad R2 Pro KS-HCR1AA + QStringLiteral("KS-BLR"))) && // Treadmill KingSmith WalkingPad R2 Pro KS-HCR1AA !kingsmithR1ProTreadmill && !kingsmithR2Treadmill && filter) { this->setLastBluetoothDevice(b); @@ -1019,7 +1055,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { &kingsmithr1protreadmill::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(kingsmithR1ProTreadmill); + this->signalBluetoothDeviceConnected(kingsmithR1ProTreadmill); } else if ((b.name().toUpper().startsWith(QStringLiteral("ZW-"))) && !shuaA5Treadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1037,9 +1073,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { shuaA5Treadmill->deviceDiscovered(b); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(shuaA5Treadmill); + this->signalBluetoothDeviceConnected(shuaA5Treadmill); } else if ((b.name().toUpper().startsWith(QStringLiteral("TRUE")) || - b.name().toUpper().startsWith(QStringLiteral("TREADMILL"))) && + b.name().toUpper().startsWith(QStringLiteral("ASSAULT TREADMILL ")) || + (b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && !gem_module_inclination)) && !trueTreadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1057,14 +1094,14 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { trueTreadmill->deviceDiscovered(b); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(trueTreadmill); - } else if ((b.name().toUpper().startsWith(QStringLiteral("F80")) || + this->signalBluetoothDeviceConnected(trueTreadmill); + } else if (((b.name().toUpper().startsWith(QStringLiteral("F80")) && sole_inclination) || b.name().toUpper().startsWith(QStringLiteral("F65")) || b.name().toUpper().startsWith(QStringLiteral("TT8")) || b.name().toUpper().startsWith(QStringLiteral("F63")) || b.name().toUpper().startsWith(QStringLiteral("ST90")) || b.name().toUpper().startsWith(QStringLiteral("S77")) || - b.name().toUpper().startsWith(QStringLiteral("F85"))) && + (b.name().toUpper().startsWith(QStringLiteral("F85")) && sole_inclination)) && !soleF80 && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1087,7 +1124,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(soleF80); + this->signalBluetoothDeviceConnected(soleF80); } else if ((b.name().toUpper().startsWith(QStringLiteral("LF")) && b.name().length() == 18) && !lifefitnessTreadmill && filter) { this->setLastBluetoothDevice(b); @@ -1112,25 +1149,36 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(lifefitnessTreadmill); + this->signalBluetoothDeviceConnected(lifefitnessTreadmill); } else if ((b.name().toUpper().startsWith(QStringLiteral("HORIZON")) || b.name().toUpper().startsWith(QStringLiteral("AFG SPORT")) || b.name().toUpper().startsWith(QStringLiteral("WLT2541")) || + (b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && gem_module_inclination) || b.name().toUpper().startsWith(QStringLiteral("T318_")) || // FTMS (b.name().toUpper().startsWith(QStringLiteral("DK")) && b.name().length() >= 11 && !toorx_bike) || // FTMS b.name().toUpper().startsWith(QStringLiteral("T218_")) || // FTMS b.name().toUpper().startsWith(QStringLiteral("TRX3500")) || // FTMS b.name().toUpper().startsWith(QStringLiteral("JFTMPARAGON")) || + b.name().toUpper().startsWith(QStringLiteral("PARAGON X")) || b.name().toUpper().startsWith(QStringLiteral("MX-TM ")) || // FTMS b.name().toUpper().startsWith(QStringLiteral("JFTM")) || // FTMS b.name().toUpper().startsWith(QStringLiteral("CT800")) || // FTMS b.name().toUpper().startsWith(QStringLiteral("TRX4500")) || // FTMS b.name().toUpper().startsWith(QStringLiteral("MATRIXTF50")) || // FTMS + b.name().toUpper().startsWith(QStringLiteral("T01_")) || // FTMS + (b.name().toUpper().startsWith(QStringLiteral("TF-")) && + horizon_treadmill_force_ftms) || // FTMS, TF-769DF2 ((b.name().toUpper().startsWith(QStringLiteral("TOORX")) || (b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")))) && !toorx_ftms && toorx_ftms_treadmill) || - b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS + !b.name().compare(ftms_treadmill, Qt::CaseInsensitive) || + b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS + b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS + b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS + (b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15) || // FTMS + (b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS + (b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS b.name().toUpper().startsWith(QStringLiteral("ESANGLINKER"))) && !horizonTreadmill && filter) { this->setLastBluetoothDevice(b); @@ -1155,7 +1203,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(horizonTreadmill); + this->signalBluetoothDeviceConnected(horizonTreadmill); } else if ((b.name().toUpper().startsWith(QStringLiteral("MYRUN ")) || b.name().toUpper().startsWith(QStringLiteral("MERACH-U3")) // FTMS ) && @@ -1192,7 +1240,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(technogymmyrunTreadmill); + this->signalBluetoothDeviceConnected(technogymmyrunTreadmill); } #ifndef Q_OS_IOS else { @@ -1216,12 +1264,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(technogymmyrunrfcommTreadmill); + this->signalBluetoothDeviceConnected(technogymmyrunrfcommTreadmill); } #endif - } else if ((b.name().toUpper().startsWith("TACX NEO") || - b.name().toUpper().startsWith(QStringLiteral("TACX FLOW")) || + } else if ((b.name().toUpper().startsWith("TACX ") || b.name().toUpper().startsWith(QStringLiteral("THINK X")) || + b.address() == QBluetoothAddress("C1:14:D9:9C:FB:01") || // specific TACX NEO 2 #1707 (b.name().toUpper().startsWith("TACX SMART BIKE"))) && !tacxneo2Bike && filter) { this->setLastBluetoothDevice(b); @@ -1235,7 +1283,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(tacxneo2Bike, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(tacxneo2Bike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); tacxneo2Bike->deviceDiscovered(b); - this->startTemplateManagers(tacxneo2Bike); + this->signalBluetoothDeviceConnected(tacxneo2Bike); } else if ((b.name().toUpper().startsWith(QStringLiteral(">CABLE")) || (b.name().toUpper().startsWith(QStringLiteral("MD")) && b.name().length() == 7) || // BIKE 1, BIKE 2, BIKE 3... @@ -1255,26 +1303,34 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); npeCableBike->deviceDiscovered(b); - this->startTemplateManagers(npeCableBike); + this->signalBluetoothDeviceConnected(npeCableBike); } else if (((b.name().startsWith("FS-") && hammerRacerS) || - (b.name().toUpper().startsWith("DHZ-")) || // JK fitness 577 - (b.name().toUpper().startsWith("MKSM")) || // MKSM3600036 - (b.name().toUpper().startsWith("YS_C1_")) || // Yesoul C1H - (b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25 + (b.name().toUpper().startsWith("DI") && b.name().length() == 2) || // Elite smart trainer #1682 + (b.name().toUpper().startsWith("DHZ-")) || // JK fitness 577 + (b.name().toUpper().startsWith("MKSM")) || // MKSM3600036 + (b.name().toUpper().startsWith("YS_C1_")) || // Yesoul C1H + (b.name().toUpper().startsWith("YS_G1_")) || // Yesoul S3 + (b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25 (b.name().toUpper().startsWith("SCHWINN 510T")) || (b.name().toUpper().startsWith("ZWIFT HUB")) || (b.name().toUpper().startsWith("MAGNUS ")) || - (b.name().toUpper().startsWith("HAMMER ")) || // HAMMER 64123 + (b.name().toUpper().startsWith("HAMMER ") && !power_as_bike && !saris_trainer) || // HAMMER 64123 (b.name().toUpper().startsWith("FLXCY-")) || // Pro FlexBike + (b.name().toUpper().startsWith("QB-WC01")) || // Nexgim QB-C01 smart bike (b.name().toUpper().startsWith("XBR55")) || // Sprint XBR555 + (b.name().toUpper().startsWith("EW-JS-")) || // EW-JS-4990 (b.name().toUpper().startsWith("DT-") && b.name().length() >= 14) || // SOLE SB700 + (b.name().toUpper().startsWith("URSB") && b.name().length() == 7) || // URSB005 + (b.name().toUpper().startsWith("DBF") && b.name().length() == 6) || // DBF135 (b.name().toUpper().startsWith(ftmsAccessoryName.toUpper()) && settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton) .toBool()) || // ss2k on a peloton bike (b.name().toUpper().startsWith("KICKR CORE")) || + (b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) || (b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) || (b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) || - (b.name().toUpper().startsWith("DIRETO XR")) || (b.name().toUpper().startsWith("SMB1")) || - (b.name().toUpper().startsWith("INRIDE"))) && + (b.name().toUpper().startsWith("DIRETO XR")) || + !b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) || + (b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) && !ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1284,9 +1340,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart())); connect(ftmsBike, &ftmsbike::debug, this, &bluetooth::debug); ftmsBike->deviceDiscovered(b); - this->startTemplateManagers(ftmsBike); + this->signalBluetoothDeviceConnected(ftmsBike); } else if ((b.name().toUpper().startsWith("KICKR SNAP") || b.name().toUpper().startsWith("KICKR BIKE") || b.name().toUpper().startsWith("KICKR ROLLR") || + (b.name().toUpper().startsWith("HAMMER ") && saris_trainer) || (b.name().toUpper().startsWith("WAHOO KICKR"))) && !wahooKickrSnapBike && filter) { this->setLastBluetoothDevice(b); @@ -1299,7 +1356,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(wahooKickrSnapBike, SIGNAL(disconnected()), this, SLOT(restart())); connect(wahooKickrSnapBike, &wahookickrsnapbike::debug, this, &bluetooth::debug); wahooKickrSnapBike->deviceDiscovered(b); - this->startTemplateManagers(wahooKickrSnapBike); + this->signalBluetoothDeviceConnected(wahooKickrSnapBike); } else if (((b.name().toUpper().startsWith("JFIC")) // HORIZON GR7 ) && !horizonGr7Bike && filter) { @@ -1313,7 +1370,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart())); connect(horizonGr7Bike, &horizongr7bike::debug, this, &bluetooth::debug); horizonGr7Bike->deviceDiscovered(b); - this->startTemplateManagers(horizonGr7Bike); + this->signalBluetoothDeviceConnected(horizonGr7Bike); } else if ((b.name().toUpper().startsWith(QStringLiteral("STAGES ")) || (b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) && powerSensorName.startsWith(QStringLiteral("Disabled")))) && @@ -1329,8 +1386,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(stagesBike, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(stagesBike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); stagesBike->deviceDiscovered(b); - this->startTemplateManagers(stagesBike); - } else if (b.name().startsWith(QStringLiteral("SMARTROW")) && !smartrowRower && filter) { + this->signalBluetoothDeviceConnected(stagesBike); + } else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !smartrowRower && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); smartrowRower = @@ -1344,9 +1401,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(v, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(smartrowRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); smartrowRower->deviceDiscovered(b); - this->startTemplateManagers(smartrowRower); + this->signalBluetoothDeviceConnected(smartrowRower); } else if ((b.name().toUpper().startsWith(QStringLiteral("PM5")) && - b.name().toUpper().endsWith(QStringLiteral("SKI"))) && + !b.name().toUpper().endsWith(QStringLiteral("ROW"))) && !concept2Skierg && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1360,15 +1417,18 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(v, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(concept2Skierg, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); concept2Skierg->deviceDiscovered(b); - this->startTemplateManagers(concept2Skierg); + this->signalBluetoothDeviceConnected(concept2Skierg); } else if ((b.name().toUpper().startsWith(QStringLiteral("CR 00")) || b.name().toUpper().startsWith(QStringLiteral("KAYAKPRO")) || b.name().toUpper().startsWith(QStringLiteral("WHIPR")) || + b.name().toUpper().startsWith(QStringLiteral("S4 COMMS")) || b.name().toUpper().startsWith(QStringLiteral("KS-WLT")) || // KS-WLT-W1 b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) || b.name().toUpper().startsWith(QStringLiteral("SF-RW")) || + b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) || + !b.name().compare(ftms_rower, Qt::CaseInsensitive) || (b.name().toUpper().startsWith(QStringLiteral("PM5")) && - b.name().toUpper().contains(QStringLiteral("ROW")))) && + b.name().toUpper().endsWith(QStringLiteral("ROW")))) && !ftmsRower && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1381,9 +1441,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(v, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(ftmsRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); ftmsRower->deviceDiscovered(b); - this->startTemplateManagers(ftmsRower); + this->signalBluetoothDeviceConnected(ftmsRower); } else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) || b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) || + b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) || b.name().toUpper().startsWith(QLatin1String("ECH-SD-SPT"))) && !echelonStride && filter) { this->setLastBluetoothDevice(b); @@ -1398,7 +1459,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(echelonStride, &echelonstride::speedChanged, this, &bluetooth::speedChanged); connect(echelonStride, &echelonstride::inclinationChanged, this, &bluetooth::inclinationChanged); echelonStride->deviceDiscovered(b); - this->startTemplateManagers(echelonStride); + this->signalBluetoothDeviceConnected(echelonStride); } else if ((b.name().toUpper().startsWith(QLatin1String("Q37"))) && !octaneElliptical && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1412,7 +1473,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(octaneElliptical, &octaneelliptical::speedChanged, this, &bluetooth::speedChanged); connect(octaneElliptical, &octaneelliptical::inclinationChanged, this, &bluetooth::inclinationChanged); octaneElliptical->deviceDiscovered(b); - this->startTemplateManagers(octaneElliptical); + this->signalBluetoothDeviceConnected(octaneElliptical); } else if ((b.name().toUpper().startsWith(QLatin1String("ZR7")) || b.name().toUpper().startsWith(QLatin1String("ZR8"))) && !octaneTreadmill && filter) { @@ -1428,7 +1489,21 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(octaneTreadmill, &octanetreadmill::speedChanged, this, &bluetooth::speedChanged); connect(octaneTreadmill, &octanetreadmill::inclinationChanged, this, &bluetooth::inclinationChanged); octaneTreadmill->deviceDiscovered(b); - this->startTemplateManagers(octaneTreadmill); + this->signalBluetoothDeviceConnected(octaneTreadmill); + } else if ((b.name().toUpper().startsWith(QLatin1String("RZ_TREADMIL"))) && !ziproTreadmill && filter) { + this->setLastBluetoothDevice(b); + this->stopDiscovery(); + ziproTreadmill = new ziprotreadmill(this->pollDeviceTime, noConsole, noHeartService); + // stateFileRead(); + emit deviceConnected(b); + connect(ziproTreadmill, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(ziproTreadmill, SIGNAL(disconnected()), this, SLOT(restart())); connect(echelonStride, + connect(ziproTreadmill, &ziprotreadmill::debug, this, &bluetooth::debug); + connect(ziproTreadmill, &ziprotreadmill::speedChanged, this, &bluetooth::speedChanged); + connect(ziproTreadmill, &ziprotreadmill::inclinationChanged, this, &bluetooth::inclinationChanged); + ziproTreadmill->deviceDiscovered(b); + this->signalBluetoothDeviceConnected(ziproTreadmill); } else if ((b.name().startsWith(QStringLiteral("ECH-ROW")) || b.name().toUpper().startsWith(QStringLiteral("ROWSPORT-")) || b.name().startsWith(QStringLiteral("ROW-S"))) && @@ -1446,7 +1521,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonRower, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(echelonRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); echelonRower->deviceDiscovered(b); - this->startTemplateManagers(echelonRower); + this->signalBluetoothDeviceConnected(echelonRower); } else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride && !echelonConnectSport && filter) { this->setLastBluetoothDevice(b); @@ -1463,7 +1538,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); echelonConnectSport->deviceDiscovered(b); - this->startTemplateManagers(echelonConnectSport); + this->signalBluetoothDeviceConnected(echelonConnectSport); } else if (b.name().toUpper().startsWith(QStringLiteral("WLT8266BM")) && !apexBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1472,7 +1547,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit deviceConnected(b); connect(apexBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); apexBike->deviceDiscovered(b); - this->startTemplateManagers(apexBike); + this->signalBluetoothDeviceConnected(apexBike); + } else if (b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) && !bkoolBike && filter) { + this->setLastBluetoothDevice(b); + this->stopDiscovery(); + bkoolBike = new bkoolbike(noWriteResistance, noHeartService); + // stateFileRead(); + emit deviceConnected(b); + connect(bkoolBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); + connect(bkoolBike, &bkoolbike::debug, this, &bluetooth::debug); + bkoolBike->deviceDiscovered(b); + this->signalBluetoothDeviceConnected(bkoolBike); } else if (b.name().toUpper().startsWith(QStringLiteral("MEPANEL")) && !mepanelBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1483,7 +1568,24 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(mepanelBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); mepanelBike->deviceDiscovered(b); - this->startTemplateManagers(mepanelBike); + this->signalBluetoothDeviceConnected(mepanelBike); + } else if ((b.name().toUpper().startsWith(QStringLiteral("SCHWINN 170/270"))) && !schwinn170Bike && + filter) { + this->setLastBluetoothDevice(b); + this->stopDiscovery(); + schwinn170Bike = + new schwinn170bike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + // stateFileRead(); + emit deviceConnected(b); + connect(schwinn170Bike, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(echelonConnectSport, SIGNAL(disconnected()), this, SLOT(restart())); + connect(schwinn170Bike, &schwinn170bike::debug, this, &bluetooth::debug); + // connect(echelonConnectSport, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); + // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, + // SLOT(inclinationChanged(double))); + schwinn170Bike->deviceDiscovered(b); + this->signalBluetoothDeviceConnected(schwinn170Bike); } else if ((b.name().toUpper().startsWith(QStringLiteral("IC BIKE")) || (b.name().toUpper().startsWith(QStringLiteral("C7-")) && b.name().length() != 17) || b.name().toUpper().startsWith(QStringLiteral("C9/C10"))) && @@ -1501,7 +1603,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); schwinnIC4Bike->deviceDiscovered(b); - this->startTemplateManagers(schwinnIC4Bike); + this->signalBluetoothDeviceConnected(schwinnIC4Bike); } else if (b.name().toUpper().startsWith(QStringLiteral("EW-BK")) && !sportsTechBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1516,7 +1618,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); sportsTechBike->deviceDiscovered(b); - this->startTemplateManagers(sportsTechBike); + this->signalBluetoothDeviceConnected(sportsTechBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("CARDIOFIT")) || (b.name().toUpper().contains(QStringLiteral("CARE")) && b.name().length() == 11)) // CARE9040177 - Carefitness CV-351 @@ -1534,7 +1636,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(sportsPlusBike, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); sportsPlusBike->deviceDiscovered(b); - this->startTemplateManagers(sportsPlusBike); + this->signalBluetoothDeviceConnected(sportsPlusBike); } else if (b.name().startsWith(yesoulbike::bluetoothName) && !yesoulBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1549,7 +1651,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); yesoulBike->deviceDiscovered(b); - this->startTemplateManagers(yesoulBike); + this->signalBluetoothDeviceConnected(yesoulBike); } else if ((b.name().startsWith(QStringLiteral("I_EB")) || b.name().startsWith(QStringLiteral("I_SB"))) && !proformBike && filter) { this->setLastBluetoothDevice(b); @@ -1565,7 +1667,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(proformBike, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); // connect(proformBike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double))); proformBike->deviceDiscovered(b); - this->startTemplateManagers(proformBike); + this->signalBluetoothDeviceConnected(proformBike); } else if ((b.name().startsWith(QStringLiteral("I_TL")) || b.name().startsWith(QStringLiteral("I_IT"))) && !proformTreadmill && filter) { this->setLastBluetoothDevice(b); @@ -1581,7 +1683,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(proformtreadmill, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); proformTreadmill->deviceDiscovered(b); - this->startTemplateManagers(proformTreadmill); + this->signalBluetoothDeviceConnected(proformTreadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("ESLINKER")) && !eslinkerTreadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1596,7 +1698,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(proformtreadmill, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); eslinkerTreadmill->deviceDiscovered(b); - this->startTemplateManagers(eslinkerTreadmill); + this->signalBluetoothDeviceConnected(eslinkerTreadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("PAFERS_")) && !pafersTreadmill && (pafers_treadmill || pafers_treadmill_bh_iboxster_plus) && filter) { this->setLastBluetoothDevice(b); @@ -1612,7 +1714,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(pafersTreadmill, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); pafersTreadmill->deviceDiscovered(b); - this->startTemplateManagers(pafersTreadmill); + this->signalBluetoothDeviceConnected(pafersTreadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("BOWFLEX T")) && !bowflexT216Treadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1627,7 +1729,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(bowflexTreadmill, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); bowflexT216Treadmill->deviceDiscovered(b); - this->startTemplateManagers(bowflexT216Treadmill); + this->signalBluetoothDeviceConnected(bowflexT216Treadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("NAUTILUS T")) && !nautilusTreadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1642,7 +1744,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(nautilusTreadmill, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); nautilusTreadmill->deviceDiscovered(b); - this->startTemplateManagers(nautilusTreadmill); + this->signalBluetoothDeviceConnected(nautilusTreadmill); } else if ((b.name().startsWith(QStringLiteral("Flywheel")) || // BIKE 1, BIKE 2, BIKE 3... (b.name().toUpper().startsWith(QStringLiteral("BIKE")) && flywheel_life_fitness_ic8 == true && @@ -1661,7 +1763,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); flywheelBike->deviceDiscovered(b); - this->startTemplateManagers(flywheelBike); + this->signalBluetoothDeviceConnected(flywheelBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("MCF-"))) && !mcfBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1675,7 +1777,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(mcfBike, SIGNAL(inclinationChanged(double)), this, // SLOT(inclinationChanged(double))); mcfBike->deviceDiscovered(b); - this->startTemplateManagers(mcfBike); + this->signalBluetoothDeviceConnected(mcfBike); } else if ((b.name().startsWith(QStringLiteral("TRX ROUTE KEY"))) && !toorx && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1685,8 +1787,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(toorx, SIGNAL(disconnected()), this, SLOT(restart())); connect(toorx, &toorxtreadmill::debug, this, &bluetooth::debug); toorx->deviceDiscovered(b); - this->startTemplateManagers(toorx); - } else if ((b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT"))) && !iConceptBike && filter) { + this->signalBluetoothDeviceConnected(toorx); + } else if ((b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT"))) && !iConceptBike && + !iconcept_elliptical && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); iConceptBike = new iconceptbike(); @@ -1696,7 +1799,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(toorx, SIGNAL(disconnected()), this, SLOT(restart())); connect(iConceptBike, &iconceptbike::debug, this, &bluetooth::debug); iConceptBike->deviceDiscovered(b); - this->startTemplateManagers(iConceptBike); + this->signalBluetoothDeviceConnected(iConceptBike); + } else if ((b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT"))) && !iConceptElliptical && + iconcept_elliptical && filter) { + this->setLastBluetoothDevice(b); + this->stopDiscovery(); + iConceptElliptical = + new iconceptelliptical(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + emit deviceConnected(b); + connect(iConceptElliptical, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(toorx, SIGNAL(disconnected()), this, SLOT(restart())); + connect(iConceptElliptical, &iconceptelliptical::debug, this, &bluetooth::debug); + iConceptElliptical->deviceDiscovered(b); + this->signalBluetoothDeviceConnected(iConceptElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("XT385")) || b.name().toUpper().startsWith(QStringLiteral("XT485")) || b.name().toUpper().startsWith(QStringLiteral("XT800")) || @@ -1712,7 +1828,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(spiritTreadmill, &spirittreadmill::debug, this, &bluetooth::debug); connect(spiritTreadmill, &spirittreadmill::inclinationChanged, this, &bluetooth::inclinationChanged); spiritTreadmill->deviceDiscovered(b); - this->startTemplateManagers(spiritTreadmill); + this->signalBluetoothDeviceConnected(spiritTreadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("RUNNERT")) && !activioTreadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1723,7 +1839,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(activioTreadmill, SIGNAL(disconnected()), this, SLOT(restart())); connect(activioTreadmill, &activiotreadmill::debug, this, &bluetooth::debug); activioTreadmill->deviceDiscovered(b); - this->startTemplateManagers(activioTreadmill); + this->signalBluetoothDeviceConnected(activioTreadmill); } else if (((b.name().startsWith(QStringLiteral("TOORX"))) || (b.name().startsWith(QStringLiteral("V-RUN"))) || (b.name().toUpper().startsWith(QStringLiteral("K80_"))) || @@ -1731,6 +1847,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { (b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) || (b.name().toUpper().startsWith(QStringLiteral("I-RUNNING"))) || (b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) || + (b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) || (b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) && !trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && filter) { @@ -1743,8 +1860,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart())); connect(trxappgateusb, &trxappgateusbtreadmill::debug, this, &bluetooth::debug); trxappgateusb->deviceDiscovered(b); - this->startTemplateManagers(trxappgateusb); + this->signalBluetoothDeviceConnected(trxappgateusb); } else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) || + b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) || ((b.name().startsWith(QStringLiteral("TOORX")) || b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) || b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) || @@ -1765,7 +1883,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart())); connect(trxappgateusbBike, &trxappgateusbbike::debug, this, &bluetooth::debug); trxappgateusbBike->deviceDiscovered(b); - this->startTemplateManagers(trxappgateusbBike); + this->signalBluetoothDeviceConnected(trxappgateusbBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("X-BIKE"))) && !ultraSportBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1777,7 +1895,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(ultraSportBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(ultraSportBike, &solebike::debug, this, &bluetooth::debug); ultraSportBike->deviceDiscovered(b); - this->startTemplateManagers(ultraSportBike); + this->signalBluetoothDeviceConnected(ultraSportBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("KEEP_BIKE_"))) && !keepBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1787,7 +1905,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(keepBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(keepBike, &solebike::debug, this, &bluetooth::debug); keepBike->deviceDiscovered(b); - this->startTemplateManagers(keepBike); + this->signalBluetoothDeviceConnected(keepBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("LCB")) || b.name().toUpper().startsWith(QStringLiteral("R92"))) && !soleBike && filter) { @@ -1799,8 +1917,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(soleBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(soleBike, &solebike::debug, this, &bluetooth::debug); soleBike->deviceDiscovered(b); - this->startTemplateManagers(soleBike); - } else if (b.name().toUpper().startsWith(QStringLiteral("BFCP")) && !skandikaWiriBike && filter) { + this->signalBluetoothDeviceConnected(soleBike); + } else if ((b.name().toUpper().startsWith(QStringLiteral("BFCP")) || + (b.name().toUpper().startsWith(QStringLiteral("HT")) && b.name().length() == 11)) && + !skandikaWiriBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); skandikaWiriBike = @@ -1811,8 +1931,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(skandikaWiriBike, SIGNAL(disconnected()), this, SLOT(restart())); connect(skandikaWiriBike, &skandikawiribike::debug, this, &bluetooth::debug); skandikaWiriBike->deviceDiscovered(b); - this->startTemplateManagers(skandikaWiriBike); + this->signalBluetoothDeviceConnected(skandikaWiriBike); } else if (((b.name().toUpper().startsWith("RQ") && b.name().length() == 5) || + (b.name().toUpper().startsWith("R-Q") && b.name().length() > 6) || (b.name().toUpper().startsWith("SCH130")) || // not a renpho bike an FTMS one ((b.name().startsWith(QStringLiteral("TOORX"))) && toorx_ftms && !toorx_ftms_treadmill)) && !renphoBike && !snodeBike && !fitPlusBike && filter) { @@ -1824,7 +1945,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart())); connect(renphoBike, SIGNAL(debug(QString)), this, SLOT(debug(QString))); renphoBike->deviceDiscovered(b); - this->startTemplateManagers(renphoBike); + this->signalBluetoothDeviceConnected(renphoBike); } else if ((b.name().toUpper().startsWith("PAFERS_")) && !pafersBike && !pafers_treadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1835,9 +1956,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(pafersBike, SIGNAL(disconnected()), this, SLOT(restart())); connect(pafersBike, SIGNAL(debug(QString)), this, SLOT(debug(QString))); pafersBike->deviceDiscovered(b); - this->startTemplateManagers(pafersBike); + this->signalBluetoothDeviceConnected(pafersBike); } else if (((b.name().startsWith(QStringLiteral("FS-")) && snode_bike) || - b.name().startsWith(QStringLiteral("TF-"))) && // TF-769DF2 + (b.name().toUpper().startsWith(QStringLiteral("TF-")) && + !horizon_treadmill_force_ftms)) && // TF-769DF2 !snodeBike && !ftmsBike && !fitPlusBike && filter) { this->setLastBluetoothDevice(b); @@ -1848,7 +1970,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(trxappgateusb, SIGNAL(disconnected()), this, SLOT(restart())); connect(snodeBike, &snodebike::debug, this, &bluetooth::debug); snodeBike->deviceDiscovered(b); - this->startTemplateManagers(snodeBike); + this->signalBluetoothDeviceConnected(snodeBike); } else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) || b.name().startsWith(QStringLiteral("MRK-"))) && !fitPlusBike && !ftmsBike && !snodeBike && filter) { @@ -1863,7 +1985,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // NOTE: Commented due to #358 // connect(fitPlusBike, SIGNAL(debug(QString)), this, SLOT(debug(QString))); fitPlusBike->deviceDiscovered(b); - this->startTemplateManagers(fitPlusBike); + this->signalBluetoothDeviceConnected(fitPlusBike); } else if (((b.name().startsWith(QStringLiteral("FS-")) && !snode_bike && !fitplus_bike && !ftmsBike) || b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) || // FTMS (b.name().startsWith(QStringLiteral("SW")) && b.name().length() == 14 && @@ -1882,7 +2004,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(this, &bluetooth::searchingStop, fitshowTreadmill, &fitshowtreadmill::searchingStop); if (this->discoveryAgent && !this->discoveryAgent->isActive()) emit searchingStop(); - this->startTemplateManagers(fitshowTreadmill); + this->signalBluetoothDeviceConnected(fitshowTreadmill); } else if (b.name().toUpper().startsWith(QStringLiteral("IC")) && b.name().length() == 8 && !inspireBike && filter) { this->setLastBluetoothDevice(b); @@ -1905,7 +2027,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(inspireBike); + this->signalBluetoothDeviceConnected(inspireBike); } else if (b.name().toUpper().startsWith(QStringLiteral("CHRONO ")) && !chronoBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1926,7 +2048,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (this->discoveryAgent && !this->discoveryAgent->isActive()) { emit searchingStop(); } - this->startTemplateManagers(chronoBike); + this->signalBluetoothDeviceConnected(chronoBike); } } } @@ -2202,10 +2324,12 @@ void bluetooth::connectedAndDiscovered() { QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;"); KeepAwakeHelper::antObject(true)->callMethod( - "antStart", "(Landroid/app/Activity;ZZZ)V", activity.object(), + "antStart", "(Landroid/app/Activity;ZZZZ)V", activity.object(), settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(), settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(), - settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool()); + settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(), + device()->deviceType() == bluetoothdevice::TREADMILL || + device()->deviceType() == bluetoothdevice::ELLIPTICAL); } if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) { @@ -2218,7 +2342,8 @@ void bluetooth::connectedAndDiscovered() { #ifdef Q_OS_ANDROID if (settings.value(QZSettings::peloton_workout_ocr, QZSettings::default_peloton_workout_ocr).toBool() || - settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool()) { + settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool() || + settings.value(QZSettings::zwift_ocr, QZSettings::default_zwift_ocr).toBool()) { AndroidActivityResultReceiver *a = new AndroidActivityResultReceiver(); QAndroidJniObject MediaProjectionManager = QtAndroid::androidActivity().callObjectMethod( "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", @@ -2229,6 +2354,22 @@ void bluetooth::connectedAndDiscovered() { } #endif +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + if (settings.value(QZSettings::garmin_companion, QZSettings::default_garmin_companion).toBool()) { +#ifdef Q_OS_ANDROID + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Garmin", "init", + "(Landroid/content/Context;)V", QtAndroid::androidContext().object()); +#else +#ifndef IO_UNDER_QT + if (!h) { + h = new lockscreen(); + h->garminconnect_init(); + } +#endif +#endif + } +#endif + #ifdef Q_OS_IOS // in order to allow to populate the tiles with the IC BIKE auto connect feature if (firstConnected) { @@ -2265,18 +2406,7 @@ void bluetooth::restart() { devices.clear(); - this->stopTemplateManagers(); - - if (device() && device()->VirtualDevice()) { - if (device()->deviceType() == bluetoothdevice::TREADMILL) { - - delete static_cast(device()->VirtualDevice()); - } else if (device()->deviceType() == bluetoothdevice::BIKE) { - delete static_cast(device()->VirtualDevice()); - } else if (device()->deviceType() == bluetoothdevice::ELLIPTICAL) { - delete static_cast(device()->VirtualDevice()); - } - } + emit this->bluetoothDeviceDisconnected(); if (domyos) { @@ -2354,11 +2484,20 @@ void bluetooth::restart() { delete apexBike; apexBike = nullptr; } + if (bkoolBike) { + delete bkoolBike; + bkoolBike = nullptr; + } if (domyosElliptical) { delete domyosElliptical; domyosElliptical = nullptr; } + if (ypooElliptical) { + + delete ypooElliptical; + ypooElliptical = nullptr; + } if (soleElliptical) { delete soleElliptical; @@ -2424,6 +2563,11 @@ void bluetooth::restart() { delete fakeElliptical; fakeElliptical = nullptr; } + if (fakeRower) { + + delete fakeRower; + fakeRower = nullptr; + } if (fakeTreadmill) { delete fakeTreadmill; @@ -2455,6 +2599,11 @@ void bluetooth::restart() { delete iConceptBike; iConceptBike = nullptr; } + if (iConceptElliptical) { + + delete iConceptElliptical; + iConceptElliptical = nullptr; + } if (trxappgateusb) { delete trxappgateusb; @@ -2515,6 +2664,11 @@ void bluetooth::restart() { delete octaneTreadmill; octaneTreadmill = nullptr; } + if (ziproTreadmill) { + + delete ziproTreadmill; + ziproTreadmill = nullptr; + } if (octaneElliptical) { delete octaneElliptical; @@ -2610,6 +2764,11 @@ void bluetooth::restart() { delete schwinnIC4Bike; schwinnIC4Bike = nullptr; } + if (schwinn170Bike) { + + delete schwinn170Bike; + schwinn170Bike = nullptr; + } if (sportsTechBike) { delete sportsTechBike; @@ -2636,6 +2795,11 @@ void bluetooth::restart() { delete computrainerBike; computrainerBike = nullptr; } + if (csafeRower) { + + delete csafeRower; + csafeRower = nullptr; + } #endif if (chronoBike) { @@ -2755,6 +2919,8 @@ bluetoothdevice *bluetooth::device() { return fitshowTreadmill; } else if (domyosElliptical) { return domyosElliptical; + } else if (ypooElliptical) { + return ypooElliptical; } else if (soleElliptical) { return soleElliptical; } else if (nautilusElliptical) { @@ -2781,6 +2947,8 @@ bluetoothdevice *bluetooth::device() { return fakeBike; } else if (fakeElliptical) { return fakeElliptical; + } else if (fakeRower) { + return fakeRower; } else if (fakeTreadmill) { return fakeTreadmill; } else if (npeCableBike) { @@ -2793,6 +2961,8 @@ bluetoothdevice *bluetooth::device() { return toorx; } else if (iConceptBike) { return iConceptBike; + } else if (iConceptElliptical) { + return iConceptElliptical; } else if (spiritTreadmill) { return spiritTreadmill; } else if (activioTreadmill) { @@ -2807,6 +2977,8 @@ bluetoothdevice *bluetooth::device() { return keepBike; } else if (apexBike) { return apexBike; + } else if (bkoolBike) { + return bkoolBike; } else if (ultraSportBike) { return ultraSportBike; } else if (horizonTreadmill) { @@ -2839,6 +3011,8 @@ bluetoothdevice *bluetooth::device() { return echelonStride; } else if (octaneTreadmill) { return octaneTreadmill; + } else if (ziproTreadmill) { + return ziproTreadmill; } else if (octaneElliptical) { return octaneElliptical; } else if (ftmsRower) { @@ -2877,6 +3051,8 @@ bluetoothdevice *bluetooth::device() { return mcfBike; } else if (schwinnIC4Bike) { return schwinnIC4Bike; + } else if (schwinn170Bike) { + return schwinn170Bike; } else if (sportsTechBike) { return sportsTechBike; } else if (sportsPlusBike) { @@ -2908,6 +3084,8 @@ bluetoothdevice *bluetooth::device() { #ifndef Q_OS_IOS } else if (computrainerBike) { return computrainerBike; + } else if (csafeRower) { + return csafeRower; #endif } return nullptr; diff --git a/src/bluetooth.h b/src/bluetooth.h index 4b73eb775..751d7fa49 100644 --- a/src/bluetooth.h +++ b/src/bluetooth.h @@ -23,12 +23,14 @@ #include "activiotreadmill.h" #include "apexbike.h" #include "bhfitnesselliptical.h" +#include "bkoolbike.h" #include "bluetoothdevice.h" #include "bowflext216treadmill.h" #include "bowflextreadmill.h" #include "chronobike.h" #ifndef Q_OS_IOS #include "computrainerbike.h" +#include "csaferower.h" #endif #include "concept2skierg.h" #include "cscbike.h" @@ -44,6 +46,7 @@ #include "eslinkertreadmill.h" #include "fakebike.h" #include "fakeelliptical.h" +#include "fakerower.h" #include "faketreadmill.h" #include "fitmetria_fanfit.h" #include "fitplusbike.h" @@ -56,6 +59,7 @@ #include "horizongr7bike.h" #include "horizontreadmill.h" #include "iconceptbike.h" +#include "iconceptelliptical.h" #include "inspirebike.h" #include "keepbike.h" #include "kingsmithr1protreadmill.h" @@ -83,6 +87,7 @@ #include "proformtreadmill.h" #include "proformwifibike.h" #include "proformwifitreadmill.h" +#include "schwinn170bike.h" #include "schwinnic4bike.h" #include "signalhandler.h" #include "skandikawiribike.h" @@ -118,6 +123,12 @@ #include "wahookickrheadwind.h" #include "wahookickrsnapbike.h" #include "yesoulbike.h" +#include "ypooelliptical.h" +#include "ziprotreadmill.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif class bluetooth : public QObject, public SignalHandler { @@ -127,30 +138,27 @@ class bluetooth : public QObject, public SignalHandler { explicit bluetooth(bool logs, const QString &deviceName = QLatin1String(""), bool noWriteResistance = false, bool noHeartService = false, uint32_t pollDeviceTime = 200, bool noConsole = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0, - bool createTemplateManagers = true, bool startDiscovery = true); + bool startDiscovery = true); ~bluetooth(); bluetoothdevice *device(); bluetoothdevice *externalInclination() { return eliteRizer; } bluetoothdevice *heartRateDevice() { return heartRateBelt; } QList devices; bool onlyDiscover = false; - TemplateInfoSenderBuilder *getUserTemplateManager() const { return userTemplateManager; } - TemplateInfoSenderBuilder *getInnerTemplateManager() const { return innerTemplateManager; } private: bool useDiscovery = false; - bool createTemplateManagers = false; - TemplateInfoSenderBuilder *userTemplateManager = nullptr; - TemplateInfoSenderBuilder *innerTemplateManager = nullptr; QFile *debugCommsLog = nullptr; QBluetoothDeviceDiscoveryAgent *discoveryAgent = nullptr; apexbike *apexBike = nullptr; + bkoolbike *bkoolBike = nullptr; bhfitnesselliptical *bhFitnessElliptical = nullptr; bowflextreadmill *bowflexTreadmill = nullptr; bowflext216treadmill *bowflexT216Treadmill = nullptr; fitshowtreadmill *fitshowTreadmill = nullptr; #ifndef Q_OS_IOS computrainerbike *computrainerBike = nullptr; + csaferower *csafeRower = nullptr; #endif concept2skierg *concept2Skierg = nullptr; domyostreadmill *domyos = nullptr; @@ -159,6 +167,7 @@ class bluetooth : public QObject, public SignalHandler { domyoselliptical *domyosElliptical = nullptr; toorxtreadmill *toorx = nullptr; iconceptbike *iConceptBike = nullptr; + iconceptelliptical *iConceptElliptical = nullptr; trxappgateusbtreadmill *trxappgateusb = nullptr; spirittreadmill *spiritTreadmill = nullptr; activiotreadmill *activioTreadmill = nullptr; @@ -205,6 +214,7 @@ class bluetooth : public QObject, public SignalHandler { solebike *soleBike = nullptr; soleelliptical *soleElliptical = nullptr; solef80treadmill *soleF80 = nullptr; + schwinn170bike *schwinn170Bike = nullptr; chronobike *chronoBike = nullptr; fitplusbike *fitPlusBike = nullptr; echelonrower *echelonRower = nullptr; @@ -229,11 +239,14 @@ class bluetooth : public QObject, public SignalHandler { stagesbike *powerBike = nullptr; ultrasportbike *ultraSportBike = nullptr; wahookickrsnapbike *wahooKickrSnapBike = nullptr; + ypooelliptical *ypooElliptical = nullptr; + ziprotreadmill *ziproTreadmill = nullptr; strydrunpowersensor *powerTreadmill = nullptr; eliterizer *eliteRizer = nullptr; elitesterzosmart *eliteSterzoSmart = nullptr; fakebike *fakeBike = nullptr; fakeelliptical *fakeElliptical = nullptr; + fakerower *fakeRower = nullptr; faketreadmill *fakeTreadmill = nullptr; QList fitmetriaFanfit; QList wahookickrHeadWind; @@ -275,19 +288,24 @@ class bluetooth : public QObject, public SignalHandler { QTimer discoveryTimeout; #endif +#ifdef Q_OS_IOS + lockscreen *h = nullptr; +#endif + /** * @brief Store the name and other info in the settings. * @param b The bluetooth device info. */ void setLastBluetoothDevice(const QBluetoothDeviceInfo &b); - void startTemplateManagers(bluetoothdevice *b); - void stopTemplateManagers(); + void signalBluetoothDeviceConnected(bluetoothdevice *b); signals: void deviceConnected(QBluetoothDeviceInfo b); void deviceFound(QString name); void searchingStop(); void ftmsAccessoryConnected(smartspin2k *d); + void bluetoothDeviceConnected(bluetoothdevice *b); + void bluetoothDeviceDisconnected(); public slots: void restart(); void debug(const QString &string); diff --git a/src/bluetoothdevice.cpp b/src/bluetoothdevice.cpp index 827b09970..1f331654e 100644 --- a/src/bluetoothdevice.cpp +++ b/src/bluetoothdevice.cpp @@ -4,8 +4,22 @@ #include #include +#ifdef Q_OS_ANDROID +#include +#endif +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + bluetoothdevice::bluetoothdevice() {} +bluetoothdevice::~bluetoothdevice() { + if(this->virtualDevice) { + delete this->virtualDevice; + this->virtualDevice = nullptr; + } +} + bluetoothdevice::BLUETOOTH_TYPE bluetoothdevice::deviceType() { return bluetoothdevice::UNKNOWN; } void bluetoothdevice::start() { requestStart = 1; } void bluetoothdevice::stop(bool pause) { @@ -34,6 +48,8 @@ metric bluetoothdevice::currentResistance() { return Resistance; } metric bluetoothdevice::currentCadence() { return Cadence; } double bluetoothdevice::currentCrankRevolutions() { return 0; } uint16_t bluetoothdevice::lastCrankEventTime() { return 0; } + +virtualdevice *bluetoothdevice::VirtualDevice() { return this->virtualDevice; } void bluetoothdevice::changeResistance(resistance_t resistance) {} void bluetoothdevice::changePower(int32_t power) {} void bluetoothdevice::changeInclination(double grade, double percentage) {} @@ -94,7 +110,6 @@ double bluetoothdevice::odometer() { return Distance.value(); } metric bluetoothdevice::calories() { return KCal; } metric bluetoothdevice::jouls() { return m_jouls; } uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; }; -void *bluetoothdevice::VirtualDevice() { return nullptr; } bool bluetoothdevice::changeFanSpeed(uint8_t speed) { // managing underflow if (speed > 230 && FanSpeed < 20) { @@ -137,14 +152,29 @@ void bluetoothdevice::instantaneousStrideLengthSensor(double length) { Q_UNUSED( void bluetoothdevice::groundContactSensor(double groundContact) { Q_UNUSED(groundContact); } void bluetoothdevice::verticalOscillationSensor(double verticalOscillation) { Q_UNUSED(verticalOscillation); } +bool bluetoothdevice::hasVirtualDevice() { return this->virtualDevice!=nullptr; } + double bluetoothdevice::calculateMETS() { return ((0.048 * m_watt.value()) + 1.19); } +void bluetoothdevice::setVirtualDevice(virtualdevice *virtualDevice, VIRTUAL_DEVICE_MODE mode) { + + if(mode!=VIRTUAL_DEVICE_MODE::NONE && !virtualDevice) + throw "Virtual device mode should be NONE when no virtual device is specified."; + + if(this->virtualDevice) + delete this->virtualDevice; + this->virtualDevice = virtualDevice; + this->virtualDeviceMode=mode; +} + // keiser m3i has a separate management of this, so please check it void bluetoothdevice::update_metrics(bool watt_calc, const double watts) { QDateTime current = QDateTime::currentDateTime(); double deltaTime = (((double)_lastTimeUpdate.msecsTo(current)) / ((double)1000.0)); QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); bool power_as_bike = settings.value(QZSettings::power_sensor_as_bike, QZSettings::default_power_sensor_as_bike).toBool(); bool power_as_treadmill = @@ -197,6 +227,39 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts) { _firstUpdate = false; } +void bluetoothdevice::update_hr_from_external() { + QSettings settings; + if(settings.value(QZSettings::garmin_companion, QZSettings::default_garmin_companion).toBool()) { +#ifdef Q_OS_ANDROID + Heart = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Garmin", "getHR", "()I"); +#endif +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + lockscreen h; + Heart = h.getHR(); +#endif +#endif + qDebug() << "Garmin Companion Heart:" << Heart.value(); + } else { +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + lockscreen h; + long appleWatchHeartRate = h.heartRate(); + h.setKcal(KCal.value()); + h.setDistance(Distance.value()); + h.setSpeed(Speed.value()); + h.setPower(m_watt.value()); + h.setCadence(Cadence.value()); + Heart = appleWatchHeartRate; + qDebug() << "Current Heart from Apple Watch: " << QString::number(appleWatchHeartRate); +#endif +#endif +#ifdef Q_OS_ANDROID + Heart = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/WearableController", "getHeart", "()I"); +#endif + } +} + void bluetoothdevice::clearStats() { elapsed.clear(true); diff --git a/src/bluetoothdevice.h b/src/bluetoothdevice.h index 82f174dbf..b10e72dcc 100644 --- a/src/bluetoothdevice.h +++ b/src/bluetoothdevice.h @@ -22,6 +22,8 @@ #include #include +#include "virtualdevice.h" + #if defined(Q_OS_IOS) #define SAME_BLUETOOTH_DEVICE(d1, d2) (d1.deviceUuid() == d2.deviceUuid()) #else @@ -48,8 +50,12 @@ class MetersByInclination { class bluetoothdevice : public QObject { Q_OBJECT + public: bluetoothdevice(); + + ~bluetoothdevice() override; + /** * @brief currentHeart Gets a metric object for getting and setting the current heart rate. Units: beats per minute */ @@ -192,7 +198,7 @@ class bluetoothdevice : public QObject { /** * @brief VirtualDevice The virtual bridge to Zwift for example, or to any 3rd party app. */ - virtual void *VirtualDevice(); + virtualdevice *VirtualDevice(); /** * @brief watts Calculates the amount of power used. Units: watts @@ -432,6 +438,32 @@ class bluetoothdevice : public QObject { void verticalOscillationChanged(double verticalOscillation); protected: + /** + * @brief Mode of operation for the virtual device with the bluetoothdevice object. + */ + enum VIRTUAL_DEVICE_MODE { + + /** + * @brief Not set. + */ + NONE, + /** + * @brief Virtual device represents the same type of device. + */ + PRIMARY, + + /** + * @brief Virtual device representing the device for a purpose other than the + * type of device it matches. + */ + ALTERNATIVE + }; + + /** + * @brief hasVirtualDevice shows if the object has any virtual device assigned. + */ + bool hasVirtualDevice(); + QLowEnergyController *m_control = nullptr; /** @@ -633,11 +665,37 @@ class bluetoothdevice : public QObject { */ void update_metrics(bool watt_calc, const double watts); + /** + * @brief update_hr_from_external Updates heart rate from Garmin Companion App or Apple Watch + */ + void update_hr_from_external(); + /** * @brief calculateMETS Calculate the METS (Metabolic Equivalent of Tasks) * Units: METs (1 MET is approximately 3.5mL of Oxygen consumed per kg of body weight per minute) */ double calculateMETS(); + + /** + * @brief setVirtualDevice Set the virtual device, and the way it is being used. Deletes the existing one, if + * present. + * @param virtualDevice The virtual device. + */ + void setVirtualDevice(virtualdevice *virtualDevice, VIRTUAL_DEVICE_MODE mode); + + /** + * @brief writeBuffer contains the last byte array request via bluetooth to the fitness devices + */ + QByteArray *writeBuffer = nullptr; + + private: + /** + * @brief Indicates the way the virtual device is being used. + * Normally PRIMARY, set this to ALTERNATIVE where the device is being used unusually, e.g. + * for the Zwift Auto-Inclination Workaround. + */ + VIRTUAL_DEVICE_MODE virtualDeviceMode = VIRTUAL_DEVICE_MODE::NONE; + virtualdevice *virtualDevice = nullptr; }; #endif // BLUETOOTHDEVICE_H diff --git a/src/bowflext216treadmill.cpp b/src/bowflext216treadmill.cpp index 5ea9c7620..bb48fca3d 100644 --- a/src/bowflext216treadmill.cpp +++ b/src/bowflext216treadmill.cpp @@ -1,5 +1,7 @@ #include "bowflext216treadmill.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include @@ -51,11 +53,15 @@ void bowflext216treadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, // &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -92,13 +98,14 @@ void bowflext216treadmill::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &bowflext216treadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -378,13 +385,17 @@ void bowflext216treadmill::serviceScanDone(void) { QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("edff9e80-cad7-11e5-ab63-0002a5d5c51b")); gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); if (gattCommunicationChannelService == nullptr) { - qDebug() << "trying with the BOWFLEX T6 treadmill"; - bowflex_t6 = true; - QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("15B7BF49-1693-481E-B877-69D33CE6BAFA")); + QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("B1216C2E-464E-4C46-B2B4-0B5C8EB23DAE")); gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); if (gattCommunicationChannelService == nullptr) { - qDebug() << "WRONG SERVICE"; - return; + qDebug() << "trying with the BOWFLEX T6 treadmill"; + bowflex_t6 = true; + QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("15B7BF49-1693-481E-B877-69D33CE6BAFA")); + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); + if (gattCommunicationChannelService == nullptr) { + qDebug() << "WRONG SERVICE"; + return; + } } } connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, @@ -461,10 +472,6 @@ bool bowflext216treadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *bowflext216treadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *bowflext216treadmill::VirtualDevice() { return VirtualTreadMill(); } - bool bowflext216treadmill::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/bowflext216treadmill.h b/src/bowflext216treadmill.h index d4da552ae..e407d3074 100644 --- a/src/bowflext216treadmill.h +++ b/src/bowflext216treadmill.h @@ -26,21 +26,16 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class bowflext216treadmill : public treadmill { Q_OBJECT public: bowflext216treadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadMill(); - void *VirtualDevice(); - + bool connected() override; + double minStepInclination() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; private: double GetSpeedFromPacket(const QByteArray &packet); double GetInclinationFromPacket(const QByteArray &packet); @@ -66,7 +61,6 @@ class bowflext216treadmill : public treadmill { int64_t lastStop = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/bowflextreadmill.cpp b/src/bowflextreadmill.cpp index 3026be7ce..a4b5da11e 100644 --- a/src/bowflextreadmill.cpp +++ b/src/bowflextreadmill.cpp @@ -53,11 +53,15 @@ void bowflextreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, cons // &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -94,13 +98,14 @@ void bowflextreadmill::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &bowflextreadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -415,10 +420,6 @@ bool bowflextreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *bowflextreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *bowflextreadmill::VirtualDevice() { return VirtualTreadMill(); } - bool bowflextreadmill::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/bowflextreadmill.h b/src/bowflextreadmill.h index 9313adde5..cd7d7b4a2 100644 --- a/src/bowflextreadmill.h +++ b/src/bowflextreadmill.h @@ -27,20 +27,16 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class bowflextreadmill : public treadmill { Q_OBJECT public: bowflextreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadMill(); - void *VirtualDevice(); + bool connected() override; + double minStepInclination() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -67,7 +63,6 @@ class bowflextreadmill : public treadmill { int64_t lastStop = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/characteristicnotifier2a37.h b/src/characteristicnotifier2a37.h index e5f64ddac..c159209d1 100644 --- a/src/characteristicnotifier2a37.h +++ b/src/characteristicnotifier2a37.h @@ -9,7 +9,7 @@ class CharacteristicNotifier2A37 : public CharacteristicNotifier { public: explicit CharacteristicNotifier2A37(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIER2A37_H diff --git a/src/characteristicnotifier2a53.cpp b/src/characteristicnotifier2a53.cpp index e384a8aec..676b8c7ab 100644 --- a/src/characteristicnotifier2a53.cpp +++ b/src/characteristicnotifier2a53.cpp @@ -6,18 +6,15 @@ CharacteristicNotifier2A53::CharacteristicNotifier2A53(bluetoothdevice *Bike, QO int CharacteristicNotifier2A53::notify(QByteArray &value) { bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType(); - if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) { - value.append(0x02); // total distance - uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256; - uint32_t distance = Bike->odometer() * 10000.0; - value.append((char)((speed & 0xFF))); - value.append((char)((speed >> 8) & 0xFF)); - value.append((char)(Bike->currentCadence().value())); - value.append((char)((distance & 0xFF))); - value.append((char)((distance >> 8) & 0xFF)); - value.append((char)((distance >> 16) & 0xFF)); - value.append((char)((distance >> 24) & 0xFF)); - return CN_OK; - } else - return CN_INVALID; + value.append(0x02); // total distance + uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256; + uint32_t distance = Bike->odometer() * 10000.0; + value.append((char)((speed & 0xFF))); + value.append((char)((speed >> 8) & 0xFF)); + value.append((char)(Bike->currentCadence().value())); + value.append((char)((distance & 0xFF))); + value.append((char)((distance >> 8) & 0xFF)); + value.append((char)((distance >> 16) & 0xFF)); + value.append((char)((distance >> 24) & 0xFF)); + return CN_OK; } diff --git a/src/characteristicnotifier2a53.h b/src/characteristicnotifier2a53.h index 8a1b7565c..45b1c2f5c 100644 --- a/src/characteristicnotifier2a53.h +++ b/src/characteristicnotifier2a53.h @@ -9,7 +9,7 @@ class CharacteristicNotifier2A53 : public CharacteristicNotifier { public: explicit CharacteristicNotifier2A53(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIER2A53_H diff --git a/src/characteristicnotifier2a5b.h b/src/characteristicnotifier2a5b.h index d0610a451..7e6802a12 100644 --- a/src/characteristicnotifier2a5b.h +++ b/src/characteristicnotifier2a5b.h @@ -12,7 +12,7 @@ class CharacteristicNotifier2A5B : public CharacteristicNotifier { public: explicit CharacteristicNotifier2A5B(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIER2A5B_H diff --git a/src/characteristicnotifier2a63.h b/src/characteristicnotifier2a63.h index e8c836e04..aff7a3e4a 100644 --- a/src/characteristicnotifier2a63.h +++ b/src/characteristicnotifier2a63.h @@ -9,7 +9,7 @@ class CharacteristicNotifier2A63 : public CharacteristicNotifier { public: explicit CharacteristicNotifier2A63(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIER2A63_H diff --git a/src/characteristicnotifier2acc.h b/src/characteristicnotifier2acc.h index d5e8e362c..a35460de3 100644 --- a/src/characteristicnotifier2acc.h +++ b/src/characteristicnotifier2acc.h @@ -9,7 +9,7 @@ class CharacteristicNotifier2ACC : public CharacteristicNotifier { public: explicit CharacteristicNotifier2ACC(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIES2ACC_H diff --git a/src/characteristicnotifier2acd.h b/src/characteristicnotifier2acd.h index a14fe786e..cb674fe1b 100644 --- a/src/characteristicnotifier2acd.h +++ b/src/characteristicnotifier2acd.h @@ -8,7 +8,7 @@ class CharacteristicNotifier2ACD : public CharacteristicNotifier { public: explicit CharacteristicNotifier2ACD(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIER2ACD_H diff --git a/src/characteristicnotifier2ad2.cpp b/src/characteristicnotifier2ad2.cpp index 7e9d933b4..c326073f8 100644 --- a/src/characteristicnotifier2ad2.cpp +++ b/src/characteristicnotifier2ad2.cpp @@ -1,5 +1,6 @@ #include "characteristicnotifier2ad2.h" #include "elliptical.h" +#include "rower.h" #include "treadmill.h" #include @@ -8,11 +9,17 @@ CharacteristicNotifier2AD2::CharacteristicNotifier2AD2(bluetoothdevice *Bike, QO int CharacteristicNotifier2AD2::notify(QByteArray &value) { bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType(); + + QSettings settings; + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); + bool rowerAsABike = !virtual_device_rower && dt == bluetoothdevice::ROWING; + double normalizeWattage = Bike->wattsMetric().value(); if (normalizeWattage < 0) normalizeWattage = 0; - if (dt == bluetoothdevice::BIKE) { + if (dt == bluetoothdevice::BIKE || rowerAsABike) { uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100); value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power value.append((char)0x02); // heart rate @@ -32,7 +39,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) { value.append(char(Bike->currentHeart().value())); // Actual value. value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix return CN_OK; - } else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) { + } else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL || dt == bluetoothdevice::ROWING) { QSettings settings; bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool(); double cadence_multiplier = 2.0; @@ -50,6 +57,8 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) { cadence = ((elliptical *)Bike)->currentCadence().value(); else if (dt == bluetoothdevice::TREADMILL) cadence = ((treadmill *)Bike)->currentCadence().value(); + else if (dt == bluetoothdevice::ROWING) + cadence = ((rower *)Bike)->currentCadence().value(); value.append((char)((uint16_t)(cadence * cadence_multiplier) & 0xFF)); // cadence value.append((char)(((uint16_t)(cadence * cadence_multiplier) >> 8) & 0xFF)); // cadence diff --git a/src/characteristicnotifier2ad2.h b/src/characteristicnotifier2ad2.h index f30e351f0..d24670c81 100644 --- a/src/characteristicnotifier2ad2.h +++ b/src/characteristicnotifier2ad2.h @@ -9,7 +9,7 @@ class CharacteristicNotifier2AD2 : public CharacteristicNotifier { public: explicit CharacteristicNotifier2AD2(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; }; #endif // CHARACTERISTICNOTIFIER2AD2_H diff --git a/src/characteristicnotifier2ad9.h b/src/characteristicnotifier2ad9.h index 31da81c91..adacd59f8 100644 --- a/src/characteristicnotifier2ad9.h +++ b/src/characteristicnotifier2ad9.h @@ -9,7 +9,7 @@ class CharacteristicNotifier2AD9 : public CharacteristicNotifier { public: explicit CharacteristicNotifier2AD9(bluetoothdevice *Bike, QObject *parent = nullptr); - virtual int notify(QByteArray &out); + int notify(QByteArray &out) override; QByteArray answer; }; diff --git a/src/characteristicwriteprocessor.cpp b/src/characteristicwriteprocessor.cpp index b34eab7f1..796f27cff 100644 --- a/src/characteristicwriteprocessor.cpp +++ b/src/characteristicwriteprocessor.cpp @@ -1,3 +1,5 @@ +#include "bike.h" +#include "elliptical.h" #include "characteristicwriteprocessor.h" #include diff --git a/src/characteristicwriteprocessor.h b/src/characteristicwriteprocessor.h index fd2e6fd78..d00fcbebf 100644 --- a/src/characteristicwriteprocessor.h +++ b/src/characteristicwriteprocessor.h @@ -1,10 +1,7 @@ #ifndef CHARACTERISTICWRITEPROCESSOR_H #define CHARACTERISTICWRITEPROCESSOR_H -#include "bike.h" #include "bluetoothdevice.h" -#include "elliptical.h" -#include "treadmill.h" #include #include #include diff --git a/src/characteristicwriteprocessor2ad9.h b/src/characteristicwriteprocessor2ad9.h index 2ce24ef3f..69e540ce9 100644 --- a/src/characteristicwriteprocessor2ad9.h +++ b/src/characteristicwriteprocessor2ad9.h @@ -12,7 +12,7 @@ class CharacteristicWriteProcessor2AD9 : public CharacteristicWriteProcessor { explicit CharacteristicWriteProcessor2AD9(double bikeResistanceGain, uint8_t bikeResistanceOffset, bluetoothdevice *bike, CharacteristicNotifier2AD9 *notifier, QObject *parent = nullptr); - virtual int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out); + int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out) override; signals: void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); }; diff --git a/src/characteristicwriteprocessore005.cpp b/src/characteristicwriteprocessore005.cpp index 7467d38f5..631eb71f1 100644 --- a/src/characteristicwriteprocessore005.cpp +++ b/src/characteristicwriteprocessore005.cpp @@ -21,7 +21,7 @@ int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArra weight = ((double)((uint16_t)data.at(1)) + (((uint16_t)data.at(2)) >> 8)) / 100.0; rrc = ((double)((uint16_t)data.at(3)) + (((uint16_t)data.at(4)) >> 8)) / 1000.0; wrc = ((double)((uint16_t)data.at(5)) + (((uint16_t)data.at(6)) >> 8)) / 1000.0; - qDebug() << "weigth" << weight << "rrc" << rrc << "wrc" << wrc; + qDebug() << "weight" << weight << "rrc" << rrc << "wrc" << wrc; } else if (cmd == wahookickrsnapbike::_setSimGrade && data.count() >= 3) { uint16_t grade; double fgrade; diff --git a/src/characteristicwriteprocessore005.h b/src/characteristicwriteprocessore005.h index 973aad847..10850fd16 100644 --- a/src/characteristicwriteprocessore005.h +++ b/src/characteristicwriteprocessore005.h @@ -2,7 +2,6 @@ #define CHARACTERISTICWRITEPROCESSORE005_H #include "bluetoothdevice.h" -#include "characteristicnotifier2ad9.h" #include "characteristicwriteprocessor.h" class CharacteristicWriteProcessorE005 : public CharacteristicWriteProcessor { @@ -12,7 +11,7 @@ class CharacteristicWriteProcessorE005 : public CharacteristicWriteProcessor { explicit CharacteristicWriteProcessorE005(double bikeResistanceGain, uint8_t bikeResistanceOffset, bluetoothdevice *bike, // CharacteristicNotifier2AD9 *notifier, QObject *parent = nullptr); - virtual int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out); + int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out) override; private: double weight, rrc, wrc; diff --git a/src/chronobike.cpp b/src/chronobike.cpp index 66c109667..c93c7016e 100644 --- a/src/chronobike.cpp +++ b/src/chronobike.cpp @@ -1,6 +1,7 @@ #include "chronobike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -198,16 +199,7 @@ void chronobike::characteristicChanged(const QLowEnergyCharacteristic &character #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -264,7 +256,7 @@ void chronobike::stateChanged(QLowEnergyService::ServiceState state) { &chronobike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -289,9 +281,10 @@ void chronobike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); connect(virtualBike, &virtualbike::changeInclination, this, &chronobike::changeInclination); // connect(virtualBike,&virtualbike::debug ,this,&chronobike::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -383,10 +376,6 @@ bool chronobike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *chronobike::VirtualBike() { return virtualBike; } - -void *chronobike::VirtualDevice() { return VirtualBike(); } - uint16_t chronobike::watts() { QSettings settings; if (currentCadence().value() == 0) { diff --git a/src/chronobike.h b/src/chronobike.h index a8732f5e7..4a93d5536 100644 --- a/src/chronobike.h +++ b/src/chronobike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,20 +36,16 @@ class chronobike : public bike { Q_OBJECT public: chronobike(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: // void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, // Unused // bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; QTimer *t_timeout; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattNotify1Characteristic; diff --git a/src/computrainerbike.cpp b/src/computrainerbike.cpp index ade407f00..e7553862a 100644 --- a/src/computrainerbike.cpp +++ b/src/computrainerbike.cpp @@ -39,7 +39,7 @@ computrainerbike::computrainerbike(bool noWriteResistance, bool noHeartService, initRequest = true; // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -64,10 +64,10 @@ computrainerbike::computrainerbike(bool noWriteResistance, bool noHeartService, #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = - new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,& computrainerbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &computrainerbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -282,16 +282,7 @@ void computrainerbike::update() { #endif { if (disable_hr_frommachinery && heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -411,8 +402,4 @@ void computrainerbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { bool computrainerbike::connected() { return true; } -void *computrainerbike::VirtualBike() { return virtualBike; } - -void *computrainerbike::VirtualDevice() { return VirtualBike(); } - uint16_t computrainerbike::watts() { return m_watt.value(); } diff --git a/src/computrainerbike.h b/src/computrainerbike.h index 5fdbcaad4..b59509dd7 100644 --- a/src/computrainerbike.h +++ b/src/computrainerbike.h @@ -34,14 +34,11 @@ class computrainerbike : public bike { public: computrainerbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool inclinationAvailableByHardware(); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool inclinationAvailableByHardware() override; + bool connected() override; private: resistance_t max_resistance = 100; @@ -52,7 +49,7 @@ class computrainerbike : public bike { void btinit(); void startDiscover(); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(double requestResistance); void innerWriteResistance(); diff --git a/src/concept2skierg.cpp b/src/concept2skierg.cpp index b6bcbdb0d..18097260c 100644 --- a/src/concept2skierg.cpp +++ b/src/concept2skierg.cpp @@ -1,6 +1,5 @@ #include "concept2skierg.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualtreadmill.h" #include #include @@ -12,9 +11,10 @@ #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -42,10 +42,15 @@ void concept2skierg::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -140,87 +145,90 @@ void concept2skierg::characteristicChanged(const QLowEnergyCharacteristic &chara // ce060080 multiplexes ce06003x characteristics // warning: data packets are not exactly the same as separate characteristics - if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060080-43e5-11e4-916c-0800200c9a66")) && newValue.length() > 0) { - switch(newValue.at(0)) { - case 0x31: - qDebug() << "31"; - if (newValue.length() >= 20) { - uint32_t distance_dm = ((((uint32_t)newValue.at(6)) << 16) | ((uint32_t)((uint16_t)newValue.at(5)) << 8) | - (uint32_t)((uint8_t)newValue.at(4))); - Distance = distance_dm / 10000.0; - emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); - - uint8_t rowing_state = newValue.at(10); - isActive = (rowing_state != 0); - if (!isActive) // SkiErg keeps reporting old Speed when not used - { - Speed = 0; - Cadence = 0; - //m_watt = 0; - qDebug() << "Device inactive. Zeroing speed, cadence, and watts."; - } - uint8_t drag_factor = newValue.at(19); - Resistance = drag_factor; + if (characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060080-43e5-11e4-916c-0800200c9a66")) && + newValue.length() > 0) { + switch (newValue.at(0)) { + case 0x31: + qDebug() << "31"; + if (newValue.length() >= 20) { + uint32_t distance_dm = + ((((uint32_t)newValue.at(6)) << 16) | ((uint32_t)((uint16_t)newValue.at(5)) << 8) | + (uint32_t)((uint8_t)newValue.at(4))); + Distance = distance_dm / 10000.0; + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + + uint8_t rowing_state = newValue.at(10); + isActive = (rowing_state != 0); + if (!isActive) // SkiErg keeps reporting old Speed when not used + { + Speed = 0; + Cadence = 0; + // m_watt = 0; + qDebug() << "Device inactive. Zeroing speed, cadence, and watts."; + } + uint8_t drag_factor = newValue.at(19); + Resistance = drag_factor; + } + break; + case 0x32: + qDebug() << "32"; + if (newValue.length() >= 20) { + // 0.001 m/s + uint16_t speed_ms = (((uint16_t)((uint16_t)newValue.at(5)) << 8) | (uint16_t)((uint8_t)newValue.at(4))); + uint8_t stroke_rate = newValue.at(6); + if (isActive) { + Speed = speed_ms * 0.0036; + Cadence = stroke_rate; + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); } - break; - case 0x32: - qDebug() << "32"; - if (newValue.length() >= 20) { - // 0.001 m/s - uint16_t speed_ms = (((uint16_t)((uint16_t)newValue.at(5)) << 8) | (uint16_t)((uint8_t)newValue.at(4))); - uint8_t stroke_rate = newValue.at(6); - if (isActive) - { - Speed = speed_ms * 0.0036; - Cadence = stroke_rate; - emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); - emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); - } - uint8_t heart_rate = newValue.at(7); + uint8_t heart_rate = newValue.at(7); #ifdef Q_OS_ANDROID - if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) - Heart = (uint8_t)KeepAwakeHelper::heart(); - else + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else #endif - { - if (heart_rate != 0xFF) - Heart = heart_rate; - emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); - } - } - break; - case 0x33: - qDebug() << "33"; - if (newValue.length() >= 19) { - uint16_t total_calories = (((uint16_t)((uint16_t)newValue.at(6)) << 8) | (uint16_t)((uint8_t)newValue.at(5))); - KCal = total_calories; - emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); - } - break; - case 0x35: - qDebug() << "35"; - if (newValue.length() >= 19) { - uint16_t stroke_count = (((uint16_t)((uint16_t)newValue.at(18)) << 8) | (uint16_t)((uint8_t)newValue.at(17))); - StrokesCount = stroke_count; - emit debug(QStringLiteral("Strokes Count: ") + QString::number(StrokesCount.value())); + { + if (heart_rate != 0xFF) + Heart = heart_rate; + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } - break; - case 0x36: - qDebug() << "36"; - if (newValue.length() >= 16) { - uint16_t stroke_power = (((uint16_t)((uint16_t)newValue.at(5)) << 8) | (uint16_t)((uint8_t)newValue.at(4))); - if (isActive) - { - m_watt = stroke_power; - emit debug(QStringLiteral("Current Watts: ") + QString::number(m_watt.value())); - } + } + break; + case 0x33: + qDebug() << "33"; + if (newValue.length() >= 19) { + uint16_t total_calories = + (((uint16_t)((uint16_t)newValue.at(6)) << 8) | (uint16_t)((uint8_t)newValue.at(5))); + KCal = total_calories; + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + } + break; + case 0x35: + qDebug() << "35"; + if (newValue.length() >= 19) { + uint16_t stroke_count = + (((uint16_t)((uint16_t)newValue.at(18)) << 8) | (uint16_t)((uint8_t)newValue.at(17))); + StrokesCount = stroke_count; + emit debug(QStringLiteral("Strokes Count: ") + QString::number(StrokesCount.value())); + } + break; + case 0x36: + qDebug() << "36"; + if (newValue.length() >= 16) { + uint16_t stroke_power = + (((uint16_t)((uint16_t)newValue.at(5)) << 8) | (uint16_t)((uint8_t)newValue.at(4))); + if (isActive) { + m_watt = stroke_power; + emit debug(QStringLiteral("Current Watts: ") + QString::number(m_watt.value())); } - break; - default: - qDebug() << "Unhandled: " << newValue.toHex(' '); - break; + } + break; + default: + qDebug() << "Unhandled: " << newValue.toHex(' '); + break; } } @@ -233,23 +241,14 @@ void concept2skierg::characteristicChanged(const QLowEnergyCharacteristic &chara lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); @@ -302,7 +301,8 @@ void concept2skierg::stateChanged(QLowEnergyService::ServiceState state) { qDebug() << "char uuid" << c.uuid() << QStringLiteral("handle") << c.handle(); // only one multiplexed characteristic is needed - if (c.uuid() != QBluetoothUuid(QStringLiteral("{ce060080-43e5-11e4-916c-0800200c9a66}"))) continue; + if (c.uuid() != QBluetoothUuid(QStringLiteral("{ce060080-43e5-11e4-916c-0800200c9a66}"))) + continue; auto descriptors_list = c.descriptors(); for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) { @@ -356,7 +356,7 @@ void concept2skierg::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualTreadmill + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -365,11 +365,14 @@ void concept2skierg::stateChanged(QLowEnergyService::ServiceState state) { ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; @@ -382,10 +385,11 @@ void concept2skierg::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &concept2skierg::debug); // connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, // &domyostreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -494,10 +498,6 @@ bool concept2skierg::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *concept2skierg::VirtualTreadmill() { return virtualTreadmill; } - -void *concept2skierg::VirtualDevice() { return VirtualTreadmill(); } - uint16_t concept2skierg::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/concept2skierg.h b/src/concept2skierg.h index 53cc7c65e..86fed87b1 100644 --- a/src/concept2skierg.h +++ b/src/concept2skierg.h @@ -27,7 +27,7 @@ #include #include "rower.h" -#include "virtualtreadmill.h" + #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,20 +37,16 @@ class concept2skierg : public rower { Q_OBJECT public: concept2skierg(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(resistance_t requestResistance); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; diff --git a/src/csafe.cpp b/src/csafe.cpp new file mode 100644 index 000000000..749b19c99 --- /dev/null +++ b/src/csafe.cpp @@ -0,0 +1,445 @@ +#include "csafe.h" // Include the header file containing csafe_dic definitions +#include +#include + +csafe::csafe() { + + // Short Commands + /* + cmds["CSAFE_GETSTATUS_CMD"] = QList() << 0x80 << QList(); + cmds["CSAFE_RESET_CMD"] = QList() << 0x81 << QList(); + cmds["CSAFE_GOIDLE_CMD"] = QList() << 0x82 << QList(); + cmds["CSAFE_GOHAVEID_CMD"] = QList() << 0x83 << QList(); + cmds["CSAFE_GOINUSE_CMD"] = QList() << 0x85 << QList(); + cmds["CSAFE_GOFINISHED_CMD"] = QList() << 0x86 << QList(); + cmds["CSAFE_GOREADY_CMD"] = QList() << 0x87 << QList(); + cmds["CSAFE_BADID_CMD"] = QList() << 0x88 << QList(); + cmds["CSAFE_GETVERSION_CMD"] = QList() << 0x91 << QList(); + cmds["CSAFE_GETID_CMD"] = QList() << 0x92 << QList(); + cmds["CSAFE_GETUNITS_CMD"] = QList() << 0x93 << QList(); + cmds["CSAFE_GETSERIAL_CMD"] = QList() << 0x94 << QList(); + cmds["CSAFE_GETODOMETER_CMD"] = QList() << 0x9B << QList(); + cmds["CSAFE_GETERRORCODE_CMD"] = QList() << 0x9C << QList(); + cmds["CSAFE_GETTWORK_CMD"] = QList() << 0xA0 << QList(); + cmds["CSAFE_GETHORIZONTAL_CMD"] = QList() << 0xA1 << QList(); + cmds["CSAFE_GETCALORIES_CMD"] = QList() << 0xA3 << QList(); + cmds["CSAFE_GETPROGRAM_CMD"] = QList() << 0xA4 << QList(); + cmds["CSAFE_GETPACE_CMD"] = QList() << 0xA6 << QList(); + cmds["CSAFE_GETCADENCE_CMD"] = QList() << 0xA7 << QList(); + cmds["CSAFE_GETUSERINFO_CMD"] = QList() << 0xAB << QList(); + cmds["CSAFE_GETHRCUR_CMD"] = QList() << 0xB0 << QList(); + cmds["CSAFE_GETPOWER_CMD"] = QList() << 0xB4 << QList(); + // Long Commands + cmds["CSAFE_AUTOUPLOAD_CMD"] = QList() << 0x01 << QList() << 1; + cmds["CSAFE_IDDIGITS_CMD"] = QList() << 0x10 << QList() << 1; + cmds["CSAFE_SETTIME_CMD"] = QList() << 0x11 << QList() << 1 << 1 << 1; + cmds["CSAFE_SETDATE_CMD"] = QList() << 0x12 << QList() << 1 << 1 << 1; + cmds["CSAFE_SETTIMEOUT_CMD"] = QList() << 0x13 << QList() << 1; + cmds["CSAFE_SETUSERCFG1_CMD"] = QList() << 0x1A << QList() << 0; + cmds["CSAFE_SETTWORK_CMD"] = QList() << 0x20 << QList() << 1 << 1 << 1; + cmds["CSAFE_SETHORIZONTAL_CMD"] = QList() << 0x21 << QList() << 2 << 1; + cmds["CSAFE_SETCALORIES_CMD"] = QList() << 0x23 << QList() << 2; + cmds["CSAFE_SETPROGRAM_CMD"] = QList() << 0x24 << QList() << 1 << 1; + cmds["CSAFE_SETPOWER_CMD"] = QList() << 0x34 << QList() << 2 << 1; + cmds["CSAFE_GETCAPS_CMD"] = QList() << 0x70 << QList() << 1; + + // PM3 Specific Short Commands + cmds["CSAFE_PM_GET_WORKOUTTYPE"] = QList() << 0x89 << QList() << 0x1A; + cmds["CSAFE_PM_GET_DRAGFACTOR"] = QList() << 0xC1 << QList() << 0x1A; + cmds["CSAFE_PM_GET_STROKESTATE"] = QList() << 0xBF << QList() << 0x1A; + cmds["CSAFE_PM_GET_WORKTIME"] = QList() << 0xA0 << QList() << 0x1A; + cmds["CSAFE_PM_GET_WORKDISTANCE"] = QList() << 0xA3 << QList() << 0x1A; + cmds["CSAFE_PM_GET_ERRORVALUE"] = QList() << 0xC9 << QList() << 0x1A; + cmds["CSAFE_PM_GET_WORKOUTSTATE"] = QList() << 0x8D << QList() << 0x1A; + cmds["CSAFE_PM_GET_WORKOUTINTERVALCOUNT"] = QList() << 0x9F << QList() << 0x1A; + cmds["CSAFE_PM_GET_INTERVALTYPE"] = QList() << 0x8E << QList() << 0x1A; + cmds["CSAFE_PM_GET_RESTTIME"] = QList() << 0xCF << QList() << 0x1A; + + // PM3 Specific Long Commands + cmds["CSAFE_PM_SET_SPLITDURATION"] = QList() << 0x05 << QList() << 1 << 4 << 0x1A; + cmds["CSAFE_PM_GET_FORCEPLOTDATA"] = QList() << 0x6B << QList() << 1 << 0x1A; + cmds["CSAFE_PM_SET_SCREENERRORMODE"] = QList() << 0x27 << QList() << 1 << 0x1A; + cmds["CSAFE_PM_GET_HEARTBEATDATA"] = QList() << 0x6C << QList() << 1 << 0x1A;*/ + + cmds["CSAFE_PM_GET_WORKTIME"] = populateCmd(0xa0, QList(), 0x1a); + cmds["CSAFE_PM_GET_WORKDISTANCE"] = populateCmd(0xa3, QList(), 0x1a); + + cmds["CSAFE_GETCALORIES_CMD"] = populateCmd(0xa3, QList()); + cmds["CSAFE_GETCADENCE_CMD"] = populateCmd(0xa7, QList()); + cmds["CSAFE_GETHRCUR_CMD"] = populateCmd(0xb0, QList()); + cmds["CSAFE_GETPOWER_CMD"] = populateCmd(0xb4, QList()); + + // Response Data to Short Commands + resp[0x80] = qMakePair(QString("CSAFE_GETSTATUS_CMD"), QList() << 0); + resp[0x81] = qMakePair(QString("CSAFE_RESET_CMD"), QList() << 0); + resp[0x82] = qMakePair(QString("CSAFE_GOIDLE_CMD"), QList() << 0); + resp[0x83] = qMakePair(QString("CSAFE_GOHAVEID_CMD"), QList() << 0); + resp[0x85] = qMakePair(QString("CSAFE_GOINUSE_CMD"), QList() << 0); + resp[0x86] = qMakePair(QString("CSAFE_GOFINISHED_CMD"), QList() << 0); + resp[0x87] = qMakePair(QString("CSAFE_GOREADY_CMD"), QList() << 0); + resp[0x88] = qMakePair(QString("CSAFE_BADID_CMD"), QList() << 0); + resp[0x91] = qMakePair(QString("CSAFE_GETVERSION_CMD"), QList() << 1 << 1 << 1 << 2 << 2); + resp[0x92] = qMakePair(QString("CSAFE_GETID_CMD"), QList() << -5); + resp[0x93] = qMakePair(QString("CSAFE_GETUNITS_CMD"), QList() << 1); + resp[0x94] = qMakePair(QString("CSAFE_GETSERIAL_CMD"), QList() << -9); + resp[0x9B] = qMakePair(QString("CSAFE_GETODOMETER_CMD"), QList() << 4 << 1); + resp[0x9C] = qMakePair(QString("CSAFE_GETERRORCODE_CMD"), QList() << 3); + resp[0xA0] = qMakePair(QString("CSAFE_GETTWORK_CMD"), QList() << 1 << 1 << 1); + resp[0xA1] = qMakePair(QString("CSAFE_GETHORIZONTAL_CMD"), QList() << 2 << 1); + resp[0xA3] = qMakePair(QString("CSAFE_GETCALORIES_CMD"), QList() << 2); + resp[0xA4] = qMakePair(QString("CSAFE_GETPROGRAM_CMD"), QList() << 1); + resp[0xA6] = qMakePair(QString("CSAFE_GETPACE_CMD"), QList() << 2 << 1); + resp[0xA7] = qMakePair(QString("CSAFE_GETCADENCE_CMD"), QList() << 2 << 1); + resp[0xAB] = qMakePair(QString("CSAFE_GETUSERINFO_CMD"), QList() << 2 << 1 << 1 << 1); + resp[0xB0] = qMakePair(QString("CSAFE_GETHRCUR_CMD"), QList() << 1); + resp[0xB4] = qMakePair(QString("CSAFE_GETPOWER_CMD"), QList() << 2 << 1); + + // Response Data to Long Commands + resp[0x01] = qMakePair(QString("CSAFE_AUTOUPLOAD_CMD"), QList() << 0); + resp[0x10] = qMakePair(QString("CSAFE_IDDIGITS_CMD"), QList() << 0); + resp[0x11] = qMakePair(QString("CSAFE_SETTIME_CMD"), QList() << 0); + resp[0x12] = qMakePair(QString("CSAFE_SETDATE_CMD"), QList() << 0); + resp[0x13] = qMakePair(QString("CSAFE_SETTIMEOUT_CMD"), QList() << 0); + resp[0x1A] = qMakePair(QString("CSAFE_SETUSERCFG1_CMD"), QList() << 0); + resp[0x20] = qMakePair(QString("CSAFE_SETTWORK_CMD"), QList() << 0); + resp[0x21] = qMakePair(QString("CSAFE_SETHORIZONTAL_CMD"), QList() << 0); + resp[0x23] = qMakePair(QString("CSAFE_SETCALORIES_CMD"), QList() << 0); + resp[0x24] = qMakePair(QString("CSAFE_SETPROGRAM_CMD"), QList() << 0); + resp[0x34] = qMakePair(QString("CSAFE_SETPOWER_CMD"), QList() << 0); + resp[0x70] = qMakePair(QString("CSAFE_GETCAPS_CMD"), QList() << 11); + + // Response Data to PM3 Specific Short Commands + resp[0x1A89] = qMakePair(QString("CSAFE_PM_GET_WORKOUTTYPE"), QList() << 1); + resp[0x1AC1] = qMakePair(QString("CSAFE_PM_GET_DRAGFACTOR"), QList() << 1); + resp[0x1ABF] = qMakePair(QString("CSAFE_PM_GET_STROKESTATE"), QList() << 1); + resp[0x1AA0] = qMakePair(QString("CSAFE_PM_GET_WORKTIME"), QList() << 4 << 1); + resp[0x1AA3] = qMakePair(QString("CSAFE_PM_GET_WORKDISTANCE"), QList() << 4 << 1); + resp[0x1AC9] = qMakePair(QString("CSAFE_PM_GET_ERRORVALUE"), QList() << 2); + resp[0x1A8D] = qMakePair(QString("CSAFE_PM_GET_WORKOUTSTATE"), QList() << 1); + resp[0x1A9F] = qMakePair(QString("CSAFE_PM_GET_WORKOUTINTERVALCOUNT"), QList() << 1); + resp[0x1A8E] = qMakePair(QString("CSAFE_PM_GET_INTERVALTYPE"), QList() << 1); + resp[0x1ACF] = qMakePair(QString("CSAFE_PM_GET_RESTTIME"), QList() << 2); + + // Response Data to PM3 Specific Long Commands + resp[0x1A05] = qMakePair(QString("CSAFE_PM_SET_SPLITDURATION"), QList() << 0); + resp[0x1A6B] = + qMakePair(QString("CSAFE_PM_GET_FORCEPLOTDATA"), + QList() << 1 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2); + resp[0x1A27] = qMakePair(QString("CSAFE_PM_SET_SCREENERRORMODE"), QList() << 0); + resp[0x1A6C] = + qMakePair(QString("CSAFE_PM_GET_HEARTBEATDATA"), + QList() << 1 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2); +} + +QList> csafe::populateCmd(int First, QList Second, int Third) { + QList> ret; + QList first; + QList second; + QList third; + first.clear(); + second.clear(); + third.clear(); + first.append(First); + foreach (int a, Second) { second.append(a); } + third.append(Third); + ret.append(first); + ret.append(second); + ret.append(third); + return ret; +} + +QList> csafe::populateCmd(int First, QList Second) { + QList> ret; + QList first; + QList second; + first.clear(); + second.clear(); + first.append(First); + foreach (int a, Second) { second.append(a); } + ret.append(first); + ret.append(second); + return ret; +} + +QVector csafe::int2bytes(int numbytes, int integer) { + if (!(0 <= integer && integer <= (1 << (8 * numbytes)))) { + qWarning("Integer is outside the allowable range"); + } + + QVector byte; + for (int k = 0; k < numbytes; ++k) { + int calcbyte = (integer >> (8 * k)) & 0xFF; + byte.append(calcbyte); + } + + return byte; +} + +int csafe::bytes2int(const QVector &raw_bytes) { + int num_bytes = raw_bytes.size(); + int integer = 0; + + for (int k = 0; k < num_bytes; ++k) { + integer = (raw_bytes[k] << (8 * k)) | integer; + } + + return integer; +} + +QString csafe::bytes2ascii(const QVector &raw_bytes) { + QString word; + for (quint8 letter : raw_bytes) { + word += QChar(letter); + } + + return word; +} + +QByteArray csafe::write(const QStringList &arguments) { + int i = 0; + QVector message; + int wrapper = 0; + QVector wrapped; + int maxresponse = 3; // start & stop flag & status + + while (i < arguments.size()) { + QString arg = arguments[i]; + const auto &cmdprop = cmds[arg]; + QVector command; + + if (cmdprop.at(1).size() != 0) // Long Command + { + for (int a = 0; a < (int)cmdprop.at(1).size(); a++) { + ++i; + int intvalue = arguments[i].toInt(); + QVector value = int2bytes(cmdprop.at(1).at(a), intvalue); + command.append(value); + } + + int cmdbytes = command.size(); + command.prepend(cmdbytes); // data byte count + } + + command.prepend(cmdprop.at(0).at(0)); // add command id + + if (!wrapped.isEmpty() && (cmdprop.size() < 3 || cmdprop.at(2).at(0) != wrapper)) { + wrapped.prepend(wrapped.size()); // data byte count for wrapper + wrapped.prepend(wrapper); // wrapper command id + message.append(wrapped); // adds wrapper to message + wrapped.clear(); + wrapper = 0; + } + + if (cmdprop.size() == 3) // checks if command needs a wrapper + { + if (wrapper == cmdprop.at(2).at(0)) // checks if currently in the same wrapper + { + wrapped.append(command); + } else // creating a new wrapper + { + wrapped = command; + wrapper = cmdprop.at(2).at(0); + maxresponse += 2; + } + + command.clear(); // clear command to prevent it from getting into message + } + + int cmdid = cmdprop[0].at(0) | (wrapper << 8); // max message length + int sum = 0; + for (int aa = 0; aa < resp[cmdid].second.size(); aa++) { + sum += resp[cmdid].second.at(aa); + } + maxresponse += qAbs(sum) * 2 + 1; // double return to account for stuffing + + message.append(command); // add completed command to final message + + ++i; + } + + if (!wrapped.isEmpty()) // closes wrapper if message ended on it + { + wrapped.prepend(wrapped.size()); // data byte count for wrapper + wrapped.prepend(wrapper); // wrapper command id + message.append(wrapped); // adds wrapper to message + } + + int checksum = 0; + int j = 0; + + while (j < message.size()) // checksum and byte stuffing + { + checksum ^= message[j]; // calculate checksum + + if (0xF0 <= message[j] && message[j] <= 0xF3) // byte stuffing + { + message.insert(j, Byte_Stuffing_Flag); + ++j; + message[j] = message[j] & 0x3; + } + + ++j; + } + + message.append(checksum); // add checksum to end of message + message.prepend(Standard_Frame_Start_Flag); // start frame + message.append(Stop_Frame_Flag); // stop frame + + if (message.size() > 96) // check for frame size (96 bytes) + { + qWarning("Message is too long: %d", message.size()); + } + + int maxmessage = qMax(message.size() + 1, maxresponse); // report IDs + + if (maxmessage <= 21) { + message.prepend(0x01); + message.append(QVector(21 - message.size())); + } else if (maxmessage <= 63) { + message.prepend(0x04); + message.append(QVector(63 - message.size())); + } else if ((message.size() + 1) <= 121) { + message.prepend(0x02); + message.append(QVector(121 - message.size())); + if (maxresponse > 121) { + qWarning("Response may be too long to receive. Max possible length: %d", maxresponse); + } + } else { + qWarning("Message too long. Message length: %d", message.size()); + message.clear(); + } + + QByteArray ret; + foreach (int a, message) { ret.append(a); } + return ret; +} + +QVector csafe::check_message(QVector message) { + int i = 0; + int checksum = 0; + + while (i < message.size()) // checksum and unstuff + { + if (message[i] == Byte_Stuffing_Flag) // byte unstuffing + { + quint8 stuffvalue = message.takeAt(i + 1); + message[i] = 0xF0 | stuffvalue; + } + + checksum ^= message[i]; // calculate checksum + + ++i; + } + + if (checksum != 0) // checks checksum + { + qWarning("Checksum error"); + message.clear(); + } else { + message.removeLast(); // remove checksum from end of message + } + + return message; +} + +QVariantMap csafe::read(const QVector &transmission) { + QVector message; + bool stopfound = false; + + int startflag = transmission[1]; + + int j = 0; + if (startflag == Extended_Frame_Start_Flag) { + j = 4; + } else if (startflag == Standard_Frame_Start_Flag) { + j = 2; + } else { + qWarning("No Start Flag found."); + return QVariantMap(); + } + + while (j < transmission.size()) { + if (transmission[j] == Stop_Frame_Flag) { + stopfound = true; + break; + } + message.append(transmission[j]); + ++j; + } + + if (!stopfound) { + qWarning("No Stop Flag found."); + return QVariantMap(); + } + + message = check_message(message); + int status = message.takeFirst(); + + QVariantMap response; + response["CSAFE_GETSTATUS_CMD"] = QVariantList() << status; + + int k = 0; + int wrapend = -1; + int wrapper = 0x0; + + while (k < message.size()) // loop through complete frames + { + QVariantList result; + + int msgcmd = message[k]; + if (k <= wrapend) { + msgcmd = wrapper | msgcmd; + } + auto &msgprop = resp[msgcmd]; + ++k; + + int bytecount = message[k]; + ++k; + + if (msgprop.first == "CSAFE_SETUSERCFG1_CMD") // if wrapper command + { + wrapper = message[k - 2] << 8; + wrapend = k + bytecount - 1; + if (bytecount != 0) { + msgcmd = wrapper | message[k]; + const auto &wrapper_msgprop = resp[msgcmd]; + msgprop.second = wrapper_msgprop.second; + ++k; + bytecount = message[k]; + ++k; + } + } + + if (msgprop.first == "CSAFE_GETCAPS_CMD") // special case for capability code + { + // msgprop.second = QList(bytecount, 1); + } + + if (msgprop.first == "CSAFE_GETID_CMD") // special case for get id + { + // msgprop.second = QList(1, -bytecount); + } + + if (abs(std::accumulate(msgprop.second.begin(), msgprop.second.end(), 0)) != 0 && + bytecount != abs(std::accumulate(msgprop.second.begin(), msgprop.second.end(), 0))) { + qWarning("Warning: bytecount is an unexpected length"); + } + + for (int numbytes : msgprop.second) // extract values + { + QVector raw_bytes = message.mid(k, abs(numbytes)); + QVariant value; + if (numbytes >= 0) { + value = bytes2int(raw_bytes); + } else { + value = bytes2ascii(raw_bytes); + } + result.append(value); + k += abs(numbytes); + } + + response[msgprop.first] = result; + } + + return response; +} diff --git a/src/csafe.h b/src/csafe.h new file mode 100644 index 000000000..ae9e8b0a6 --- /dev/null +++ b/src/csafe.h @@ -0,0 +1,42 @@ +#ifndef CSAFE_H +#define CSAFE_H + +#include + +#include + +#include +#include +#include +#include + +#include +#include + +class csafe { + private: + QVector int2bytes(int numbytes, int integer); + int bytes2int(const QVector &raw_bytes); + QString bytes2ascii(const QVector &raw_bytes); + + // Unique Frame Flags + const int Extended_Frame_Start_Flag = 0xF0; + const int Standard_Frame_Start_Flag = 0xF1; + const int Stop_Frame_Flag = 0xF2; + const int Byte_Stuffing_Flag = 0xF3; + + // cmds['COMMAND_NAME'] = [0xCmd_Id, [Bytes, ...]] + QMap>> cmds; + QMap>> resp; + + QList> populateCmd(int First, QList Second, int Third); + QList> populateCmd(int First, QList Second); + + public: + csafe(); + QByteArray write(const QStringList &arguments); + QVector check_message(QVector message); + QVariantMap read(const QVector &transmission); +}; + +#endif // CSAFE_H diff --git a/src/csaferower.cpp b/src/csaferower.cpp new file mode 100644 index 000000000..91ee16157 --- /dev/null +++ b/src/csaferower.cpp @@ -0,0 +1,412 @@ +#include "csaferower.h" + +using namespace std::chrono_literals; + +csaferower::csaferower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) { + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->noVirtualDevice = noVirtualDevice; + initDone = false; + connect(refresh, &QTimer::timeout, this, &csaferower::update); + refresh->start(200ms); + csaferowerThread *t = new csaferowerThread(); + connect(t, &csaferowerThread::onPower, this, &csaferower::onPower); + connect(t, &csaferowerThread::onCadence, this, &csaferower::onCadence); + connect(t, &csaferowerThread::onHeart, this, &csaferower::onHeart); + connect(t, &csaferowerThread::onCalories, this, &csaferower::onCalories); + connect(t, &csaferowerThread::onDistance, this, &csaferower::onDistance); + t->start(); +} + +void csaferower::onPower(double power) { + qDebug() << "Current Power received:" << power; + m_watt = power; + + double pace = (pow((2.8 / power), (1. / 3))) * 1000; // pace to m/km put *500 instead to have a m/500m + Speed = (60.0 / (double)(pace)) * 30.0; + + qDebug() << "Current Speed calculated:" << Speed.value() << pace; +} + +void csaferower::onCadence(double cadence) { qDebug() << "Current Cadence received:" << cadence; } + +void csaferower::onHeart(double hr) { + qDebug() << "Current Heart received:" << hr; + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else +#endif + { + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + uint8_t heart = ((uint8_t)hr); + if (heart == 0 || disable_hr_frommachinery) { + update_hr_from_external(); + } else + Heart = heart; + } + } +} + +void csaferower::onCalories(double calories) { + qDebug() << "Current Calories received:" << calories; + KCal = calories; +} + +void csaferower::onDistance(double distance) { qDebug() << "Current Distance received:" << distance / 1000.0; } + +csaferowerThread::csaferowerThread() {} + +void csaferowerThread::run() { + QSettings settings; + /*devicePort = + settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();*/ + + openPort(); + csafe *aa = new csafe(); + while (1) { + QStringList command; + command << "CSAFE_PM_GET_WORKTIME"; + command << "CSAFE_PM_GET_WORKDISTANCE"; + command << "CSAFE_GETCADENCE_CMD"; + command << "CSAFE_GETPOWER_CMD"; + command << "CSAFE_GETCALORIES_CMD"; + command << "CSAFE_GETHRCUR_CMD"; + QByteArray ret = aa->write(command); + + qDebug() << " >> " << ret.toHex(' '); + rawWrite((uint8_t *)ret.data(), ret.length()); + static uint8_t rx[100]; + rawRead(rx, 100); + qDebug() << " << " << QByteArray::fromRawData((const char *)rx, 64).toHex(' '); + + QVector v; + for (int i = 0; i < 64; i++) + v.append(rx[i]); + QVariantMap f = aa->read(v); + if (f["CSAFE_GETCADENCE_CMD"].isValid()) { + emit onCadence(f["CSAFE_GETCADENCE_CMD"].value()[0].toDouble()); + } + if (f["CSAFE_GETPOWER_CMD"].isValid()) { + emit onPower(f["CSAFE_GETPOWER_CMD"].value()[0].toDouble()); + } + if (f["CSAFE_GETHRCUR_CMD"].isValid()) { + emit onHeart(f["CSAFE_GETHRCUR_CMD"].value()[0].toDouble()); + } + if (f["CSAFE_GETCALORIES_CMD"].isValid()) { + emit onCalories(f["CSAFE_GETCALORIES_CMD"].value()[0].toDouble()); + } + if (f["CSAFE_PM_GET_WORKDISTANCE"].isValid()) { + emit onDistance(f["CSAFE_PM_GET_WORKDISTANCE"].value()[0].toDouble()); + } + + memset(rx, 0x00, sizeof(rx)); + QThread::msleep(50); + } + closePort(); +} + +int csaferowerThread::closePort() { +#ifdef WIN32 + return (int)!CloseHandle(devicePort); +#else + tcflush(devicePort, TCIOFLUSH); // clear out the garbage + return close(devicePort); +#endif +} + +int csaferowerThread::openPort() { +#ifdef Q_OS_ANDROID + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/CSafeRowerUSBHID", "open", + "(Landroid/content/Context;)V", QtAndroid::androidContext().object()); +#elif !defined(WIN32) + + // LINUX AND MAC USES TERMIO / IOCTL / STDIO + +#if defined(Q_OS_MACX) + int ldisc = TTYDISC; +#else + int ldisc = N_TTY; // LINUX +#endif + + if ((devicePort = open(deviceFilename.toLatin1(), O_RDWR | O_NOCTTY | O_NONBLOCK)) == -1) + return errno; + + tcflush(devicePort, TCIOFLUSH); // clear out the garbage + + if (ioctl(devicePort, TIOCSETD, &ldisc) == -1) + return errno; + + // get current settings for the port + tcgetattr(devicePort, &deviceSettings); + + // set raw mode i.e. ignbrk, brkint, parmrk, istrip, inlcr, igncr, icrnl, ixon + // noopost, cs8, noecho, noechonl, noicanon, noisig, noiexn + cfmakeraw(&deviceSettings); + cfsetspeed(&deviceSettings, B2400); + + // further attributes + deviceSettings.c_iflag &= + ~(IGNBRK | BRKINT | ICRNL | INLCR | PARMRK | INPCK | ICANON | ISTRIP | IXON | IXOFF | IXANY); + deviceSettings.c_iflag |= IGNPAR; + deviceSettings.c_cflag &= (~CSIZE & ~CSTOPB); + deviceSettings.c_oflag = 0; + +#if defined(Q_OS_MACX) + deviceSettings.c_cflag &= (~CCTS_OFLOW & ~CRTS_IFLOW); // no hardware flow control + deviceSettings.c_cflag |= (CS8 | CLOCAL | CREAD | HUPCL); +#else + deviceSettings.c_cflag &= (~CRTSCTS); // no hardware flow control + deviceSettings.c_cflag |= (CS8 | CLOCAL | CREAD | HUPCL); +#endif + deviceSettings.c_lflag = 0; + deviceSettings.c_cc[VSTART] = 0x11; + deviceSettings.c_cc[VSTOP] = 0x13; + deviceSettings.c_cc[VEOF] = 0x20; + deviceSettings.c_cc[VMIN] = 0; + deviceSettings.c_cc[VTIME] = 0; + + // set those attributes + if (tcsetattr(devicePort, TCSANOW, &deviceSettings) == -1) + return errno; + tcgetattr(devicePort, &deviceSettings); + + tcflush(devicePort, TCIOFLUSH); // clear out the garbage +#else + + +#endif + + // success + return 0; +} + +int csaferowerThread::rawWrite(uint8_t *bytes, int size) // unix!! +{ + qDebug() << size << QByteArray((const char *)bytes, size).toHex(' '); + + int rc = 0; + +#ifdef Q_OS_ANDROID + + QAndroidJniEnvironment env; + jbyteArray d = env->NewByteArray(size); + jbyte *b = env->GetByteArrayElements(d, 0); + for (int i = 0; i < size; i++) + b[i] = bytes[i]; + env->SetByteArrayRegion(d, 0, size, b); + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/CSafeRowerUSBHID", "write", "([B)V", d); +#elif defined(WIN32) + DWORD cBytes; + rc = WriteFile(devicePort, bytes, size, &cBytes, NULL); + if (!rc) + return -1; + return rc; + +#else + int ibytes; + ioctl(devicePort, FIONREAD, &ibytes); + + // timeouts are less critical for writing, since vols are low + rc = write(devicePort, bytes, size); + + // but it is good to avoid buffer overflow since the + // computrainer microcontroller has almost no RAM + if (rc != -1) + tcdrain(devicePort); // wait till its gone. + + ioctl(devicePort, FIONREAD, &ibytes); +#endif + + return rc; +} + +int csaferowerThread::rawRead(uint8_t bytes[], int size) { + int rc = 0; + +#ifdef Q_OS_ANDROID + int64_t start = QDateTime::currentMSecsSinceEpoch(); + jint len = 0; + + do { + QAndroidJniEnvironment env; + QAndroidJniObject dd = + QAndroidJniObject::callStaticObjectMethod("org/cagnulen/qdomyoszwift/CSafeRowerUSBHID", "read", "()[B"); + len = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/CSafeRowerUSBHID", "readLen", "()I"); + if (len > 0) { + jbyteArray d = dd.object(); + jbyte *b = env->GetByteArrayElements(d, 0); + for (int i = 0; i < len; i++) { + bytes[i] = b[i]; + } + qDebug() << len << QByteArray((const char *)b, len).toHex(' '); + } + } while (len == 0 && start + 2000 > QDateTime::currentMSecsSinceEpoch()); + + return len; +#elif defined(WIN32) + Q_UNUSED(size); + // Readfile deals with timeouts and readyread issues + DWORD cBytes; + rc = ReadFile(devicePort, bytes, 7, &cBytes, NULL); + if (rc) + return (int)cBytes; + else + return (-1); + +#else + + int timeout = 0, i = 0; + uint8_t byte; + + // read one byte at a time sleeping when no data ready + // until we timeout waiting then return error + for (i = 0; i < size; i++) { + timeout = 0; + rc = 0; + while (rc == 0 && timeout < CT_READTIMEOUT) { + rc = read(devicePort, &byte, 1); + if (rc == -1) + return -1; // error! + else if (rc == 0) { + msleep(50); // sleep for 1/20th of a second + timeout += 50; + } else { + bytes[i] = byte; + } + } + if (timeout >= CT_READTIMEOUT) + return -1; // we timed out! + } + + qDebug() << i << QString::fromLocal8Bit((const char *)bytes, i); + + return i; + +#endif +} + +void csaferower::update() { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + + update_metrics(false, watts()); + + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + + Distance += ((Speed.value() / (double)3600.0) / + ((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + // ******************************************* virtual bike/rower init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + QSettings settings; + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && !virtual_device_rower) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + { + if (virtual_device_enabled) { + if (!virtual_device_rower) { + qDebug() << QStringLiteral("creating virtual bike interface..."); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + // connect(virtualBike,&virtualbike::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + qDebug() << QStringLiteral("creating virtual rower interface..."); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + } + } + if (!firstStateChanged) + emit connectedAndDiscovered(); + firstStateChanged = 1; + // ******************************************************************************************************** + + if (!noVirtualDevice) { +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + Heart = (uint8_t)KeepAwakeHelper::heart(); + debug("Current Heart: " + QString::number(Heart.value())); + } +#endif + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + } + + /* + if (Heart.value()) { + static double lastKcal = 0; + if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator + lastKcal = abs(KCal.value()); + KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal; + }*/ + + if (requestResistance != -1 && requestResistance != currentResistance().value()) { + Resistance = requestResistance; + } +} + +void csaferower::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + QByteArray b = newValue; + qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' '); +} + +bool csaferower::connected() { return true; } + +void csaferower::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + " (" + device.address().toString() + ')'); +} + +void csaferower::newPacket(QByteArray p) {} + +uint16_t csaferower::watts() { return m_watt.value(); } diff --git a/src/csaferower.h b/src/csaferower.h new file mode 100644 index 000000000..e4f0f877f --- /dev/null +++ b/src/csaferower.h @@ -0,0 +1,166 @@ +#ifndef CSAFEROWER_H +#define CSAFEROWER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include + +#include "csafe.h" +#include "rower.h" +#include "virtualbike.h" +#include "virtualrower.h" +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include + +#include +#else +#include +#include // unix!! +#include // unix!! +#ifndef N_TTY // for OpenBSD, this is a hack XXX +#define N_TTY 0 +#endif +#endif + +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +/* read timeouts in microseconds */ +#define CT_READTIMEOUT 1000 +#define CT_WRITETIMEOUT 2000 + +class csaferowerThread : public QThread { + Q_OBJECT + + public: + explicit csaferowerThread(); + + void run(); + + signals: + void onDebug(QString debug); + void newPacket(QByteArray p); + void onPower(double power); + void onCadence(double cadence); + void onHeart(double hr); + void onCalories(double calories); + void onDistance(double distance); + + private: + // Utility and BG Thread functions + int openPort(); + int closePort(); + + // Mutex for controlling accessing private data + QMutex pvars; + + // device port + QString deviceFilename; +#ifdef WIN32 + HANDLE devicePort; // file descriptor for reading from com3 + DCB deviceSettings; // serial port settings baud rate et al +#else + int devicePort; // unix!! + struct termios deviceSettings; // unix!! +#endif + // raw device utils + int rawWrite(uint8_t *bytes, int size); // unix!! + int rawRead(uint8_t *bytes, int size); // unix!! + +#ifdef Q_OS_ANDROID + QList bufRX; + bool cleanFrame = false; +#endif +}; + +class csaferower : public rower { + Q_OBJECT + public: + csaferower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); + bool connected() override; + + private: + QTimer *refresh; + + uint8_t sec1Update = 0; + QByteArray lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastGoodCadence = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + + uint16_t watts() override; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + bool noVirtualDevice = false; + + uint16_t oldLastCrankEventTime = 0; + uint16_t oldCrankRevs = 0; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); + + private slots: + void update(); + void newPacket(QByteArray p); + void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void onPower(double power); + void onCadence(double cadence); + void onHeart(double hr); + void onCalories(double calories); + void onDistance(double distance); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); +}; + +#endif // CSAFEROWER_H diff --git a/src/cscbike.cpp b/src/cscbike.cpp index d4b6b844e..55f3ed0ab 100644 --- a/src/cscbike.cpp +++ b/src/cscbike.cpp @@ -1,5 +1,4 @@ #include "cscbike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -9,9 +8,9 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -68,37 +67,12 @@ void cscbike::update() { } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } - if (Heart.value() > 0) { - int avgP = ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() * - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()) - - (settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble() * - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble())) / - (settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble() - - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble()) + - (Heart.value() * - ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() - - settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble()) / - (settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble() - - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()))); - if (avgP < 50) { - avgP = 50; - } - m_watt = avgP; - emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); - } + m_watt = wattFromHR(false); + emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); if (m_control->state() == QLowEnergyController::UnconnectedState) { emit disconnected(); @@ -115,6 +89,15 @@ void cscbike::update() { /*initDone*/) { update_metrics(true, watts()); + if(lastGoodCadence.secsTo(QDateTime::currentDateTime()) > 5 && !charNotified) { + readMethod = true; + qDebug() << "no cadence for 5 secs, switching to reading method"; + } + + if(readMethod && cadenceService) { + cadenceService->readCharacteristic(cadenceChar); + } + // updating the treadmill console every second if (sec1Update++ == (500 / refresh->interval())) { sec1Update = 0; @@ -167,6 +150,8 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist double _WheelRevs = 0; uint8_t battery = 0; + charNotified = true; + emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); if (characteristic.uuid() == QBluetoothUuid((quint16)0x2A19)) { @@ -337,8 +322,16 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) { qDebug() << QStringLiteral("all services discovered!"); + QBluetoothUuid CyclingSpeedAndCadence(QBluetoothUuid::CyclingSpeedAndCadence); + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { if (s->state() == QLowEnergyService::ServiceDiscovered) { + + if(s->serviceUuid() == CyclingSpeedAndCadence) { + qDebug() << "CyclingSpeedAndCadence found"; + cadenceService = s; + } + // establish hook into notifications connect(s, &QLowEnergyService::characteristicChanged, this, &cscbike::characteristicChanged); connect(s, &QLowEnergyService::characteristicWritten, this, &cscbike::characteristicWritten); @@ -353,7 +346,11 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) { auto characteristics_list = s->characteristics(); for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) { - qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle(); + if(c.uuid() == QBluetoothUuid((quint16)0x2A5B)) { + qDebug() << "CyclingSpeedAndCadence char found"; + cadenceChar = c; + } + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle() << QStringLiteral("properties") << c.properties(); auto descriptors_list = c.descriptors(); for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) { qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); @@ -397,7 +394,7 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike && !noVirtualDevice + if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -422,9 +419,10 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination); // connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -449,6 +447,8 @@ void cscbike::characteristicWritten(const QLowEnergyCharacteristic &characterist void cscbike::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { qDebug() << QStringLiteral("characteristicRead ") << characteristic.uuid() << newValue.toHex(' '); + + characteristicChanged(characteristic, newValue); } void cscbike::serviceScanDone(void) { @@ -536,10 +536,6 @@ bool cscbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *cscbike::VirtualBike() { return virtualBike; } - -void *cscbike::VirtualDevice() { return VirtualBike(); } - uint16_t cscbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/cscbike.h b/src/cscbike.h index 716837e47..825e81c60 100644 --- a/src/cscbike.h +++ b/src/cscbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,28 +36,26 @@ class cscbike : public bike { Q_OBJECT public: cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: // void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, //Unused // bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; - // QLowEnergyCharacteristic gattNotify1Characteristic; + QLowEnergyService* cadenceService = nullptr; + QLowEnergyCharacteristic cadenceChar; uint8_t sec1Update = 0; QByteArray lastPacket; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); QDateTime lastGoodCadence = QDateTime::currentDateTime(); uint8_t firstStateChanged = 0; + bool charNotified = false; bool initDone = false; bool initRequest = false; @@ -67,6 +64,8 @@ class cscbike : public bike { bool noHeartService = false; bool noVirtualDevice = false; + bool readMethod = false; + uint16_t oldLastCrankEventTime = 0; uint16_t oldCrankRevs = 0; diff --git a/src/dirconmanager.cpp b/src/dirconmanager.cpp index 953def4be..00e436a60 100644 --- a/src/dirconmanager.cpp +++ b/src/dirconmanager.cpp @@ -1,6 +1,9 @@ #include "dirconmanager.h" #include #include +#include + +using namespace std::chrono_literals; #define DM_MACHINE_TYPE_BIKE 1 #define DM_MACHINE_TYPE_TREADMILL 2 @@ -166,7 +169,10 @@ DirconManager::DirconManager(bluetoothdevice *Bike, uint8_t bikeResistanceOffset QObject::connect(&bikeTimer, &QTimer::timeout, this, &DirconManager::bikeProvider); QString mac = getMacAddress(); DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type) - bikeTimer.start(1000); + if (settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool()) + bikeTimer.start(100ms); + else + bikeTimer.start(1s); } #define DM_CHAR_NOTIF_NOTIF1_OP(UUID, P1, P2, P3) \ diff --git a/src/discoveryoptions.h b/src/discoveryoptions.h index a8ec15865..f87bea72a 100644 --- a/src/discoveryoptions.h +++ b/src/discoveryoptions.h @@ -49,11 +49,6 @@ class discoveryoptions { * @brief Use to suppress starting discovery in the constructor, e.g. for testing. */ bool startDiscovery = true; - - /** - * @brief Used to suppress the creation of the tempalte managers to decouple from the UI. - */ - bool createTemplateManagers = true; }; diff --git a/src/domyosbike.cpp b/src/domyosbike.cpp index b0ea5f6ed..068f4409a 100644 --- a/src/domyosbike.cpp +++ b/src/domyosbike.cpp @@ -1,6 +1,7 @@ #include "domyosbike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -29,12 +30,7 @@ domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testRes refresh->start(300ms); } -domyosbike::~domyosbike() { - qDebug() << QStringLiteral("~domyosbike()") << virtualBike; - if (virtualBike) { - delete virtualBike; - } -} +domyosbike::~domyosbike() { qDebug() << QStringLiteral("~domyosbike()"); } void domyosbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -55,11 +51,15 @@ void domyosbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStr return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -74,8 +74,13 @@ void domyosbike::updateDisplay(uint16_t elapsed) { uint16_t multiplier = 1; QSettings settings; - bool distance = settings.value(QZSettings::domyos_treadmill_distance_display, QZSettings::default_domyos_treadmill_distance_display).toBool(); - bool domyos_bike_display_calories = settings.value(QZSettings::domyos_bike_display_calories, QZSettings::default_domyos_bike_display_calories).toBool(); + bool distance = + settings + .value(QZSettings::domyos_treadmill_distance_display, QZSettings::default_domyos_treadmill_distance_display) + .toBool(); + bool domyos_bike_display_calories = + settings.value(QZSettings::domyos_bike_display_calories, QZSettings::default_domyos_bike_display_calories) + .toBool(); if (domyos_bike_display_calories) { multiplier = 10; @@ -182,7 +187,7 @@ void domyosbike::update() { update_metrics(true, watts()); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -190,11 +195,14 @@ void domyosbike::update() { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -204,10 +212,11 @@ void domyosbike::update() { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&schwinnic4bike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &domyosbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -347,7 +356,9 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character double distance = GetDistanceFromPacket(value); double ucadence = ((uint8_t)value.at(9)); - double cadenceFilter = settings.value(QZSettings::domyos_bike_cadence_filter, QZSettings::default_domyos_bike_cadence_filter).toDouble(); + double cadenceFilter = + settings.value(QZSettings::domyos_bike_cadence_filter, QZSettings::default_domyos_bike_cadence_filter) + .toDouble(); if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() .startsWith(QStringLiteral("Disabled"))) { @@ -367,7 +378,8 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character emit resistanceRead(Resistance.value()); m_pelotonResistance = (Resistance.value() * 100) / max_resistance; - bool disable_hr_frommachinery = settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); #ifdef Q_OS_ANDROID if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) @@ -378,16 +390,7 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { uint8_t heart = ((uint8_t)value.at(18)); if (heart == 0 || disable_hr_frommachinery) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } else Heart = heart; } @@ -402,7 +405,8 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -427,7 +431,9 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = speed; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } KCal = kcal; Distance = distance; @@ -642,11 +648,9 @@ bool domyosbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *domyosbike::VirtualBike() { return virtualBike; } - -void *domyosbike::VirtualDevice() { return VirtualBike(); } - -resistance_t domyosbike::pelotonToBikeResistance(int pelotonResistance) { return (pelotonResistance * max_resistance) / 100; } +resistance_t domyosbike::pelotonToBikeResistance(int pelotonResistance) { + return (pelotonResistance * max_resistance) / 100; +} resistance_t domyosbike::resistanceFromPowerRequest(uint16_t power) { qDebug() << QStringLiteral("resistanceFromPowerRequest") << currentCadence().value(); @@ -664,26 +668,28 @@ resistance_t domyosbike::resistanceFromPowerRequest(uint16_t power) { uint16_t domyosbike::wattsFromResistance(double resistance) { QSettings settings; - if(!settings.value(QZSettings::domyos_bike_500_profile_v1, QZSettings::default_domyos_bike_500_profile_v1).toBool() || resistance < 8) + if (!settings.value(QZSettings::domyos_bike_500_profile_v1, QZSettings::default_domyos_bike_500_profile_v1) + .toBool() || + resistance < 8) return ((10.39 + 1.45 * (resistance - 1.0)) * (exp(0.028 * (currentCadence().value())))); else { - switch((int)resistance) { - case 8: - return (13.6 * Cadence.value()) / 9.5488; - case 9: - return (15.3 * Cadence.value()) / 9.5488; - case 10: - return (17.3 * Cadence.value()) / 9.5488; - case 11: - return (19.8 * Cadence.value()) / 9.5488; - case 12: - return (22.5 * Cadence.value()) / 9.5488; - case 13: - return (25.6 * Cadence.value()) / 9.5488; - case 14: - return (28.4 * Cadence.value()) / 9.5488; - case 15: - return (35.9 * Cadence.value()) / 9.5488; + switch ((int)resistance) { + case 8: + return (13.6 * Cadence.value()) / 9.5488; + case 9: + return (15.3 * Cadence.value()) / 9.5488; + case 10: + return (17.3 * Cadence.value()) / 9.5488; + case 11: + return (19.8 * Cadence.value()) / 9.5488; + case 12: + return (22.5 * Cadence.value()) / 9.5488; + case 13: + return (25.6 * Cadence.value()) / 9.5488; + case 14: + return (28.4 * Cadence.value()) / 9.5488; + case 15: + return (35.9 * Cadence.value()) / 9.5488; } return ((10.39 + 1.45 * (resistance - 1.0)) * (exp(0.028 * (currentCadence().value())))); } diff --git a/src/domyosbike.h b/src/domyosbike.h index 01540f3ec..7168e8522 100644 --- a/src/domyosbike.h +++ b/src/domyosbike.h @@ -38,14 +38,11 @@ class domyosbike : public bike { public: domyosbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t maxResistance() { return max_resistance; } - ~domyosbike(); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t maxResistance() override { return max_resistance; } + ~domyosbike() override; + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -60,11 +57,10 @@ class domyosbike : public bike { void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; const resistance_t max_resistance = 15; QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t firstVirtual = 0; uint8_t firstStateChanged = 0; diff --git a/src/domyoselliptical.cpp b/src/domyoselliptical.cpp index b3af01b3c..e902b725e 100644 --- a/src/domyoselliptical.cpp +++ b/src/domyoselliptical.cpp @@ -1,6 +1,8 @@ #include "domyoselliptical.h" - +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -29,12 +31,7 @@ domyoselliptical::domyoselliptical(bool noWriteResistance, bool noHeartService, refresh->start(300ms); } -domyoselliptical::~domyoselliptical() { - qDebug() << QStringLiteral("~domyoselliptical()") << virtualTreadmill; - - if (virtualTreadmill) - delete virtualTreadmill; -} +domyoselliptical::~domyoselliptical() { qDebug() << QStringLiteral("~domyoselliptical()"); } void domyoselliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -49,11 +46,15 @@ void domyoselliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, cons timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -162,15 +163,13 @@ void domyoselliptical::update() { btinit_changyow(false); // else // btinit_telink(false); - } else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState && - gattCommunicationChannelService && gattWriteCharacteristic.isValid() && - gattNotifyCharacteristic.isValid() && initDone) { + } else if (initDone) { update_metrics(true, watts()); // ******************************************* virtual bike init ************************************* QSettings settings; - if (!firstVirtual && searchStopped && !virtualTreadmill && !virtualBike) { + if (!firstVirtual && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -179,17 +178,19 @@ void domyoselliptical::update() { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &domyoselliptical::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &domyoselliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, - bikeResistanceGain); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); connect(virtualBike, &virtualbike::changeInclination, this, &domyoselliptical::changeInclinationRequested); connect(virtualBike, &virtualbike::changeInclination, this, &domyoselliptical::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstVirtual = 1; } @@ -301,11 +302,17 @@ void domyoselliptical::characteristicChanged(const QLowEnergyCharacteristic &cha .toDouble(); bool disable_hr_frommachinery = settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + double cadence_gain = settings.value(QZSettings::cadence_gain, QZSettings::default_cadence_gain).toDouble(); + double cadence_offset = settings.value(QZSettings::cadence_offset, QZSettings::default_cadence_offset).toDouble(); if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() .startsWith(QStringLiteral("Disabled"))) { - Cadence = ((uint8_t)newValue.at(9)); + uint8_t c = ((uint8_t)newValue.at(9)); + if (c > 0) + Cadence = (c * cadence_gain) + cadence_offset; + else + Cadence = 0; } Resistance = newValue.at(14); Inclination = newValue.at(21); @@ -328,19 +335,9 @@ void domyoselliptical::characteristicChanged(const QLowEnergyCharacteristic &cha { if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) { Heart = ((uint8_t)newValue.at(18)); + } else if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); } -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - else { - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); - } -#endif -#endif } CrankRevs++; @@ -593,10 +590,6 @@ bool domyoselliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *domyoselliptical::VirtualTreadmill() { return virtualTreadmill; } - -void *domyoselliptical::VirtualDevice() { return VirtualTreadmill(); } - uint16_t domyoselliptical::watts() { QSettings settings; diff --git a/src/domyoselliptical.h b/src/domyoselliptical.h index 6904b3d75..2e49efa4e 100644 --- a/src/domyoselliptical.h +++ b/src/domyoselliptical.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" class domyoselliptical : public elliptical { Q_OBJECT @@ -36,11 +34,8 @@ class domyoselliptical : public elliptical { domyoselliptical(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); ~domyoselliptical(); - bool connected(); - bool inclinationAvailableByHardware(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; + bool inclinationAvailableByHardware() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -58,8 +53,6 @@ class domyoselliptical : public elliptical { uint16_t watts(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t firstVirtual = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; diff --git a/src/domyosrower.cpp b/src/domyosrower.cpp index f72f8af5c..8586d030d 100644 --- a/src/domyosrower.cpp +++ b/src/domyosrower.cpp @@ -1,6 +1,10 @@ #include "domyosrower.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" +#include "virtualrower.h" #include "virtualtreadmill.h" #include #include @@ -29,12 +33,7 @@ domyosrower::domyosrower(bool noWriteResistance, bool noHeartService, bool testR refresh->start(300ms); } -domyosrower::~domyosrower() { - qDebug() << QStringLiteral("~domyosrower()") << virtualTreadmill; - - if (virtualTreadmill) - delete virtualTreadmill; -} +domyosrower::~domyosrower() { qDebug() << QStringLiteral("~domyosrower()"); } void domyosrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -49,12 +48,15 @@ void domyosrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -170,27 +172,35 @@ void domyosrower::update() { // ******************************************* virtual treadmill init ************************************* QSettings settings; - bool virtual_device_rower = settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); - if (!firstVirtual && !virtualTreadmill && !virtualBike && !virtualRower) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstVirtual && searchStopped && !this->hasVirtualDevice()) { + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (virtual_device_rower) { qDebug() << QStringLiteral("creating virtual rower interface..."); - virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } else if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &domyosrower::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &domyosrower::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); connect(virtualBike, &virtualbike::changeInclination, this, &domyosrower::changeInclinationRequested); connect(virtualBike, &virtualbike::changeInclination, this, &domyosrower::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstVirtual = 1; } @@ -291,10 +301,14 @@ void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characte inclination and speed status return;*/ double speed = - GetSpeedFromPacket(newValue) * settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); + GetSpeedFromPacket(newValue) * + settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio) + .toDouble(); double kcal = GetKcalFromPacket(newValue); - double distance = GetDistanceFromPacket(newValue) * - settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); + double distance = + GetDistanceFromPacket(newValue) * + settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio) + .toDouble(); if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() @@ -353,8 +367,9 @@ void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characte double domyosrower::GetSpeedFromPacket(const QByteArray &packet) { uint16_t convertedData = (packet.at(6) << 8) | packet.at(7); - double data = (double)convertedData / 10.0f; - return data; + if (convertedData > 65000 || convertedData == 0 || currentCadence().value() == 0) + return 0; + return (60.0 / (double)(convertedData)) * 30.0; } double domyosrower::GetKcalFromPacket(const QByteArray &packet) { @@ -573,10 +588,6 @@ bool domyosrower::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *domyosrower::VirtualTreadmill() { return virtualTreadmill; } - -void *domyosrower::VirtualDevice() { return VirtualTreadmill(); } - uint16_t domyosrower::watts() { QSettings settings; diff --git a/src/domyosrower.h b/src/domyosrower.h index d9dbe6a0d..2d44d6608 100644 --- a/src/domyosrower.h +++ b/src/domyosrower.h @@ -27,9 +27,6 @@ #include #include "rower.h" -#include "virtualbike.h" -#include "virtualrower.h" -#include "virtualtreadmill.h" class domyosrower : public rower { Q_OBJECT @@ -37,10 +34,7 @@ class domyosrower : public rower { domyosrower(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); ~domyosrower(); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -55,12 +49,9 @@ class domyosrower : public rower { void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; - virtualrower *virtualRower = nullptr; uint8_t firstVirtual = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; diff --git a/src/domyostreadmill.cpp b/src/domyostreadmill.cpp index 2f2e95be9..07f911f99 100644 --- a/src/domyostreadmill.cpp +++ b/src/domyostreadmill.cpp @@ -1,6 +1,6 @@ #include "domyostreadmill.h" -#include "ios/lockscreen.h" #include "keepawakehelper.h" +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -100,11 +100,15 @@ void domyostreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, const return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') << QStringLiteral(" // ") + info; } @@ -271,7 +275,7 @@ void domyostreadmill::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && searchStopped && !virtualTreadMill && !virtualBike) { + if (!firstInit && searchStopped && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -280,15 +284,17 @@ void domyostreadmill::update() { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &domyostreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &domyostreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &domyostreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstInit = 1; } @@ -560,20 +566,10 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char uint8_t heart = ((uint8_t)value.at(18)); if (heart == 0 || disable_hr_frommachinery) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif - } else - + update_hr_from_external(); + } else { Heart = heart; + } } } @@ -815,8 +811,4 @@ bool domyostreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *domyostreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *domyostreadmill::VirtualDevice() { return VirtualTreadMill(); } - void domyostreadmill::searchingStop() { searchStopped = true; } diff --git a/src/domyostreadmill.h b/src/domyostreadmill.h index b5c2bd27d..eaa083c2f 100644 --- a/src/domyostreadmill.h +++ b/src/domyostreadmill.h @@ -28,8 +28,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -41,11 +39,8 @@ class domyostreadmill : public treadmill { public: domyostreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - bool changeFanSpeed(uint8_t speed); - - void *VirtualTreadMill(); - void *VirtualDevice(); + bool connected() override; + bool changeFanSpeed(uint8_t speed) override; private: bool sendChangeFanSpeed(uint8_t speed); @@ -71,8 +66,6 @@ class domyostreadmill : public treadmill { bool firstCharacteristicChanged = true; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/echelonconnectsport.cpp b/src/echelonconnectsport.cpp index e52ac2b0d..ba289a127 100644 --- a/src/echelonconnectsport.cpp +++ b/src/echelonconnectsport.cpp @@ -1,6 +1,7 @@ #include "echelonconnectsport.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -59,11 +60,15 @@ void echelonconnectsport::writeCharacteristic(uint8_t *data, uint8_t data_len, c return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -153,7 +158,7 @@ void echelonconnectsport::serviceDiscovered(const QBluetoothUuid &gatt) { resistance_t echelonconnectsport::pelotonToBikeResistance(int pelotonResistance) { for (resistance_t i = 1; i < max_resistance; i++) { - if (bikeResistanceToPeloton(i) <= pelotonResistance && bikeResistanceToPeloton(i + 1) >= pelotonResistance) { + if (bikeResistanceToPeloton(i) <= pelotonResistance && bikeResistanceToPeloton(i + 1) > pelotonResistance) { return i; } } @@ -210,16 +215,17 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic & resistance_t res = newValue.at(3); if (settings.value(QZSettings::gears_from_bike, QZSettings::default_gears_from_bike).toBool()) { qDebug() << QStringLiteral("gears_from_bike") << res << Resistance.value() << gears() - << lastRawRequestedResistanceValue; + << lastRawRequestedResistanceValue << lastRequestedResistance().value(); if ( // if the resistance is different from the previous one res != qRound(Resistance.value()) && // and the last target resistance is different from the current one or there is no any pending last // requested resistance - ((lastRawRequestedResistanceValue != res && lastRawRequestedResistanceValue != -1) || + ((lastRequestedResistance().value() != res && lastRequestedResistance().value() != 0) || lastRawRequestedResistanceValue == -1) && // and the difference between the 2 resistances are less than 6 qRound(Resistance.value()) > 1 && qAbs(res - qRound(Resistance.value())) < 6) { + int8_t g = gears(); g += (res - qRound(Resistance.value())); qDebug() << QStringLiteral("gears_from_bike APPLIED") << gears() << g; @@ -281,16 +287,7 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic & #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } @@ -397,7 +394,7 @@ void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) { &echelonconnectsport::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -407,6 +404,8 @@ void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = @@ -421,11 +420,19 @@ void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) { #endif #endif if (virtual_device_enabled) { - qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = - new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); - // connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug); - connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination); + if (virtual_device_rower) { + qDebug() << QStringLiteral("creating virtual rower interface..."); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE); + } else { + qDebug() << QStringLiteral("creating virtual bike interface..."); + auto virtualBike = + new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + // connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug); + connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } } } firstStateChanged = 1; @@ -523,10 +530,6 @@ bool echelonconnectsport::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *echelonconnectsport::VirtualBike() { return virtualBike; } - -void *echelonconnectsport::VirtualDevice() { return VirtualBike(); } - uint16_t echelonconnectsport::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/echelonconnectsport.h b/src/echelonconnectsport.h index 2f4bfe866..006037af7 100644 --- a/src/echelonconnectsport.h +++ b/src/echelonconnectsport.h @@ -28,6 +28,7 @@ #include "bike.h" #include "virtualbike.h" +#include "virtualrower.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -38,13 +39,10 @@ class echelonconnectsport : public bike { public: echelonconnectsport(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t maxResistance() { return max_resistance; } - resistance_t resistanceFromPowerRequest(uint16_t power); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t maxResistance() override { return max_resistance; } + resistance_t resistanceFromPowerRequest(uint16_t power) override; + bool connected() override; private: const resistance_t max_resistance = 32; @@ -58,10 +56,9 @@ class echelonconnectsport : public bike { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/echelonrower.cpp b/src/echelonrower.cpp index e661cc154..a1659f4da 100644 --- a/src/echelonrower.cpp +++ b/src/echelonrower.cpp @@ -1,7 +1,9 @@ #include "echelonrower.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" +#include "virtualrower.h" #include #include #include @@ -59,11 +61,15 @@ void echelonrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QS return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -195,7 +201,7 @@ void echelonrower::characteristicChanged(const QLowEnergyCharacteristic &charact qDebug() << QStringLiteral(" << ") + newvalue.toHex(' '); - if(lastPacket.count() + newvalue.count() == 21 && ((unsigned char)lastPacket.at(0)) == 0xf0) { + if (lastPacket.count() > 0 && lastPacket.count() + newvalue.count() == 21 && ((unsigned char)lastPacket.at(0)) == 0xf0) { lastPacket = lastPacket.append(newvalue); qDebug() << QStringLiteral(" << concatenated ") + lastPacket.toHex(' '); } else { @@ -203,7 +209,8 @@ void echelonrower::characteristicChanged(const QLowEnergyCharacteristic &charact } // resistance value is in another frame - if (lastPacket.length() == 5 && ((unsigned char)lastPacket.at(0)) == 0xf0 && ((unsigned char)lastPacket.at(1)) == 0xd2) { + if (lastPacket.length() == 5 && ((unsigned char)lastPacket.at(0)) == 0xf0 && + ((unsigned char)lastPacket.at(1)) == 0xd2) { Resistance = lastPacket.at(3); emit resistanceRead(Resistance.value()); m_pelotonResistance = bikeResistanceToPeloton(Resistance.value()); @@ -228,13 +235,19 @@ void echelonrower::characteristicChanged(const QLowEnergyCharacteristic &charact StrokesCount += (Cadence.value()) * ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())) / 60000; } - Speed = (0.37497622 * ((double)Cadence.value())) / 2.0; + // instant pace to km/h + if (((uint8_t)lastPacket.at(14)) > 0 && Cadence.value() > 0) + Speed = (60.0 / (double)((uint8_t)lastPacket.at(14))) * 30.0; + else + Speed = 0; + StrokesLength = ((Speed.value() / 60.0) * 1000.0) / Cadence.value(); // this is just to fill the tile, but it's quite useless since the machinery doesn't report it if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -256,24 +269,17 @@ void echelonrower::characteristicChanged(const QLowEnergyCharacteristic &charact #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); - bool virtual_device_rower = settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); if (ios_peloton_workaround && cadence && !virtual_device_rower && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -367,7 +373,7 @@ void echelonrower::stateChanged(QLowEnergyService::ServiceState state) { &echelonrower::descriptorWritten); // ******************************************* virtual bike/rower init ************************************* - if (!firstStateChanged && !virtualBike && !virtualRower + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -375,12 +381,16 @@ void echelonrower::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_rower = settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && !virtual_device_rower) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -391,13 +401,15 @@ void echelonrower::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { if (!virtual_device_rower) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, - bikeResistanceGain); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } else { qDebug() << QStringLiteral("creating virtual rower interface..."); - virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY); } } } @@ -490,15 +502,6 @@ bool echelonrower::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *echelonrower::VirtualBike() { - if (virtualBike) - return virtualBike; - else - return virtualRower; -} - -void *echelonrower::VirtualDevice() { return VirtualBike(); } - uint16_t echelonrower::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/echelonrower.h b/src/echelonrower.h index 0d3477471..96a5a0477 100644 --- a/src/echelonrower.h +++ b/src/echelonrower.h @@ -27,8 +27,6 @@ #include #include "rower.h" -#include "virtualbike.h" -#include "virtualrower.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -38,13 +36,10 @@ class echelonrower : public rower { Q_OBJECT public: echelonrower(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override{ return max_resistance; } + bool connected() override; private: const resistance_t max_resistance = 32; @@ -58,11 +53,9 @@ class echelonrower : public rower { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; - virtualrower *virtualRower = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/echelonstride.cpp b/src/echelonstride.cpp index 7a2c654e2..1ec34dee5 100644 --- a/src/echelonstride.cpp +++ b/src/echelonstride.cpp @@ -1,6 +1,8 @@ #include "echelonstride.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -51,11 +53,15 @@ void echelonstride::writeCharacteristic(uint8_t *data, uint8_t data_len, const Q return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -131,21 +137,26 @@ void echelonstride::update() { gattNotify2Characteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstInit && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &echelonstride::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &echelonstride::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &echelonstride::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstInit = 1; } @@ -168,7 +179,7 @@ void echelonstride::update() { requestSpeed = -1; } if (requestInclination != -100) { - if(requestInclination < 0) + if (requestInclination < 0) requestInclination = 0; if (requestInclination != currentInclination().value() && requestInclination >= 0 && requestInclination <= 15) { @@ -239,9 +250,11 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac if (((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd3) { - writeCharacteristic((uint8_t*)newValue.constData(), newValue.length(), "reply to d3", false, false); + writeCharacteristic((uint8_t *)newValue.constData(), newValue.length(), "reply to d3", false, false); - double miles = 1.60934; + double miles = 1; + if (settings.value(QZSettings::sole_treadmill_miles, QZSettings::default_sole_treadmill_miles).toBool()) + miles = 1.60934; // this line on iOS sometimes gives strange overflow values // uint16_t convertedData = (((uint16_t)newValue.at(3)) << 8) | (uint16_t)newValue.at(4); @@ -256,7 +269,7 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac qDebug() << "speed5" << convertedData; Speed = (((double)convertedData) / 1000.0) * miles; - if(Speed.value() > 0) + if (Speed.value() > 0) lastStart = 0; else lastStop = 0; @@ -264,12 +277,12 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value()); return; } else if (((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd2) { - writeCharacteristic((uint8_t*)newValue.constData(), newValue.length(), "reply to d2", false, false); + writeCharacteristic((uint8_t *)newValue.constData(), newValue.length(), "reply to d2", false, false); Inclination = (uint8_t)newValue.at(3); qDebug() << QStringLiteral("Current Inclination: ") + QString::number(Inclination.value()); return; } else if (((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd0) { - writeCharacteristic((uint8_t*)newValue.constData(), newValue.length(), "reply to d0", false, false); + writeCharacteristic((uint8_t *)newValue.constData(), newValue.length(), "reply to d0", false, false); return; } @@ -282,7 +295,8 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac if (!firstCharacteristicChanged) { if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) KCal += - ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) * + ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo( @@ -292,32 +306,25 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac (1000.0 / (lastTimeCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); } - if((uint8_t)newValue.at(1) == 0xD1 && newValue.length() > 11) + if ((uint8_t)newValue.at(1) == 0xD1 && newValue.length() > 11) #ifdef Q_OS_ANDROID - if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) - Heart = (uint8_t)KeepAwakeHelper::heart(); - else -#endif - { - if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { - - uint8_t heart = ((uint8_t)newValue.at(11)); - if (heart == 0) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else #endif - } else { - Heart = heart; + { + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + + uint8_t heart = ((uint8_t)newValue.at(11)); + if (heart == 0) { + update_hr_from_external(); + } else { + Heart = heart; + } } } - } + + cadenceFromAppleWatch(); qDebug() << QStringLiteral("Current Heart: ") + QString::number(Heart.value()); qDebug() << QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value()); @@ -334,7 +341,7 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac void echelonstride::btinit() { uint8_t initData0[] = {0xf0, 0xa4, 0x00, 0x94}; uint8_t initData1[] = {0xf0, 0xa1, 0x00, 0x91}; - uint8_t initData2[] = {0xf0, 0xa3, 0x00, 0x93}; + uint8_t initData2[] = {0xf0, 0xa3, 0x00, 0x93}; writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true); @@ -344,7 +351,7 @@ void echelonstride::btinit() { writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); - writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); initDone = true; } @@ -471,10 +478,6 @@ bool echelonstride::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *echelonstride::VirtualTreadMill() { return virtualTreadMill; } - -void *echelonstride::VirtualDevice() { return VirtualTreadMill(); } - bool echelonstride::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; @@ -488,4 +491,3 @@ bool echelonstride::autoStartWhenSpeedIsGreaterThenZero() { else return false; } - diff --git a/src/echelonstride.h b/src/echelonstride.h index 399ec1e65..2f4956a2f 100644 --- a/src/echelonstride.h +++ b/src/echelonstride.h @@ -27,8 +27,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -40,10 +38,7 @@ class echelonstride : public treadmill { echelonstride(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); bool connected() override; - - void *VirtualTreadMill(); double minStepInclination() override; - void *VirtualDevice() override; bool autoPauseWhenSpeedIsZero() override; bool autoStartWhenSpeedIsGreaterThenZero() override; @@ -75,8 +70,6 @@ class echelonstride : public treadmill { int64_t lastStop = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/eliterizer.cpp b/src/eliterizer.cpp index afc12c150..3812cac50 100644 --- a/src/eliterizer.cpp +++ b/src/eliterizer.cpp @@ -1,6 +1,4 @@ #include "eliterizer.h" -#include "ios/lockscreen.h" -#include "virtualbike.h" #include #include #include @@ -12,7 +10,7 @@ #ifdef Q_OS_ANDROID #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -57,11 +55,15 @@ void eliterizer::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStr timeout.singleShot(300, &loop, SLOT(quit())); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -291,10 +293,6 @@ bool eliterizer::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *eliterizer::VirtualBike() { return virtualBike; } - -void *eliterizer::VirtualDevice() { return VirtualBike(); } - uint16_t eliterizer::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/eliterizer.h b/src/eliterizer.h index 4ffe439d6..f9999ea48 100644 --- a/src/eliterizer.h +++ b/src/eliterizer.h @@ -30,7 +30,6 @@ #include "bike.h" #include "ftmsbike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -41,19 +40,15 @@ class eliterizer : public bike { Q_OBJECT public: eliterizer(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/elitesterzosmart.cpp b/src/elitesterzosmart.cpp index 9104dff32..f2de70723 100644 --- a/src/elitesterzosmart.cpp +++ b/src/elitesterzosmart.cpp @@ -1,6 +1,4 @@ #include "elitesterzosmart.h" -#include "ios/lockscreen.h" -#include "virtualbike.h" #include #include #include @@ -12,7 +10,6 @@ #ifdef Q_OS_ANDROID #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -42,11 +39,15 @@ void elitesterzosmart::writeCharacteristic(uint8_t *data, uint8_t data_len, cons timeout.singleShot(300, &loop, SLOT(quit())); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -118,7 +119,7 @@ void elitesterzosmart::stateChanged(QLowEnergyService::ServiceState state) { qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); } } - + gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId); gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId); Q_ASSERT(gattWriteCharacteristic.isValid()); @@ -240,10 +241,6 @@ bool elitesterzosmart::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *elitesterzosmart::VirtualBike() { return virtualBike; } - -void *elitesterzosmart::VirtualDevice() { return VirtualBike(); } - uint16_t elitesterzosmart::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/elitesterzosmart.h b/src/elitesterzosmart.h index df1e3fe0f..036f5f1ce 100644 --- a/src/elitesterzosmart.h +++ b/src/elitesterzosmart.h @@ -30,7 +30,6 @@ #include "bike.h" #include "ftmsbike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -41,19 +40,15 @@ class elitesterzosmart : public bike { Q_OBJECT public: elitesterzosmart(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/elliptical.cpp b/src/elliptical.cpp index 067f7de6f..03b3a026b 100644 --- a/src/elliptical.cpp +++ b/src/elliptical.cpp @@ -56,13 +56,29 @@ uint16_t elliptical::watts() { m_watt.setValue(watts); return m_watt.value(); } + +double elliptical::speedFromWatts() { + + QSettings settings; + double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + // calc Watts ref. https://alancouzens.com/blog/Run_Power.html + + double speed = 0; + if (wattsMetric().value() > 0) { + double vwatts = ((9.8 * weight) * (currentInclination().value() / 100.0)); + speed = 210.0 / ((wattsMetric().value() - vwatts) / 75.0 / weight * 1000.0); + speed = 60.0 / speed; + } + return speed; +} + void elliptical::changeResistance(resistance_t resistance) { lastRawRequestedResistanceValue = resistance; requestResistance = resistance + gears(); RequestedResistance = resistance + gears(); } -int8_t elliptical::gears() { return m_gears; } -void elliptical::setGears(int8_t gears) { +double elliptical::gears() { return m_gears; } +void elliptical::setGears(double gears) { QSettings settings; qDebug() << "setGears" << gears; m_gears = gears; diff --git a/src/elliptical.h b/src/elliptical.h index 00a421230..4b408f136 100644 --- a/src/elliptical.h +++ b/src/elliptical.h @@ -13,29 +13,30 @@ class elliptical : public bluetoothdevice { metric lastRequestedCadence(); metric lastRequestedResistance(); metric lastRequestedSpeed() { return RequestedSpeed; } - virtual metric currentInclination(); - virtual metric currentResistance(); + metric currentInclination() override; + metric currentResistance() override; virtual double requestedSpeed(); - virtual uint8_t fanSpeed(); - virtual double currentCrankRevolutions(); - virtual uint16_t lastCrankEventTime(); - virtual bool connected(); + uint8_t fanSpeed() override; + double currentCrankRevolutions() override; + uint16_t lastCrankEventTime() override; + bool connected() override; metric pelotonResistance(); virtual int pelotonToEllipticalResistance(int pelotonResistance); virtual bool inclinationAvailableByHardware(); - bluetoothdevice::BLUETOOTH_TYPE deviceType(); - void clearStats(); - void setPaused(bool p); - void setLap(); - uint16_t watts(); - void setGears(int8_t d); - int8_t gears(); + bluetoothdevice::BLUETOOTH_TYPE deviceType() override; + void clearStats() override; + void setPaused(bool p) override; + void setLap() override; + virtual uint16_t watts(); + double speedFromWatts(); + void setGears(double d); + double gears(); virtual double minStepInclination() { return 0.5; } public Q_SLOTS: virtual void changeSpeed(double speed); - virtual void changeResistance(resistance_t res); - virtual void changeInclination(double grade, double inclination); + void changeResistance(resistance_t res) override; + void changeInclination(double grade, double inclination) override; virtual void changeCadence(int16_t cad); virtual void changeRequestedPelotonResistance(int8_t resistance); @@ -54,7 +55,7 @@ class elliptical : public bluetoothdevice { volatile double requestSpeed = -1; double requestInclination = -100; double CrankRevs = 0; - int8_t m_gears = 0; + double m_gears = 0; resistance_t lastRawRequestedResistanceValue = -1; }; diff --git a/src/eslinkertreadmill.cpp b/src/eslinkertreadmill.cpp index 215f64d32..ce456272c 100644 --- a/src/eslinkertreadmill.cpp +++ b/src/eslinkertreadmill.cpp @@ -42,11 +42,16 @@ void eslinkertreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, con // &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic( - gattWriteCharacteristic, QByteArray((const char *)data, data_len), QLowEnergyService::WriteWithoutResponse); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -99,6 +104,16 @@ void eslinkertreadmill::forceSpeed(double requestSpeed) { display[4] = display[4] ^ display[i]; } + writeCharacteristic(display, sizeof(display), + QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, true); + } else if (treadmill_type == COSTAWAY) { + // CheckSum 8 Xor + uint8_t display[] = {0xa9, 0xa0, 0x03, 0x02, 0x00, 0x00, 0x00}; + display[4] = requestSpeed * 10; + for (int i = 0; i < 6; i++) { + display[6] = display[6] ^ display[i]; + } + writeCharacteristic(display, sizeof(display), QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, true); } @@ -122,13 +137,14 @@ void eslinkertreadmill::update() { gattNotifyCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &eslinkertreadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -142,7 +158,9 @@ void eslinkertreadmill::update() { updateDisplay(elapsed.value()); } - if (treadmill_type == TYPE::RHYTHM_FUN || treadmill_type == TYPE::YPOO_MINI_CHANGE) { // + if (treadmill_type == TYPE::RHYTHM_FUN || treadmill_type == TYPE::YPOO_MINI_CHANGE || + treadmill_type == TYPE::COSTAWAY) { + if (requestSpeed != -1) { if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); @@ -170,6 +188,9 @@ void eslinkertreadmill::update() { } requestInclination = -100; } + } else if (treadmill_type == COSTAWAY) { + uint8_t initData11[] = {0xa9, 0xa0, 0x03, 0x02, 0x06, 0x00, 0x0e}; + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("noop"), false, true); } else { if (requestVar2) { uint8_t display[] = {0x08, 0x04, 0x01, 0x00, 0x00, 0x01}; @@ -328,6 +349,8 @@ void eslinkertreadmill::characteristicChanged(const QLowEnergyCharacteristic &ch if ((newValue.length() != 17 && (treadmill_type == RHYTHM_FUN || treadmill_type == YPOO_MINI_CHANGE))) return; + else if (newValue.length() != 5 && treadmill_type == COSTAWAY) + return; if (treadmill_type == RHYTHM_FUN || treadmill_type == YPOO_MINI_CHANGE) { double speed = GetSpeedFromPacket(value); @@ -365,6 +388,14 @@ void eslinkertreadmill::characteristicChanged(const QLowEnergyCharacteristic &ch lastSpeed = speed; lastInclination = incline; } + } else if (treadmill_type == COSTAWAY) { + const double miles = 1.60934; + if(newValue.at(3) == 0xFF) + Speed = 0; + else + Speed = (double)((uint8_t)newValue.at(3)) / 10.0 * miles; + Inclination = 0; // this treadmill doesn't have inclination + emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); } if (!firstCharacteristicChanged) { @@ -433,23 +464,46 @@ double eslinkertreadmill::GetInclinationFromPacket(const QByteArray &packet) { void eslinkertreadmill::btinit(bool startTape) { Q_UNUSED(startTape) - // set speed and incline to 0 - uint8_t initData1[] = {0x08, 0x01, 0x86}; - uint8_t initData2[] = {0xa9, 0x08, 0x01, 0x86, 0x26}; - uint8_t initData3[] = {0xa9, 0x80, 0x05, 0x05, 0xb0, 0x04, 0x52, 0xa9, 0x66}; - uint8_t initData4[] = {0xa9, 0x08, 0x04, 0xb2, 0x51, 0x03, 0x52, 0x17}; - uint8_t initData5[] = {0xa9, 0x1e, 0x01, 0xfe, 0x48}; - uint8_t initData6[] = {0xa9, 0x0a, 0x01, 0x01, 0xa3}; - uint8_t initData7[] = {0xa9, 0xf0, 0x01, 0x01, 0x59}; - uint8_t initData8[] = {0xa9, 0xa0, 0x03, 0xff, 0x00, 0x00, 0xf5}; - uint8_t initData9[] = {0xa9, 0xa0, 0x03, 0x00, 0x00, 0x00, 0x0a}; - uint8_t initData10[] = {0xa9, 0xa0, 0x03, 0x01, 0x00, 0x00, 0x0b}; - uint8_t initData11[] = {0xa9, 0x01, 0x01, 0x08, 0xa1}; - uint8_t initData12[] = {0xa9, 0xa0, 0x03, 0x02, 0x08, 0x00, 0x00}; - - uint8_t initData2_CADENZA[] = {0x08, 0x01, 0x01}; - if (treadmill_type == RHYTHM_FUN || treadmill_type == YPOO_MINI_CHANGE) { + if (treadmill_type == COSTAWAY) { + uint8_t initData1[] = {0xa9, 0xf2, 0x01, 0x2f, 0x75}; + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + + uint8_t initData2[] = {0xa9, 0x08, 0x01, 0x79, 0xd9}; + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); + + uint8_t initData3[] = {0xa9, 0x08, 0x04, 0x05, 0x04, 0x04, 0x01, 0xa1}; + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true); + + uint8_t initData4[] = {0xa9, 0x1e, 0x01, 0xfe, 0x48}; + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, true); + + uint8_t initData5[] = {0xa9, 0xa0, 0x03, 0x00, 0x00, 0x00, 0x0a}; + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + + uint8_t initData6[] = {0xa9, 0x0a, 0x01, 0x48, 0xea}; + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, true); + + uint8_t initData7[] = {0xa9, 0xae, 0x01, 0xfe, 0xf8}; + writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, true); + + uint8_t initData8[] = {0xa9, 0xa0, 0x03, 0x00, 0x00, 0x00, 0x0a}; + writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, true); + } else if (treadmill_type == RHYTHM_FUN || treadmill_type == YPOO_MINI_CHANGE) { + // set speed and incline to 0 + uint8_t initData1[] = {0x08, 0x01, 0x86}; + uint8_t initData2[] = {0xa9, 0x08, 0x01, 0x86, 0x26}; + uint8_t initData3[] = {0xa9, 0x80, 0x05, 0x05, 0xb0, 0x04, 0x52, 0xa9, 0x66}; + uint8_t initData4[] = {0xa9, 0x08, 0x04, 0xb2, 0x51, 0x03, 0x52, 0x17}; + uint8_t initData5[] = {0xa9, 0x1e, 0x01, 0xfe, 0x48}; + uint8_t initData6[] = {0xa9, 0x0a, 0x01, 0x01, 0xa3}; + uint8_t initData7[] = {0xa9, 0xf0, 0x01, 0x01, 0x59}; + uint8_t initData8[] = {0xa9, 0xa0, 0x03, 0xff, 0x00, 0x00, 0xf5}; + uint8_t initData9[] = {0xa9, 0xa0, 0x03, 0x00, 0x00, 0x00, 0x0a}; + uint8_t initData10[] = {0xa9, 0xa0, 0x03, 0x01, 0x00, 0x00, 0x0b}; + uint8_t initData11[] = {0xa9, 0x01, 0x01, 0x08, 0xa1}; + uint8_t initData12[] = {0xa9, 0xa0, 0x03, 0x02, 0x08, 0x00, 0x00}; + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true); @@ -467,6 +521,7 @@ void eslinkertreadmill::btinit(bool startTape) { writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, true); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, true); } else { + uint8_t initData2_CADENZA[] = {0x08, 0x01, 0x01}; writeCharacteristic(initData2_CADENZA, sizeof(initData2_CADENZA), QStringLiteral("init"), false, true); } @@ -576,10 +631,14 @@ void eslinkertreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { bool eslinker_cadenza = settings.value(QZSettings::eslinker_cadenza, QZSettings::default_eslinker_cadenza).toBool(); bool eslinker_ypoo = settings.value(QZSettings::eslinker_ypoo, QZSettings::default_eslinker_ypoo).toBool(); + bool eslinker_costaway = + settings.value(QZSettings::eslinker_costaway, QZSettings::default_eslinker_costaway).toBool(); if (eslinker_cadenza) { treadmill_type = CADENZA_FITNESS_T45; } else if (eslinker_ypoo) { treadmill_type = YPOO_MINI_CHANGE; + } else if (eslinker_costaway) { + treadmill_type = COSTAWAY; } else treadmill_type = RHYTHM_FUN; @@ -605,10 +664,6 @@ bool eslinkertreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *eslinkertreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *eslinkertreadmill::VirtualDevice() { return VirtualTreadMill(); } - bool eslinkertreadmill::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/eslinkertreadmill.h b/src/eslinkertreadmill.h index f454f5576..8a6edfd1c 100644 --- a/src/eslinkertreadmill.h +++ b/src/eslinkertreadmill.h @@ -26,20 +26,16 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class eslinkertreadmill : public treadmill { Q_OBJECT public: eslinkertreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadMill(); - void *VirtualDevice(); + bool connected() override; + double minStepInclination() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -68,7 +64,8 @@ class eslinkertreadmill : public treadmill { typedef enum TYPE { RHYTHM_FUN = 0, CADENZA_FITNESS_T45 = 1, // it has the same protocol of RHYTHM_FUN but without the header and the footer - YPOO_MINI_CHANGE = 2, // Similar to RHYTHM_FUN but has no ascension + YPOO_MINI_CHANGE = 2, // Similar to RHYTHM_FUN but has no ascension + COSTAWAY = 3, } TYPE; volatile TYPE treadmill_type = RHYTHM_FUN; @@ -76,8 +73,6 @@ class eslinkertreadmill : public treadmill { int64_t lastStop = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; QLowEnergyCharacteristic gattNotifyCharacteristic; diff --git a/src/fakebike.cpp b/src/fakebike.cpp index 67473d17a..6bc0f6e09 100644 --- a/src/fakebike.cpp +++ b/src/fakebike.cpp @@ -1,6 +1,6 @@ #include "fakebike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" + #include #include #include @@ -9,9 +9,9 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -32,39 +32,63 @@ void fakebike::update() { QSettings settings; QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); - /* static int updcou = 0; updcou++; - double w = 250.0; + double w = 60.0; if (updcou > 20000 ) updcou = 0; else if (updcou > 12000) - w = 300; + w = 120; else if (updcou > 6000) - w = 150; + w = 80; + Speed = metric::calculateSpeedFromPower(w, Inclination.value(), + Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), speedLimit()); + */ + + if (requestPower != -1) { + // bepo70: don't know if this conversion is really needed, i would do it anyway. + m_watt = (double)requestPower; + emit debug(QStringLiteral("writing power ") + QString::number(requestPower)); + requestPower = -1; + // bepo70: Disregard the current inclination for calculating speed. When the video + // has a high inclination you have to give many power to get the desired playback speed, + // if inclination is very low little more power gives a quite high speed jump. + // Speed = metric::calculateSpeedFromPower(m_watt.value(), Inclination.value(), + // Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), speedLimit()); + Speed = metric::calculateSpeedFromPower( + m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), + speedLimit()); + } - Speed = metric::calculateSpeedFromPower(w, Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), speedLimit());*/ + if (requestInclination != -100) { + Inclination = requestInclination; + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + requestInclination = -100; + } - update_metrics(true, watts()); + update_metrics(false, watts()); Distance += ((Speed.value() / (double)3600.0) / ((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike && !noVirtualDevice + if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h #endif #endif ) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -74,9 +98,10 @@ void fakebike::update() { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); connect(virtualBike, &virtualbike::changeInclination, this, &fakebike::changeInclinationRequested); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &fakebike::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } if (!firstStateChanged) @@ -92,22 +117,14 @@ void fakebike::update() { } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } - #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -117,7 +134,10 @@ void fakebike::update() { } if (Heart.value()) { - KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()); + static double lastKcal = 0; + if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator + lastKcal = abs(KCal.value()); + KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal; } if (requestResistance != -1 && requestResistance != currentResistance().value()) { @@ -138,7 +158,3 @@ void fakebike::changeInclinationRequested(double grade, double percentage) { } bool fakebike::connected() { return true; } - -void *fakebike::VirtualBike() { return virtualBike; } - -void *fakebike::VirtualDevice() { return VirtualBike(); } diff --git a/src/fakebike.h b/src/fakebike.h index bb08aa1e8..dab09d493 100644 --- a/src/fakebike.h +++ b/src/fakebike.h @@ -28,7 +28,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -38,14 +37,10 @@ class fakebike : public bike { Q_OBJECT public: fakebike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t sec1Update = 0; QByteArray lastPacket; diff --git a/src/fakeelliptical.cpp b/src/fakeelliptical.cpp index 51ec38ee1..6faaa0773 100644 --- a/src/fakeelliptical.cpp +++ b/src/fakeelliptical.cpp @@ -1,5 +1,4 @@ #include "fakeelliptical.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -9,9 +8,9 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -45,7 +44,7 @@ void fakeelliptical::update() { lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike && !noVirtualDevice + if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -53,11 +52,14 @@ void fakeelliptical::update() { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -67,10 +69,11 @@ void fakeelliptical::update() { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); connect(virtualBike, &virtualbike::changeInclination, this, &fakeelliptical::changeInclinationRequested); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &fakeelliptical::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } if (!firstStateChanged) @@ -86,22 +89,15 @@ void fakeelliptical::update() { } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -111,7 +107,10 @@ void fakeelliptical::update() { } if (Heart.value()) { - KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()); + static double lastKcal = 0; + if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator + lastKcal = abs(KCal.value()); + KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal; } if (requestResistance != -1 && requestResistance != currentResistance().value()) { @@ -132,7 +131,3 @@ void fakeelliptical::changeInclinationRequested(double grade, double percentage) } bool fakeelliptical::connected() { return true; } - -void *fakeelliptical::VirtualBike() { return virtualBike; } - -void *fakeelliptical::VirtualDevice() { return VirtualBike(); } diff --git a/src/fakeelliptical.h b/src/fakeelliptical.h index 77cf40a2d..cae162523 100644 --- a/src/fakeelliptical.h +++ b/src/fakeelliptical.h @@ -27,7 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,14 +36,10 @@ class fakeelliptical : public elliptical { Q_OBJECT public: fakeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t sec1Update = 0; QByteArray lastPacket; diff --git a/src/fakerower.cpp b/src/fakerower.cpp new file mode 100644 index 000000000..8197479c9 --- /dev/null +++ b/src/fakerower.cpp @@ -0,0 +1,136 @@ +#include "fakerower.h" +#include "virtualbike.h" +#include "virtualrower.h" + +#include +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#include +#endif +#include + +using namespace std::chrono_literals; + +fakerower::fakerower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) { + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->noVirtualDevice = noVirtualDevice; + initDone = false; + connect(refresh, &QTimer::timeout, this, &fakerower::update); + refresh->start(200ms); +} + +void fakerower::update() { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + + update_metrics(false, watts()); + + Distance += ((Speed.value() / (double)3600.0) / + ((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + QSettings settings; + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && !virtual_device_rower) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + if (virtual_device_enabled) { + if (!virtual_device_rower) { + qDebug() << QStringLiteral("creating virtual bike interface..."); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, 0, 1); + // connect(virtualBike,&virtualbike::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + qDebug() << QStringLiteral("creating virtual rower interface..."); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + } + if (!firstStateChanged) + emit connectedAndDiscovered(); + firstStateChanged = 1; + // ******************************************************************************************************** + + if (!noVirtualDevice) { +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + Heart = (uint8_t)KeepAwakeHelper::heart(); + debug("Current Heart: " + QString::number(Heart.value())); + } +#endif + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); + } +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + } + + if (Heart.value()) { + static double lastKcal = 0; + if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator + lastKcal = abs(KCal.value()); + KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal; + } + + if (requestResistance != -1 && requestResistance != currentResistance().value()) { + Resistance = requestResistance; + m_pelotonResistance = requestResistance; + } +} + +void fakerower::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + QByteArray b = newValue; + qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' '); +} + +void fakerower::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} + +bool fakerower::connected() { return true; } diff --git a/src/fakerower.h b/src/fakerower.h new file mode 100644 index 000000000..7bfa6db7d --- /dev/null +++ b/src/fakerower.h @@ -0,0 +1,75 @@ +#ifndef FAKEROWER_H +#define FAKEROWER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include + +#include "rower.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class fakerower : public rower { + Q_OBJECT + public: + fakerower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); + bool connected() override; + + private: + QTimer *refresh; + + uint8_t sec1Update = 0; + QByteArray lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastGoodCadence = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + bool noVirtualDevice = false; + + uint16_t oldLastCrankEventTime = 0; + uint16_t oldCrankRevs = 0; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); + + private slots: + void changeInclinationRequested(double grade, double percentage); + void update(); + + void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); +}; + +#endif // FAKEROWER_H diff --git a/src/faketreadmill.cpp b/src/faketreadmill.cpp index 6111c540f..5396164f1 100644 --- a/src/faketreadmill.cpp +++ b/src/faketreadmill.cpp @@ -1,5 +1,4 @@ #include "faketreadmill.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -9,9 +8,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -54,7 +54,7 @@ void faketreadmill::update() { lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill && !virtualBike) { + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -63,14 +63,16 @@ void faketreadmill::update() { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &faketreadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &faketreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &faketreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } } if (!firstStateChanged) @@ -87,16 +89,7 @@ void faketreadmill::update() { } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS @@ -114,7 +107,10 @@ void faketreadmill::update() { } if (Heart.value()) { - KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()); + static double lastKcal = 0; + if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator + lastKcal = abs(KCal.value()); + KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal; } } @@ -131,9 +127,3 @@ void faketreadmill::changeInclinationRequested(double grade, double percentage) } bool faketreadmill::connected() { return true; } - -void *faketreadmill::VirtualBike() { return virtualBike; } - -void *faketreadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *faketreadmill::VirtualDevice() { return VirtualTreadmill(); } diff --git a/src/faketreadmill.h b/src/faketreadmill.h index a49e60539..63e66c67e 100644 --- a/src/faketreadmill.h +++ b/src/faketreadmill.h @@ -38,16 +38,10 @@ class faketreadmill : public treadmill { Q_OBJECT public: faketreadmill(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); - bool connected(); - - void *VirtualBike(); - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: QTimer *refresh; - virtualbike *virtualBike = nullptr; - virtualtreadmill *virtualTreadmill = nullptr; uint8_t sec1Update = 0; QByteArray lastPacket; diff --git a/src/fitmetria_fanfit.cpp b/src/fitmetria_fanfit.cpp index 696c84f70..198de9bf2 100644 --- a/src/fitmetria_fanfit.cpp +++ b/src/fitmetria_fanfit.cpp @@ -208,16 +208,19 @@ void fitmetria_fanfit::writeCharacteristic(uint8_t *data, uint8_t data_len, cons return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } - // not necessary, since the communication is one way only. also it could lead to crashes - // loop.exec(); + loop.exec(); } void fitmetria_fanfit::stateChanged(QLowEnergyService::ServiceState state) { diff --git a/src/fitmetria_fanfit.h b/src/fitmetria_fanfit.h index ad2165458..c5a2f1641 100644 --- a/src/fitmetria_fanfit.h +++ b/src/fitmetria_fanfit.h @@ -31,7 +31,7 @@ class fitmetria_fanfit : public bluetoothdevice { Q_OBJECT public: fitmetria_fanfit(bluetoothdevice *parentDevice); - bool connected(); + bool connected() override; private: QLowEnergyService *gattCommunicationChannelService = nullptr; diff --git a/src/fitplusbike.cpp b/src/fitplusbike.cpp index 81dab2d04..77cacf2b4 100644 --- a/src/fitplusbike.cpp +++ b/src/fitplusbike.cpp @@ -1,6 +1,4 @@ #include "fitplusbike.h" -#include "ios/lockscreen.h" -#include "keepawakehelper.h" #include "virtualbike.h" #include #include @@ -10,6 +8,10 @@ #include #include +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif + using namespace std::chrono_literals; #ifdef Q_OS_IOS @@ -65,12 +67,20 @@ void fitplusbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); + } if (!disable_log) - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info; + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; loop.exec(); } @@ -267,7 +277,7 @@ void fitplusbike::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && gattNotify1Characteristic.isValid() && initDone) { QSettings settings; - update_metrics(true, watts()); + update_metrics(false, watts()); bool virtufit_etappe = settings.value(QZSettings::virtufit_etappe, QZSettings::default_virtufit_etappe).toBool(); bool sportstech_sx600 = @@ -276,25 +286,8 @@ void fitplusbike::update() { if (virtufit_etappe || merach_MRK || sportstech_sx600) { } else { - - if (Heart.value() > 0) { - int avgP = ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() * - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()) - - (settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble() * - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble())) / - (settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble() - - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble()) + - (Heart.value() * - ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() - - settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble()) / - (settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble() - - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()))); - if (avgP < 50) { - avgP = 50; - } - m_watt = avgP; - qDebug() << QStringLiteral("Current Watt: ") + QString::number(m_watt.value()); - } + m_watt = wattFromHR(false); + qDebug() << QStringLiteral("Current Watt: ") + QString::number(m_watt.value()); } // sending poll every 2 seconds @@ -597,7 +590,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte .toString() .startsWith(QStringLiteral("Disabled"))) Cadence = ((uint8_t)newValue.at(6)); - m_watt = (double)((((uint8_t)newValue.at(4)) << 8) | ((uint8_t)newValue.at(3))) / 10.0; + m_watt = (double)((((uint8_t)newValue.at(10)) << 8) | ((uint8_t)newValue.at(9))) / 10.0; /*if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) Speed = (double)((((uint8_t)newValue.at(4)) << 10) | ((uint8_t)newValue.at(9))) / 100.0; @@ -660,16 +653,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } @@ -691,6 +675,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte qDebug() << QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs); qDebug() << QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime); qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts()); + qDebug() << QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()); if (m_control->error() != QLowEnergyController::NoError) { qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); @@ -814,7 +799,7 @@ void fitplusbike::stateChanged(QLowEnergyService::ServiceState state) { &fitplusbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -839,10 +824,11 @@ void fitplusbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&fitplusbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &fitplusbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -932,6 +918,7 @@ void fitplusbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { bluetoothDevice = device; if (device.name().startsWith(QStringLiteral("MRK-"))) { + qDebug() << QStringLiteral("merach_MRK workaround enabled!"); merach_MRK = true; } @@ -975,10 +962,6 @@ bool fitplusbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *fitplusbike::VirtualBike() { return virtualBike; } - -void *fitplusbike::VirtualDevice() { return VirtualBike(); } - uint16_t fitplusbike::watts() { if (currentCadence().value() == 0) { return 0; @@ -996,3 +979,117 @@ void fitplusbike::controllerStateChanged(QLowEnergyController::ControllerState s m_control->connectToDevice(); } } + +uint16_t fitplusbike::wattsFromResistance(double resistance) { + // https://github.com/cagnulein/qdomyos-zwift/issues/62#issuecomment-736913564 + /*if(currentCadence().value() < 90) + return (uint16_t)((3.59 * exp(0.0217 * (double)(currentCadence().value()))) * exp(0.095 * + (double)(currentResistance().value())) ); else return (uint16_t)((3.59 * exp(0.0217 * + (double)(currentCadence().value()))) * exp(0.088 * (double)(currentResistance().value())) );*/ + + const double Epsilon = 4.94065645841247E-324; + + if (merach_MRK) { + const int wattTableFirstDimension = 17; + const int wattTableSecondDimension = 11; + double wattTable[wattTableFirstDimension][wattTableSecondDimension] = { + {Epsilon, 14.3, 28.6, 42.9, 57.2, 71.5, 85.8, 100.1, 114.4, 128.7, 143.0}, + {Epsilon, 14.3, 28.6, 42.9, 57.2, 71.5, 85.8, 100.1, 114.4, 128.7, 143.0}, + {Epsilon, 16.4, 32.8, 49.2, 65.6, 82.0, 98.4, 114.8, 131.2, 147.6, 164.0}, + {Epsilon, 18.7, 37.4, 56.1, 74.8, 93.5, 112.2, 130.9, 149.6, 168.3, 187.0}, + {Epsilon, 21.0, 42.0, 63.0, 84.0, 105.0, 126.0, 147.0, 168.0, 189.0, 210.0}, + {Epsilon, 23.2, 46.4, 69.6, 92.8, 116.0, 139.2, 162.4, 185.6, 208.8, 232.0}, + {Epsilon, 25.3, 50.6, 75.9, 101.2, 126.5, 151.8, 177.1, 202.4, 227.7, 253.0}, + {Epsilon, 27.6, 55.2, 82.8, 110.4, 138.0, 165.6, 193.2, 220.8, 248.4, 276.0}, + {Epsilon, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 270.0, 300.0}, + {Epsilon, 31.9, 63.8, 95.7, 127.6, 159.5, 191.4, 223.3, 255.2, 287.1, 319.0}, + {Epsilon, 34.2, 68.4, 102.6, 136.8, 171.0, 205.2, 239.4, 273.6, 307.8, 342.0}, + {Epsilon, 36.5, 73.0, 109.5, 146.0, 182.5, 219.0, 255.5, 292.0, 328.5, 365.0}, + {Epsilon, 38.5, 77.0, 115.5, 154.0, 192.5, 231.0, 269.5, 308.0, 346.5, 385.0}, + {Epsilon, 40.8, 81.6, 122.4, 163.2, 204.0, 244.8, 285.6, 326.4, 367.2, 408.0}, + {Epsilon, 43.1, 86.2, 129.3, 172.4, 215.5, 258.6, 301.7, 344.8, 387.9, 431.0}, + {Epsilon, 45.1, 90.2, 135.3, 180.4, 225.5, 270.6, 315.7, 360.8, 405.9, 451.0}, + {Epsilon, 47.2, 94.4, 141.6, 188.8, 236.0, 283.2, 330.4, 377.6, 424.8, 472.0}}; + + int level = resistance; + if (level < 0) { + level = 0; + } + if (level >= wattTableFirstDimension) { + level = wattTableFirstDimension - 1; + } + double *watts_of_level = wattTable[level]; + int watt_setp = (Cadence.value() / 10.0); + if (watt_setp >= 10) { + return (((double)Cadence.value()) / 100.0) * watts_of_level[wattTableSecondDimension - 1]; + } + double watt_base = watts_of_level[watt_setp]; + return (((watts_of_level[watt_setp + 1] - watt_base) / 10.0) * ((double)(((int)(Cadence.value())) % 10))) + + watt_base; + } else { + // VirtuFit Etappe 2.0i Spinbike ERG Table #1526 + const int wattTableFirstDimension = 25; + const int wattTableSecondDimension = 11; + double wattTable[wattTableFirstDimension][wattTableSecondDimension] = { + {Epsilon, 15.0, 15.0, 15.0, 20.0, 30.0, 32.0, 38.0, 44.0, 56.0, 66.0}, + {Epsilon, 15.0, 15.0, 15.0, 20.0, 30.0, 32.0, 38.0, 44.0, 56.0, 66.0}, + {Epsilon, 16.0, 16.0, 16.0, 22.0, 30.0, 38.0, 45.0, 53.0, 67.0, 79.0}, + {Epsilon, 18.0, 18.0, 18.0, 26.0, 34.0, 43.0, 52.0, 62.0, 78.0, 92.0}, + {Epsilon, 20.0, 20.0, 20.0, 28.0, 38.0, 48.0, 59.0, 71.0, 89.0, 105.0}, + {Epsilon, 23.0, 23.0, 23.0, 32.0, 43.0, 54.0, 66.0, 80.0, 100.0, 118.0}, + {Epsilon, 24.0, 24.0, 24.0, 35.0, 46.0, 59.0, 73.0, 89.0, 110.0, 130.0}, + {Epsilon, 26.0, 26.0, 26.0, 37.0, 51.0, 65.0, 81.0, 98.0, 122.0, 143.0}, + {Epsilon, 28.0, 28.0, 28.0, 41.0, 56.0, 71.0, 88.0, 107.0, 133.0, 156.0}, + {Epsilon, 30.0, 30.0, 30.0, 44.0, 60.0, 77.0, 96.0, 116.0, 144.0, 169.0}, + {Epsilon, 33.0, 33.0, 33.0, 47.0, 65.0, 83.0, 103.0, 125.0, 155.0, 182.0}, + {Epsilon, 34.0, 34.0, 34.0, 50.0, 70.0, 89.0, 110.0, 134.0, 166.0, 195.0}, + {Epsilon, 37.0, 37.0, 37.0, 54.0, 74.0, 94.0, 117.0, 143.0, 177.0, 208.0}, + {Epsilon, 38.0, 38.0, 38.0, 56.0, 78.0, 100.0, 125.0, 152.0, 188.0, 220.0}, + {Epsilon, 41.0, 41.0, 41.0, 60.0, 82.0, 106.0, 132.0, 161.0, 199.0, 233.0}, + {Epsilon, 43.0, 43.0, 43.0, 62.0, 86.0, 111.0, 139.0, 170.0, 209.0, 245.0}, + {Epsilon, 45.0, 45.0, 45.0, 66.0, 91.0, 117.0, 147.0, 180.0, 220.0, 259.0}, + {Epsilon, 48.0, 48.0, 48.0, 70.0, 96.0, 124.0, 155.0, 190.0, 232.0, 273.0}, + {Epsilon, 50.0, 50.0, 50.0, 73.0, 101.0, 130.0, 163.0, 200.0, 244.0, 287.0}, + {Epsilon, 52.0, 52.0, 52.0, 76.0, 106.0, 136.0, 171.0, 210.0, 256.0, 300.0}, + {Epsilon, 54.0, 54.0, 54.0, 80.0, 111.0, 143.0, 179.0, 220.0, 268.0, 314.0}, + {Epsilon, 57.0, 57.0, 57.0, 84.0, 116.0, 149.0, 187.0, 230.0, 279.0, 327.0}, + {Epsilon, 59.0, 59.0, 59.0, 87.0, 121.0, 155.0, 195.0, 240.0, 290.0, 340.0}, + {Epsilon, 62.0, 62.0, 62.0, 91.0, 126.0, 162.0, 203.0, 250.0, 302.0, 353.0}, + {Epsilon, 64.0, 64.0, 64.0, 94.0, 130.0, 168.0, 211.0, 260.0, 313.0, 366.0}}; + + int level = resistance; + if (level < 0) { + level = 0; + } + if (level >= wattTableFirstDimension) { + level = wattTableFirstDimension - 1; + } + double *watts_of_level = wattTable[level]; + int watt_setp = (Cadence.value() / 10.0); + if (watt_setp >= 10) { + return (((double)Cadence.value()) / 100.0) * watts_of_level[wattTableSecondDimension - 1]; + } + double watt_base = watts_of_level[watt_setp]; + return (((watts_of_level[watt_setp + 1] - watt_base) / 10.0) * ((double)(((int)(Cadence.value())) % 10))) + + watt_base; + } +} + +resistance_t fitplusbike::resistanceFromPowerRequest(uint16_t power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value(); + + if (Cadence.value() == 0) + return 1; + + for (resistance_t i = 1; i < max_resistance; i++) { + if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i) + << wattsFromResistance(i + 1) << power; + return i; + } + } + if (power < wattsFromResistance(1)) + return 1; + else + return max_resistance; +} diff --git a/src/fitplusbike.h b/src/fitplusbike.h index 59a3c2afd..baec56efb 100644 --- a/src/fitplusbike.h +++ b/src/fitplusbike.h @@ -37,11 +37,9 @@ class fitplusbike : public bike { Q_OBJECT public: fitplusbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; private: resistance_t max_resistance = 24; @@ -51,10 +49,10 @@ class fitplusbike : public bike { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; + uint16_t wattsFromResistance(double resistance); QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyService *gattCommunicationChannelServiceFTMS = nullptr; diff --git a/src/fitshowtreadmill.cpp b/src/fitshowtreadmill.cpp index 85eb01580..6b5645be9 100644 --- a/src/fitshowtreadmill.cpp +++ b/src/fitshowtreadmill.cpp @@ -1,6 +1,7 @@ #include "fitshowtreadmill.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include @@ -44,9 +45,6 @@ fitshowtreadmill::~fitshowtreadmill() { refresh->stop(); delete refresh; } - if (virtualTreadMill) { - delete virtualTreadMill; - } #if defined(Q_OS_IOS) && !defined(IO_UNDER_QT) if (h) delete h; @@ -62,19 +60,24 @@ void fitshowtreadmill::scheduleWrite(const uint8_t *data, uint8_t data_len, cons void fitshowtreadmill::writeCharacteristic(const uint8_t *data, uint8_t data_len, const QString &info) { QEventLoop loop; QTimer timeout; - QByteArray qba((const char *)data, data_len); - if (!info.isEmpty()) { - emit debug(QStringLiteral(" >>") + qba.toHex(' ') + QStringLiteral(" // ") + info); - } connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (!info.isEmpty()) { + emit debug(QStringLiteral(" >>") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); + } + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) { - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, qba, + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, QLowEnergyService::WriteWithoutResponse); } else { - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, qba); + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); } loop.exec(); @@ -161,16 +164,16 @@ void fitshowtreadmill::update() { gattNotifyCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && searchStopped && !virtualTreadMill) { + if (!firstInit && searchStopped && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &fitshowtreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &fitshowtreadmill::changeInclinationRequested); - + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -183,18 +186,11 @@ void fitshowtreadmill::update() { if (requestSpeed != -1) { if (requestSpeed != currentSpeed().value()) { emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); - double inc = currentInclination().value(); + double inc = rawInclination.value(); if (requestInclination != -100) { - int diffInc = (int)(requestInclination - inc); - if (!diffInc) { - if (requestInclination > inc) { - inc += 1.0; - } else if (requestInclination < inc) { - inc -= 1.0; - } - } else { - inc = (int)requestInclination; - } + // only 0.5 or 1 changes otherwise it beeps forever + double a = 1.0 / minStepInclination(); + inc = qRound(treadmillInclinationOverrideReverse(requestInclination) * a) / a; requestInclination = -100; } forceSpeedOrIncline(requestSpeed, inc); @@ -203,19 +199,13 @@ void fitshowtreadmill::update() { } if (requestInclination != -100) { - double inc = currentInclination().value(); + double inc = rawInclination.value(); + // only 0.5 or 1 changes otherwise it beeps forever + double a = 1.0 / minStepInclination(); + requestInclination = qRound(treadmillInclinationOverrideReverse(requestInclination) * a) / a; if (requestInclination != inc) { emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); - int diffInc = (int)(requestInclination - inc); - if (!diffInc) { - if (requestInclination > inc) { - inc += 1.0; - } else if (requestInclination < inc) { - inc -= 1.0; - } - } else { - inc = (int)requestInclination; - } + inc = requestInclination; double speed = currentSpeed().value(); if (requestSpeed != -1) { speed = requestSpeed; @@ -286,7 +276,8 @@ void fitshowtreadmill::serviceDiscovered(const QBluetoothUuid &gatt) { QBluetoothUuid nobleproconnect(QStringLiteral("0000ae00-0000-1000-8000-00805f9b34fb")); emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString() + QStringLiteral(" ") + QString::number(servRepr)); - if (gatt == nobleproconnect || servRepr == 0xfff0 || (servRepr == 0xffe0 && serviceId.isNull())) { + if ((gatt == nobleproconnect && serviceId.isNull()) || servRepr == 0xfff0 || (servRepr == 0xffe0 && serviceId.isNull())) { + qDebug() << "adding" << gatt.toString() << "as the default service"; serviceId = gatt; // NOTE: clazy-rule-of-tow } } @@ -456,7 +447,6 @@ void fitshowtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current elapsed from treadmill: ") + QString::number(seconds_elapsed)); emit debug(QStringLiteral("Current speed: ") + QString::number(speed)); - emit debug(QStringLiteral("Current incline: ") + QString::number(incline)); emit debug(QStringLiteral("Current heart: ") + QString::number(heart)); emit debug(QStringLiteral("Current Distance: ") + QString::number(distance)); emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(DistanceCalculated)); @@ -481,18 +471,24 @@ void fitshowtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha .toBool()) miles = 1.60934; - Speed = speed * miles; + if(IS_RUNNING) + Speed = speed * miles; + else + Speed = 0; + if (Speed.value() != speed) { emit speedChanged(speed); } - - if(noblepro_connected) + + if (noblepro_connected) incline /= 2; - + + rawInclination = incline; Inclination = treadmillInclinationOverride(incline); if (Inclination.value() != incline) { emit inclinationChanged(0, incline); } + emit debug(QStringLiteral("Current incline: ") + QString::number(Inclination.value())); if (truetimer) elapsed = seconds_elapsed; @@ -508,6 +504,9 @@ void fitshowtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha long appleWatchHeartRate = h->heartRate(); h->setKcal(KCal.value()); h->setDistance(Distance.value()); + h->setSpeed(Speed.value()); + h->setPower(m_watt.value()); + h->setCadence(Cadence.value()); Heart = appleWatchHeartRate; debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); #else @@ -816,10 +815,6 @@ bool fitshowtreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *fitshowtreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *fitshowtreadmill::VirtualDevice() { return VirtualTreadMill(); } - void fitshowtreadmill::searchingStop() { searchStopped = true; } void fitshowtreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { diff --git a/src/fitshowtreadmill.h b/src/fitshowtreadmill.h index e69200ecf..b36868740 100644 --- a/src/fitshowtreadmill.h +++ b/src/fitshowtreadmill.h @@ -82,13 +82,10 @@ class fitshowtreadmill : public treadmill { fitshowtreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); virtual ~fitshowtreadmill(); - bool connected(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - double minStepInclination(); - - void *VirtualTreadMill(); - void *VirtualDevice(); + bool connected() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; + double minStepInclination() override; private: bool checkIncomingPacket(const uint8_t *data, uint8_t data_len) const; @@ -144,7 +141,6 @@ class fitshowtreadmill : public treadmill { QByteArray bufferWrite; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; @@ -156,6 +152,8 @@ class fitshowtreadmill : public treadmill { double minStepInclinationValue = 1.0; bool noblepro_connected = false; + metric rawInclination; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/flywheelbike.cpp b/src/flywheelbike.cpp index cbcf780ec..e461ace45 100644 --- a/src/flywheelbike.cpp +++ b/src/flywheelbike.cpp @@ -1,6 +1,8 @@ #include "flywheelbike.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -35,11 +37,15 @@ void flywheelbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QS timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -187,7 +193,8 @@ void flywheelbike::updateStats() { // calculate the accumulator every time on the current data, in order to avoid holes in peloton or strava if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -203,22 +210,14 @@ void flywheelbike::updateStats() { lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -245,7 +244,8 @@ void flywheelbike::characteristicChanged(const QLowEnergyCharacteristic &charact // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length(); Q_UNUSED(characteristic); QSettings settings; - // QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name) + // QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, + // QZSettings::default_heart_rate_belt_name) // .toString(); // NOTE: clazy-unused-non-trivial-variable emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); @@ -272,7 +272,8 @@ void flywheelbike::characteristicChanged(const QLowEnergyCharacteristic &charact uint16_t speed = ((parsedData->speed >> 8) & 0xFF); speed += ((parsedData->speed & 0xFF) << 8); - if (zero_fix_filter < settings.value(QZSettings::flywheel_filter, QZSettings::default_flywheel_filter).toUInt() && + if (zero_fix_filter < + settings.value(QZSettings::flywheel_filter, QZSettings::default_flywheel_filter).toUInt() && (parsedData->cadence == 0 || speed == 0 || power == 0)) { qDebug() << QStringLiteral("filtering crappy values"); zero_fix_filter++; @@ -290,7 +291,9 @@ void flywheelbike::characteristicChanged(const QLowEnergyCharacteristic &charact if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = ((double)speed) / 10.0; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } // https://www.facebook.com/groups/149984563348738/permalink/174268944253633/?comment_id=174366620910532&reply_comment_id=174666314213896 @@ -358,7 +361,7 @@ void flywheelbike::stateChanged(QLowEnergyService::ServiceState state) { &flywheelbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -366,11 +369,14 @@ void flywheelbike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -380,9 +386,10 @@ void flywheelbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&flywheelbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &flywheelbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -477,10 +484,6 @@ bool flywheelbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *flywheelbike::VirtualBike() { return virtualBike; } - -void *flywheelbike::VirtualDevice() { return VirtualBike(); } - uint16_t flywheelbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/flywheelbike.h b/src/flywheelbike.h index d1efbd811..2f1850b12 100644 --- a/src/flywheelbike.h +++ b/src/flywheelbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,10 +36,7 @@ class flywheelbike : public bike { Q_OBJECT public: flywheelbike(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: typedef enum DecoderRXState { WFSYNC_1 = 0, WFLENGTH, WFID, DATA, CHECKSUM, EOF_1 } DecoderRXState; @@ -139,11 +135,10 @@ class flywheelbike : public bike { bool wait_for_response = false); void startDiscover(); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; void updateStats(); QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/ftmsbike.cpp b/src/ftmsbike.cpp index 7f283b9d5..923066d21 100644 --- a/src/ftmsbike.cpp +++ b/src/ftmsbike.cpp @@ -1,5 +1,4 @@ #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -11,7 +10,9 @@ #ifdef Q_OS_ANDROID #include #endif +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include #ifdef Q_OS_IOS @@ -38,6 +39,12 @@ void ftmsbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin bool wait_for_response) { QEventLoop loop; QTimer timeout; + + if(!gattFTMSService) { + qDebug() << QStringLiteral("gattFTMSService is null!"); + return; + } + if (wait_for_response) { connect(gattFTMSService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); @@ -46,11 +53,20 @@ void ftmsbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (gattWriteCharControlPointId.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); + } if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -76,12 +92,15 @@ void ftmsbike::forcePower(int16_t requestPower) { write[2] = ((uint16_t)requestPower) >> 8; writeCharacteristic(write, sizeof(write), QStringLiteral("forcePower ") + QString::number(requestPower)); + + powerForced = true; } void ftmsbike::forceResistance(resistance_t requestResistance) { QSettings settings; - if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool()) { + if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool() && + resistance_lvl_mode == false) { uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset); @@ -122,6 +141,14 @@ void ftmsbike::update() { // updateDisplay(elapsed); } + if (powerForced && !autoResistance()) { + qDebug() << QStringLiteral("disabling resistance ") << QString::number(currentResistance().value()); + powerForced = false; + requestPower = -1; + init(); + forceResistance(currentResistance().value()); + } + if (requestResistance != -1) { if (requestResistance > 100) { requestResistance = 100; @@ -134,6 +161,7 @@ void ftmsbike::update() { emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); // if the FTMS is connected, the ftmsCharacteristicChanged event will do all the stuff because it's a // FTMS bike. This condition handles the peloton requests + auto virtualBike = this->VirtualBike(); if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && (requestPower == 0 || requestPower == -1)) { init(); @@ -255,16 +283,20 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris } if (Flags.totDistance) { + + /* + * the distance sent from the most trainers is a total distance, so it's useless for QZ + * Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | (uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index)))) / - 1000.0; + 1000.0;*/ index += 3; - } else { - Distance += ((Speed.value() / 3600000.0) * - ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); } + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); if (Flags.resistanceLvl) { @@ -273,7 +305,8 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris emit resistanceRead(Resistance.value()); index += 2; emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); - } else { + resistance_received = true; + } double ac = 0.01243107769; double bc = 1.145964912; double cc = -23.50977444; @@ -291,10 +324,13 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris (2.0 * ar)) * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); - Resistance = m_pelotonResistance; - emit resistanceRead(Resistance.value()); + if (!resistance_received) { + Resistance = m_pelotonResistance; + emit resistanceRead(Resistance.value()); + emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); + } } - } + if (Flags.instantPower) { if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) @@ -344,7 +380,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris #endif { if (Flags.heartRate && !disable_hr_frommachinery && newValue.length() > index) { - Heart = ((double)((newValue.at(index)))); + Heart = ((double)(((uint8_t)newValue.at(index)))); // index += 1; // NOTE: clang-analyzer-deadcode.DeadStores emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } else { @@ -534,7 +570,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris #endif { if (Flags.heartRate && !disable_hr_frommachinery && newValue.length() > index) { - Heart = ((double)((newValue.at(index)))); + Heart = ((double)(((uint8_t)newValue.at(index)))); // index += 1; // NOTE: clang-analyzer-deadcode.DeadStores emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } else { @@ -567,16 +603,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && (!heart || Heart.value() == 0 || disable_hr_frommachinery)) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS @@ -701,7 +728,7 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -726,11 +753,12 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&ftmsbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &ftmsbike::changeInclination); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &ftmsbike::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -750,7 +778,7 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' '); // handling gears - if (b.at(0) == 0x11) { + if (b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS) { qDebug() << "applying gears mod" << m_gears; int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8)); if (m_gears != 0) { @@ -760,7 +788,12 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact } } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, b); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray(b); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); } } @@ -833,6 +866,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (bluetoothDevice.name().toUpper().startsWith("SUITO")) { qDebug() << QStringLiteral("SUITO found"); max_resistance = 16; + } else if ((bluetoothDevice.name().toUpper().startsWith("MAGNUS "))) { + qDebug() << QStringLiteral("MAGNUS found"); + resistance_lvl_mode = true; } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); @@ -875,10 +911,6 @@ bool ftmsbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *ftmsbike::VirtualBike() { return virtualBike; } - -void *ftmsbike::VirtualDevice() { return VirtualBike(); } - uint16_t ftmsbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/ftmsbike.h b/src/ftmsbike.h index 548847d7c..42c2083cb 100644 --- a/src/ftmsbike.h +++ b/src/ftmsbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -70,28 +69,24 @@ class ftmsbike : public bike { Q_OBJECT public: ftmsbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t maxResistance() { return max_resistance; } - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t maxResistance() override { return max_resistance; } private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; void init(); void forceResistance(resistance_t requestResistance); void forcePower(int16_t requestPower); QTimer *refresh; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; - QLowEnergyService *gattFTMSService; + QLowEnergyService *gattFTMSService = nullptr; uint8_t sec1Update = 0; QByteArray lastPacket; @@ -107,6 +102,11 @@ class ftmsbike : public bike { bool noWriteResistance = false; bool noHeartService = false; + bool powerForced = false; + + bool resistance_lvl_mode = false; + bool resistance_received = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/ftmsrower.cpp b/src/ftmsrower.cpp index 6b9c1ef14..c1a6d45f8 100644 --- a/src/ftmsrower.cpp +++ b/src/ftmsrower.cpp @@ -1,6 +1,5 @@ #include "ftmsrower.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -12,9 +11,9 @@ #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -47,11 +46,15 @@ void ftmsrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -182,10 +185,23 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri if (!Flags.moreData) { - Cadence = ((uint8_t)newValue.at(index)) / cadence_divider; + if ((WATER_ROWER || DFIT_L_R) && lastStroke.secsTo(QDateTime::currentDateTime()) > 3) { + qDebug() << "Resetting cadence!"; + Cadence = 0; + m_watt = 0; + Speed = 0; + } else { + Cadence = ((uint8_t)newValue.at(index)) / cadence_divider; + } + StrokesCount = (((uint16_t)((uint8_t)newValue.at(index + 2)) << 8) | (uint16_t)((uint8_t)newValue.at(index + 1))); + if (lastStrokesCount != StrokesCount.value()) { + lastStroke = QDateTime::currentDateTime(); + } + lastStrokesCount = StrokesCount.value(); + index += 3; /* @@ -227,8 +243,10 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri index += 2; emit debug(QStringLiteral("Current Pace: ") + QString::number(instantPace)); - Speed = (60.0 / instantPace) * + if((DFIT_L_R && Cadence.value() > 0) || !DFIT_L_R) { + Speed = (60.0 / instantPace) * 30.0; // translating pace (min/500m) to km/h in order to match the pace function in the rower.cpp + } emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); } @@ -246,7 +264,8 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)))); index += 2; if (!filterWattNull || watt != 0) { - m_watt = watt; + if((DFIT_L_R && Cadence.value() > 0) || !DFIT_L_R) + m_watt = watt; } emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); } @@ -298,7 +317,7 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri { if (Flags.heartRate && !disable_hr_frommachinery) { if (index < newValue.length()) { - Heart = ((double)((newValue.at(index)))); + Heart = ((double)(((uint8_t)newValue.at(index)))); // index += 1; //NOTE: clang-analyzer-deadcode.DeadStores emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } else @@ -330,17 +349,7 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS @@ -450,7 +459,7 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -461,6 +470,8 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = @@ -476,15 +487,25 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) { #endif #endif + { if (virtual_device_enabled) { - emit debug(QStringLiteral("creating virtual bike interface...")); - - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); - // connect(virtualBike,&virtualbike::debug ,this,&ftmsrower::debug); + if (!virtual_device_rower) { + emit debug(QStringLiteral("creating virtual bike interface...")); + + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + // connect(virtualBike,&virtualbike::debug ,this,&ftmsrower::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + qDebug() << QStringLiteral("creating virtual rower interface..."); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } } + firstStateChanged = 1; + // ******************************************************************************************************** } - firstStateChanged = 1; - // ******************************************************************************************************** } void ftmsrower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { @@ -555,6 +576,15 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) { } else if (device.name().toUpper().startsWith(QStringLiteral("KS-WLT"))) { // KS-WLT-W1 KINGSMITH = true; qDebug() << "KINGSMITH found! cadence multiplier 1x"; + } else if (device.name().toUpper().startsWith(QStringLiteral("S4 COMMS"))) { + WATER_ROWER = true; + qDebug() << "WATER_ROWER found!"; + } else if (device.name().toUpper().startsWith(QStringLiteral("DFIT-L-R"))) { + DFIT_L_R = true; + qDebug() << "DFIT_L_R found!"; + } else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) { + PM5 = true; + qDebug() << "PM5 found!"; } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); @@ -598,10 +628,6 @@ bool ftmsrower::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *ftmsrower::VirtualBike() { return virtualBike; } - -void *ftmsrower::VirtualDevice() { return VirtualBike(); } - uint16_t ftmsrower::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/ftmsrower.h b/src/ftmsrower.h index 2d9b84236..c71513106 100644 --- a/src/ftmsrower.h +++ b/src/ftmsrower.h @@ -28,6 +28,7 @@ #include "rower.h" #include "virtualbike.h" +#include "virtualrower.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,20 +38,16 @@ class ftmsrower : public rower { Q_OBJECT public: ftmsrower(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(resistance_t requestResistance); QTimer *refresh; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; @@ -70,6 +67,12 @@ class ftmsrower : public rower { bool filterWattNull = false; bool WHIPR = false; bool KINGSMITH = false; + bool PM5 = false; + + bool WATER_ROWER = false; + bool DFIT_L_R = false; + QDateTime lastStroke = QDateTime::currentDateTime(); + double lastStrokesCount = 0; #ifdef Q_OS_IOS lockscreen *h = 0; diff --git a/src/handleurl.cpp b/src/handleurl.cpp new file mode 100644 index 000000000..0b3efed2c --- /dev/null +++ b/src/handleurl.cpp @@ -0,0 +1,18 @@ +#include "handleurl.h" +#include +#include + +void HandleURL::handleURL(const QUrl &url) +{ + qDebug() << url; +#ifndef IO_UNDER_QT + h->urlParser(url.toString().toLatin1()); +#endif +} + +HandleURL::HandleURL() { +#ifndef IO_UNDER_QT + h = new lockscreen(); +#endif +} + diff --git a/src/handleurl.h b/src/handleurl.h new file mode 100644 index 000000000..d09f2e30e --- /dev/null +++ b/src/handleurl.h @@ -0,0 +1,25 @@ +#ifndef HANDLEURL +#define HANDLEURL + +#include +#include +#include "ios/lockscreen.h" + +class HandleURL : public QObject +{ + Q_OBJECT + +public: + HandleURL(); + +private: + lockscreen* h; + +signals: + void incomingURL(QString path); + +public slots: + void handleURL(const QUrl &url); +}; +#endif + diff --git a/src/heartratebelt.h b/src/heartratebelt.h index d477fc93d..cd028f490 100644 --- a/src/heartratebelt.h +++ b/src/heartratebelt.h @@ -26,13 +26,12 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class heartratebelt : public treadmill { Q_OBJECT public: heartratebelt(); - bool connected(); + bool connected() override; private: QLowEnergyService *gattCommunicationChannelService = nullptr; @@ -42,7 +41,7 @@ class heartratebelt : public treadmill { void disconnected(); void debug(QString string); void packetReceived(); - void heartRate(uint8_t heart); + void heartRate(uint8_t heart) override; public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); diff --git a/src/homeform.cpp b/src/homeform.cpp index 88bbc2d07..d1e3447e7 100644 --- a/src/homeform.cpp +++ b/src/homeform.cpp @@ -1,6 +1,11 @@ #include "homeform.h" +#ifdef Q_OS_IOS #include "ios/lockscreen.h" +#endif +#include "localipaddress.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "material.h" #include "qfit.h" #include "simplecrypt.h" @@ -11,14 +16,18 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include @@ -38,17 +47,6 @@ using namespace std::chrono_literals; #include #endif -#if __has_include("secret.h") -#include "secret.h" -#else -#define STRAVA_SECRET_KEY test -#if defined(WIN32) -#pragma message("DEFINE STRAVA_SECRET_KEY!!!") -#else -#warning "DEFINE STRAVA_SECRET_KEY!!!" -#endif -#endif - #ifndef STRAVA_CLIENT_ID #define STRAVA_CLIENT_ID 7976 #if defined(WIN32) @@ -126,10 +124,12 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { QString unit = QStringLiteral("km"); QString meters = QStringLiteral("m"); QString weightLossUnit = QStringLiteral("Kg"); + QString cm = QStringLiteral("cm"); if (miles) { unit = QStringLiteral("mi"); weightLossUnit = QStringLiteral("Oz"); meters = QStringLiteral("ft"); + cm = QStringLiteral("in"); } #ifdef Q_OS_IOS @@ -149,6 +149,19 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { stravaAuthWebVisible = false; stravaWebVisibleChanged(stravaAuthWebVisible); + QString innerId = QStringLiteral("inner"); + QString sKey = QStringLiteral("template_") + innerId + QStringLiteral("_" TEMPLATE_PRIVATE_WEBSERVER_ID "_"); + + QString path = homeform::getWritableAppDir() + QStringLiteral("QZTemplates"); + this->userTemplateManager = TemplateInfoSenderBuilder::getInstance( + QStringLiteral("user"), QStringList({path, QStringLiteral(":/templates/")}), this); + + settings.setValue(sKey + QStringLiteral("enabled"), true); + settings.setValue(sKey + QStringLiteral("type"), TEMPLATE_TYPE_WEBSERVER); + settings.setValue(sKey + QStringLiteral("port"), 0); + this->innerTemplateManager = + TemplateInfoSenderBuilder::getInstance(innerId, QStringList({QStringLiteral(":/inner_templates/")}), this); + speed = new DataObject(QStringLiteral("Speed (") + unit + QStringLiteral("/h)"), QStringLiteral("icons/icons/speed.png"), QStringLiteral("0.0"), true, QStringLiteral("speed"), 48, labelFontSize); @@ -168,6 +181,10 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { new DataObject(QStringLiteral("Pace (m/") + unit + QStringLiteral(")"), QStringLiteral("icons/icons/pace.png"), QStringLiteral("0:00"), false, QStringLiteral("pace"), 48, labelFontSize); + target_pace = + new DataObject(QStringLiteral("T.Pace(m/") + unit + QStringLiteral(")"), QStringLiteral("icons/icons/pace.png"), + QStringLiteral("0:00"), false, QStringLiteral("pace"), 48, labelFontSize); + pace_last500m = new DataObject(QStringLiteral("Pace 500m (m/") + unit + QStringLiteral(")"), QStringLiteral("icons/icons/pace.png"), QStringLiteral("0:00"), false, QStringLiteral("pace"), 48, labelFontSize); @@ -258,7 +275,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { extIncline = new DataObject(QStringLiteral("Ext.Inclin.(%)"), QStringLiteral("icons/icons/inclination.png"), QStringLiteral("0.0"), true, QStringLiteral("external_inclination"), 48, labelFontSize); instantaneousStrideLengthCM = - new DataObject(QStringLiteral("Stride L.(cm)"), QStringLiteral("icons/icons/inclination.png"), + new DataObject(QStringLiteral("Stride L.(") + cm + ")", QStringLiteral("icons/icons/inclination.png"), QStringLiteral("0"), false, QStringLiteral("stride_length"), 48, labelFontSize); groundContactMS = new DataObject(QStringLiteral("Ground C.(ms)"), QStringLiteral("icons/icons/inclination.png"), QStringLiteral("0"), false, QStringLiteral("ground_contact"), 48, labelFontSize); @@ -392,57 +409,51 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { this->bluetoothManager = bl; this->engine = engine; + connect(bluetoothManager, &bluetooth::bluetoothDeviceConnected, this, &homeform::bluetoothDeviceConnected); + connect(bluetoothManager, &bluetooth::bluetoothDeviceDisconnected, this, &homeform::bluetoothDeviceDisconnected); connect(bluetoothManager, &bluetooth::deviceFound, this, &homeform::deviceFound); connect(bluetoothManager, &bluetooth::deviceConnected, this, &homeform::deviceConnected); connect(bluetoothManager, &bluetooth::ftmsAccessoryConnected, this, &homeform::ftmsAccessoryConnected); connect(bluetoothManager, &bluetooth::deviceConnected, this, &homeform::trainProgramSignals); - connect(this, &homeform::workoutNameChanged, bluetoothManager->getUserTemplateManager(), + connect(this, &homeform::workoutNameChanged, this->userTemplateManager, &TemplateInfoSenderBuilder::onWorkoutNameChanged); - connect(this, &homeform::workoutStartDateChanged, bluetoothManager->getUserTemplateManager(), + connect(this, &homeform::workoutStartDateChanged, this->userTemplateManager, &TemplateInfoSenderBuilder::onWorkoutStartDate); - connect(this, &homeform::instructorNameChanged, bluetoothManager->getUserTemplateManager(), + connect(this, &homeform::instructorNameChanged, this->userTemplateManager, &TemplateInfoSenderBuilder::onInstructorName); - connect(this, &homeform::workoutEventStateChanged, bluetoothManager->getUserTemplateManager(), + connect(this, &homeform::workoutEventStateChanged, this->userTemplateManager, &TemplateInfoSenderBuilder::workoutEventStateChanged); - connect(bluetoothManager->getUserTemplateManager(), &TemplateInfoSenderBuilder::activityDescriptionChanged, this, + connect(this->userTemplateManager, &TemplateInfoSenderBuilder::activityDescriptionChanged, this, &homeform::setActivityDescription); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::chartSaved, this, - &homeform::chartSaved); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::lap, this, &homeform::Lap); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::floatingClose, this, - &homeform::floatingOpen); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::autoResistance, this, + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::chartSaved, this, &homeform::chartSaved); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::lap, this, &homeform::Lap); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::floatingClose, this, &homeform::floatingOpen); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::autoResistance, this, &homeform::toggleAutoResistance); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::pelotonOffset_Plus, this, + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonOffset_Plus, this, &homeform::pelotonOffset_Plus); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::pelotonOffset_Minus, this, + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonOffset_Minus, this, &homeform::pelotonOffset_Minus); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::gears_Plus, this, - &homeform::gearUp); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::gears_Minus, this, - &homeform::gearDown); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::pelotonOffset, this, - &homeform::pelotonOffset); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::pelotonAskStart, this, - &homeform::pelotonAskStart); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::peloton_start_workout, this, + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Plus, this, &homeform::gearUp); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::gears_Minus, this, &homeform::gearDown); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonOffset, this, &homeform::pelotonOffset); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::pelotonAskStart, this, &homeform::pelotonAskStart); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::peloton_start_workout, this, &homeform::peloton_start_workout); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::peloton_abort_workout, this, + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::peloton_abort_workout, this, &homeform::peloton_abort_workout); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::Start, this, - &homeform::StartRequested); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::Pause, this, &homeform::Start); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::Stop, this, - &homeform::StopRequested); - connect(this, &homeform::workoutNameChanged, bluetoothManager->getInnerTemplateManager(), + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::Start, this, &homeform::StartRequested); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::Pause, this, &homeform::Start); + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::Stop, this, &homeform::StopRequested); + connect(this, &homeform::workoutNameChanged, this->innerTemplateManager, &TemplateInfoSenderBuilder::onWorkoutNameChanged); - connect(this, &homeform::workoutStartDateChanged, bluetoothManager->getInnerTemplateManager(), + connect(this, &homeform::workoutStartDateChanged, this->innerTemplateManager, &TemplateInfoSenderBuilder::onWorkoutStartDate); - connect(this, &homeform::instructorNameChanged, bluetoothManager->getInnerTemplateManager(), + connect(this, &homeform::instructorNameChanged, this->innerTemplateManager, &TemplateInfoSenderBuilder::onInstructorName); - connect(this, &homeform::workoutEventStateChanged, bluetoothManager->getInnerTemplateManager(), + connect(this, &homeform::workoutEventStateChanged, this->innerTemplateManager, &TemplateInfoSenderBuilder::workoutEventStateChanged); - connect(bluetoothManager->getInnerTemplateManager(), &TemplateInfoSenderBuilder::activityDescriptionChanged, this, + connect(this->innerTemplateManager, &TemplateInfoSenderBuilder::activityDescriptionChanged, this, &homeform::setActivityDescription); engine->rootContext()->setContextProperty(QStringLiteral("rootItem"), (QObject *)this); @@ -486,6 +497,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { QObject::connect(stack, SIGNAL(keyMediaPrevious()), this, SLOT(keyMediaPrevious())); QObject::connect(stack, SIGNAL(keyMediaNext()), this, SLOT(keyMediaNext())); QObject::connect(stack, SIGNAL(floatingOpen()), this, SLOT(floatingOpen())); + QObject::connect(stack, SIGNAL(openFloatingWindowBrowser()), this, SLOT(openFloatingWindowBrowser())); #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) QObject::connect(engine, &QQmlApplicationEngine::quit, &QGuiApplication::quit); @@ -538,6 +550,89 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { QBluetoothDeviceInfo b; deviceConnected(b); } + +#ifndef Q_OS_IOS + iphone_browser = new QMdnsEngine::Browser(&iphone_server, "_qz_iphone._tcp.local.", &iphone_cache); + + QObject::connect(iphone_browser, &QMdnsEngine::Browser::serviceAdded, [](const QMdnsEngine::Service &service) { + homeform::singleton()->iphone_service = service; + qDebug() << service.name() << service.hostname() << service.port() << "discovered!"; + + if (homeform::singleton()->iphone_resolver) + delete homeform::singleton()->iphone_resolver; + homeform::singleton()->iphone_resolver = new QMdnsEngine::Resolver( + &homeform::singleton()->iphone_server, service.hostname(), &homeform::singleton()->iphone_cache); + QObject::connect(homeform::singleton()->iphone_resolver, &QMdnsEngine::Resolver::resolved, + [](const QHostAddress &address) { + qDebug() << "resolved to" << address; + if (address.protocol() == QAbstractSocket::IPv4Protocol && + (homeform::singleton()->iphone_socket == nullptr || + !homeform::singleton()->iphone_address.isEqual(address))) { + if (homeform::singleton()->iphone_socket) + delete homeform::singleton()->iphone_socket; + homeform::singleton()->iphone_socket = new QTcpSocket(); + QObject::connect(homeform::singleton()->iphone_socket, &QTcpSocket::connected, + []() { qDebug() << "iphone_socket connected!"; }); + QObject::connect(homeform::singleton()->iphone_socket, &QTcpSocket::readyRead, []() { + QString rec = homeform::singleton()->iphone_socket->readAll(); + qDebug() << "iphone_socket received << " << rec; + QStringList fields = rec.split("#"); + foreach (QString f, fields) { + if (f.contains("HR")) { + QStringList values = f.split("="); + if (values.length() > 1) { + emit homeform::singleton()->heartRate(values[1].toDouble()); + } + } + } + }); + + homeform::singleton()->iphone_address = address; + homeform::singleton()->iphone_socket->connectToHost( + address, homeform::singleton()->iphone_service.port()); + } + }); + }); + + QObject::connect(iphone_browser, &QMdnsEngine::Browser::serviceUpdated, [](const QMdnsEngine::Service &service) { + homeform::singleton()->iphone_service = service; + qDebug() << service.name() << service.hostname() << service.port() << "updated!"; + + if (homeform::singleton()->iphone_resolver) + delete homeform::singleton()->iphone_resolver; + homeform::singleton()->iphone_resolver = new QMdnsEngine::Resolver( + &homeform::singleton()->iphone_server, service.hostname(), &homeform::singleton()->iphone_cache); + QObject::connect(homeform::singleton()->iphone_resolver, &QMdnsEngine::Resolver::resolved, + [](const QHostAddress &address) { + if (address.protocol() == QAbstractSocket::IPv4Protocol && + (homeform::singleton()->iphone_socket == nullptr || + !homeform::singleton()->iphone_address.isEqual(address))) { + if (homeform::singleton()->iphone_socket) + delete homeform::singleton()->iphone_socket; + qDebug() << "resolved to" << address; + homeform::singleton()->iphone_socket = new QTcpSocket(); + QObject::connect(homeform::singleton()->iphone_socket, &QTcpSocket::connected, + []() { qDebug() << "iphone_socket connected!"; }); + QObject::connect(homeform::singleton()->iphone_socket, &QTcpSocket::readyRead, []() { + QString rec = homeform::singleton()->iphone_socket->readAll(); + qDebug() << "iphone_socket received << " << rec; + QStringList fields = rec.split("#"); + foreach (QString f, fields) { + if (f.contains("HR")) { + QStringList values = f.split("="); + if (values.length() > 1) { + emit homeform::singleton()->heartRate(values[1].toDouble()); + } + } + } + }); + homeform::singleton()->iphone_address = address; + homeform::singleton()->iphone_socket->connectToHost( + address, homeform::singleton()->iphone_service.port()); + } + }); + }); +#endif } void homeform::setActivityDescription(QString desc) { activityDescription = desc; } @@ -546,7 +641,7 @@ void homeform::chartSaved(QString fileName) { if (!stopped) return; chartImagesFilenames.append(fileName); - if (chartImagesFilenames.length() >= 7) { + if (chartImagesFilenames.length() >= 8) { sendMail(); chartImagesFilenames.clear(); } @@ -608,8 +703,31 @@ void homeform::floatingOpen() { #endif } +void homeform::openFloatingWindowBrowser() { + QSettings settings; + QHostAddress a; + foreach (QNetworkInterface netInterface, QNetworkInterface::allInterfaces()) { + // Return only the first non-loopback MAC Address + QString addr = netInterface.hardwareAddress(); + if (!(netInterface.flags() & QNetworkInterface::IsLoopBack) && !addr.isEmpty()) { + const auto entries = netInterface.addressEntries(); + for (const QNetworkAddressEntry &newEntry : entries) { + qDebug() << newEntry.ip().toIPv4Address(); + if (!newEntry.ip().isLoopback()) { + a = newEntry.ip(); + break; + } + } + } + } + QString url = "http://" + localipaddress::getIP(a).toString() + ":" + + QString::number(settings.value("template_inner_QZWS_port", 6666).toInt()) + "/floating/floating.htm"; + QDesktopServices::openUrl(url); +} + void homeform::peloton_abort_workout() { m_pelotonAskStart = false; + emit changePelotonAskStart(pelotonAskStart()); qDebug() << QStringLiteral("peloton_abort_workout!"); pelotonAbortedName = pelotonAskedName; pelotonAbortedInstructor = pelotonAskedInstructor; @@ -617,6 +735,7 @@ void homeform::peloton_abort_workout() { void homeform::peloton_start_workout() { m_pelotonAskStart = false; + emit changePelotonAskStart(pelotonAskStart()); qDebug() << QStringLiteral("peloton_start_workout!"); if (pelotonHandler && !pelotonHandler->trainrows.isEmpty()) { if (trainProgram) { @@ -627,6 +746,14 @@ void homeform::peloton_start_workout() { trainProgram = nullptr; } trainProgram = new trainprogram(pelotonHandler->trainrows, bluetoothManager); + if (!stravaPelotonActivityName.isEmpty() && !stravaPelotonInstructorName.isEmpty()) { + QString path = getWritableAppDir() + "training/" + workoutNameBasedOnBluetoothDevice() + "/" + + stravaPelotonInstructorName + "/"; + QDir().mkpath(path); + lastTrainProgramFileSaved = + path + stravaPelotonActivityName.replace("/", "-") + " - " + stravaPelotonInstructorName + ".xml"; + trainProgram->save(lastTrainProgramFileSaved); + } trainProgramSignals(); trainProgram->restart(); } @@ -642,6 +769,10 @@ void homeform::pelotonLoginState(bool ok) { m_pelotonLoginState = (ok ? 1 : 0); emit pelotonLoginChanged(m_pelotonLoginState); + if (!ok) { + setToastRequested("Peloton Login Error!"); + emit toastRequestedChanged(toastRequested()); + } } void homeform::pelotonWorkoutStarted(const QString &name, const QString &instructor) { @@ -733,7 +864,7 @@ void homeform::backup() { QFile::remove(filename); qfit::save(filename, Session, dev->deviceType(), qobject_cast(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE, - stravaPelotonWorkoutType); + stravaPelotonWorkoutType, dev->bluetoothDevice.name()); index++; if (index > 1) { @@ -772,12 +903,21 @@ homeform::~homeform() { } void homeform::aboutToQuit() { + qDebug() << "homeform::aboutToQuit()"; + #ifdef Q_OS_ANDROID // closing floating window if (floating_open) floatingOpen(); QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/NotificationClient", "hide", "()V"); #endif + + QSettings settings; + if (settings.value(QZSettings::fit_file_saved_on_quit, QZSettings::default_fit_file_saved_on_quit).toBool()) { + qDebug() << "fit_file_saved_on_quit true"; + fit_save_clicked(); + } + if (bluetoothManager->device()) bluetoothManager->device()->disconnectBluetooth(); } @@ -808,6 +948,8 @@ void homeform::trainProgramSignals() { disconnect(trainProgram, &trainprogram::changePower, ((bike *)bluetoothManager->device()), &bike::changePower); disconnect(trainProgram, &trainprogram::changePower, ((rower *)bluetoothManager->device()), &rower::changePower); + disconnect(trainProgram, &trainprogram::changeSpeed, ((rower *)bluetoothManager->device()), + &rower::changeSpeed); disconnect(trainProgram, &trainprogram::changeCadence, ((elliptical *)bluetoothManager->device()), &elliptical::changeCadence); disconnect(trainProgram, &trainprogram::changePower, ((elliptical *)bluetoothManager->device()), @@ -827,10 +969,12 @@ void homeform::trainProgramSignals() { disconnect(this, &homeform::workoutEventStateChanged, bluetoothManager->device(), &bluetoothdevice::workoutEventStateChanged); disconnect(trainProgram, &trainprogram::changeTimestamp, this, &homeform::changeTimestamp); + disconnect(trainProgram, &trainprogram::toastRequest, this, &homeform::onToastRequested); connect(trainProgram, &trainprogram::start, bluetoothManager->device(), &bluetoothdevice::start); connect(trainProgram, &trainprogram::stop, bluetoothManager->device(), &bluetoothdevice::stop); connect(trainProgram, &trainprogram::lap, this, &homeform::Lap); + connect(trainProgram, &trainprogram::toastRequest, this, &homeform::onToastRequested); if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) { connect(trainProgram, &trainprogram::changeSpeed, ((treadmill *)bluetoothManager->device()), &treadmill::changeSpeed); @@ -872,6 +1016,8 @@ void homeform::trainProgramSignals() { &rower::changeResistance); connect(trainProgram, &trainprogram::changeCadence, ((rower *)bluetoothManager->device()), &rower::changeCadence); + connect(trainProgram, &trainprogram::changeSpeed, ((rower *)bluetoothManager->device()), + &rower::changeSpeed); } connect(trainProgram, &trainprogram::changeNextInclination300Meters, bluetoothManager->device(), &bluetoothdevice::changeNextInclination300Meters); @@ -881,12 +1027,30 @@ void homeform::trainProgramSignals() { connect(this, &homeform::workoutEventStateChanged, bluetoothManager->device(), &bluetoothdevice::workoutEventStateChanged); + if (trainProgram) { + setChartIconVisible(trainProgram->powerzoneWorkout()); + if (chartFooterVisible()) { + if (trainProgram->powerzoneWorkout()) { + // reloading + setChartFooterVisible(false); + setChartFooterVisible(true); + } else { + setChartFooterVisible(false); + } + } + } + qDebug() << QStringLiteral("trainProgram associated to a device"); } else { qDebug() << QStringLiteral("trainProgram NOT associated to a device"); } } +void homeform::onToastRequested(QString message) { + setToastRequested(message); + emit toastRequestedChanged(message); +} + QStringList homeform::tile_order() { QStringList r; @@ -1208,6 +1372,12 @@ void homeform::sortTiles() { preset_inclination_5->setGridId(i); dataList.append(preset_inclination_5); } + + if (settings.value(QZSettings::tile_target_pace_enabled, false).toBool() && + settings.value(QZSettings::tile_target_pace_order, 50).toInt() == i) { + target_pace->setGridId(i); + dataList.append(target_pace); + } } } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { for (int i = 0; i < 100; i++) { @@ -1760,6 +1930,70 @@ void homeform::sortTiles() { pace_last500m->setGridId(i); dataList.append(pace_last500m); } + + if (settings.value(QZSettings::tile_target_speed_enabled, false).toBool() && + settings.value(QZSettings::tile_target_speed_order, 28).toInt() == i) { + target_speed->setGridId(i); + dataList.append(target_speed); + } + + if (settings.value(QZSettings::tile_target_pace_enabled, false).toBool() && + settings.value(QZSettings::tile_target_pace_order, 50).toInt() == i) { + target_pace->setGridId(i); + target_pace->setName("T.Pace(m/500m)"); + dataList.append(target_pace); + } + + if (settings + .value(QZSettings::tile_preset_resistance_1_enabled, + QZSettings::default_tile_preset_resistance_1_enabled) + .toBool() && + settings.value(QZSettings::tile_preset_resistance_1_order, + QZSettings::default_tile_preset_resistance_1_order) + .toInt() == i) { + preset_resistance_1->setGridId(i); + dataList.append(preset_resistance_1); + } + if (settings + .value(QZSettings::tile_preset_resistance_2_enabled, + QZSettings::default_tile_preset_resistance_2_enabled) + .toBool() && + settings.value(QZSettings::tile_preset_resistance_2_order, + QZSettings::default_tile_preset_resistance_2_order) + .toInt() == i) { + preset_resistance_2->setGridId(i); + dataList.append(preset_resistance_2); + } + if (settings + .value(QZSettings::tile_preset_resistance_3_enabled, + QZSettings::default_tile_preset_resistance_3_enabled) + .toBool() && + settings.value(QZSettings::tile_preset_resistance_3_order, + QZSettings::default_tile_preset_resistance_3_order) + .toInt() == i) { + preset_resistance_3->setGridId(i); + dataList.append(preset_resistance_3); + } + if (settings + .value(QZSettings::tile_preset_resistance_4_enabled, + QZSettings::default_tile_preset_resistance_4_enabled) + .toBool() && + settings.value(QZSettings::tile_preset_resistance_4_order, + QZSettings::default_tile_preset_resistance_4_order) + .toInt() == i) { + preset_resistance_4->setGridId(i); + dataList.append(preset_resistance_4); + } + if (settings + .value(QZSettings::tile_preset_resistance_5_enabled, + QZSettings::default_tile_preset_resistance_5_enabled) + .toBool() && + settings.value(QZSettings::tile_preset_resistance_5_order, + QZSettings::default_tile_preset_resistance_5_order) + .toInt() == i) { + preset_resistance_5->setGridId(i); + dataList.append(preset_resistance_5); + } } } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) { for (int i = 0; i < 100; i++) { @@ -2055,6 +2289,18 @@ void homeform::sortTiles() { gears->setGridId(i); dataList.append(gears); } + + if (settings.value(QZSettings::tile_target_pace_enabled, false).toBool() && + settings.value(QZSettings::tile_target_pace_order, 50).toInt() == i) { + target_pace->setGridId(i); + dataList.append(target_pace); + } + + if (settings.value(QZSettings::tile_pace_enabled, true).toBool() && + settings.value(QZSettings::tile_pace_order, 51).toInt() == i) { + pace->setGridId(i); + dataList.append(pace); + } } } @@ -2160,17 +2406,25 @@ void homeform::deviceConnected(QBluetoothDeviceInfo b) { if (settings.value(QZSettings::floating_startup, QZSettings::default_floating_startup).toBool()) { floatingOpen(); } + + if (!settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name) + .toString() + .compare(QZSettings::default_heart_rate_belt_name) && + !settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/WearableController", "start", + "(Landroid/content/Context;)V", QtAndroid::androidContext().object()); + } #endif if (settings.value(QZSettings::gears_restore_value, QZSettings::default_gears_restore_value).toBool()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { ((bike *)bluetoothManager->device()) - ->setGears( - settings.value(QZSettings::gears_current_value, QZSettings::default_gears_current_value).toInt()); + ->setGears(settings.value(QZSettings::gears_current_value, QZSettings::default_gears_current_value) + .toDouble()); } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) { ((elliptical *)bluetoothManager->device()) - ->setGears( - settings.value(QZSettings::gears_current_value, QZSettings::default_gears_current_value).toInt()); + ->setGears(settings.value(QZSettings::gears_current_value, QZSettings::default_gears_current_value) + .toDouble()); } } } @@ -2439,7 +2693,7 @@ void homeform::Plus(const QString &name) { bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); qDebug() << QStringLiteral("Plus") << name; - if (name.contains(QStringLiteral("target_speed"))) { + if (name.contains(QStringLiteral("target_speed")) || name.contains(QStringLiteral("target_pace"))) { if (bluetoothManager->device()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) { @@ -2574,10 +2828,13 @@ void homeform::Plus(const QString &name) { } else if (name.contains("gears")) { if (bluetoothManager->device()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { - ((bike *)bluetoothManager->device())->setGears(((bike *)bluetoothManager->device())->gears() + 1); + ((bike *)bluetoothManager->device()) + ->setGears(((bike *)bluetoothManager->device())->gears() + + settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()); } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) { ((elliptical *)bluetoothManager->device()) - ->setGears(((elliptical *)bluetoothManager->device())->gears() + 1); + ->setGears(((elliptical *)bluetoothManager->device())->gears() + + settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()); } } } else if (name.contains(QStringLiteral("target_resistance"))) { @@ -2620,6 +2877,7 @@ void homeform::Plus(const QString &name) { } else if (name.contains(QStringLiteral("target_power"))) { if (bluetoothManager->device()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { + m_overridePower = true; ((bike *)bluetoothManager->device()) ->changePower(((bike *)bluetoothManager->device())->lastRequestedPower().value() + 10); if (trainProgram) { @@ -2662,11 +2920,25 @@ void homeform::pelotonOffset_Plus() { Plus(QStringLiteral("peloton_offset")); } void homeform::pelotonOffset_Minus() { Minus(QStringLiteral("peloton_offset")); } +void homeform::bluetoothDeviceConnected(bluetoothdevice *b) { + this->innerTemplateManager->start(b); + this->userTemplateManager->start(b); +#ifndef Q_OS_IOS + // heart rate received from apple watch while QZ is running on a different device via TCP socket (iphone_socket) + connect(this, SIGNAL(heartRate(uint8_t)), b, SLOT(heartRate(uint8_t))); +#endif +} + +void homeform::bluetoothDeviceDisconnected() { + this->innerTemplateManager->stop(); + this->userTemplateManager->stop(); +} + void homeform::Minus(const QString &name) { QSettings settings; bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); qDebug() << QStringLiteral("Minus") << name; - if (name.contains(QStringLiteral("target_speed"))) { + if (name.contains(QStringLiteral("target_speed")) || name.contains(QStringLiteral("target_pace"))) { if (bluetoothManager->device()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) { @@ -2798,10 +3070,13 @@ void homeform::Minus(const QString &name) { } else if (name.contains(QStringLiteral("gears"))) { if (bluetoothManager->device()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { - ((bike *)bluetoothManager->device())->setGears(((bike *)bluetoothManager->device())->gears() - 1); + ((bike *)bluetoothManager->device()) + ->setGears(((bike *)bluetoothManager->device())->gears() - + settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()); } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) { ((elliptical *)bluetoothManager->device()) - ->setGears(((elliptical *)bluetoothManager->device())->gears() - 1); + ->setGears(((elliptical *)bluetoothManager->device())->gears() - + settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()); } } } else if (name.contains(QStringLiteral("target_resistance"))) { @@ -2844,6 +3119,7 @@ void homeform::Minus(const QString &name) { } else if (name.contains(QStringLiteral("target_power"))) { if (bluetoothManager->device()) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { + m_overridePower = true; ((bike *)bluetoothManager->device()) ->changePower(((bike *)bluetoothManager->device())->lastRequestedPower().value() - 10); if (trainProgram) { @@ -2891,16 +3167,24 @@ void homeform::Start_inner(bool send_event_to_device) { QSettings settings; qDebug() << QStringLiteral("Start pressed - paused") << paused << QStringLiteral("stopped") << stopped; + m_overridePower = false; + if (settings.value(QZSettings::tts_enabled, QZSettings::default_tts_enabled).toBool()) m_speech.say("Start pressed"); if (!paused && !stopped) { - paused = true; if (bluetoothManager->device() && send_event_to_device) { bluetoothManager->device()->stop(paused); } emit workoutEventStateChanged(bluetoothdevice::PAUSED); + // Pause Video if running and visible + if ((trainProgram) && (videoVisible() == true)) { + QObject *rootObject = engine->rootObjects().constFirst(); + auto *videoPlaybackHalf = rootObject->findChild(QStringLiteral("videoplaybackhalf")); + auto videoPlaybackHalfPlayer = qvariant_cast(videoPlaybackHalf->property("mediaObject")); + videoPlaybackHalfPlayer->pause(); + } } else { if (bluetoothManager->device() && send_event_to_device) { @@ -2918,7 +3202,11 @@ void homeform::Start_inner(bool send_event_to_device) { #ifdef Q_OS_IOS // due to #857 - bluetoothManager->getInnerTemplateManager()->start(bluetoothManager->device()); + if (!settings + .value(QZSettings::peloton_companion_workout_ocr, + QZSettings::default_companion_peloton_workout_ocr) + .toBool()) + this->innerTemplateManager->start(bluetoothManager->device()); #endif if (!pelotonHandler || (pelotonHandler && !pelotonHandler->isWorkoutInProgress())) { @@ -2939,6 +3227,14 @@ void homeform::Start_inner(bool send_event_to_device) { trainProgram->restart(); } emit workoutEventStateChanged(bluetoothdevice::RESUMED); + // Resume Video if visible + if ((trainProgram) && (videoVisible() == true)) { + QObject *rootObject = engine->rootObjects().constFirst(); + auto *videoPlaybackHalf = rootObject->findChild(QStringLiteral("videoplaybackhalf")); + auto videoPlaybackHalfPlayer = + qvariant_cast(videoPlaybackHalf->property("mediaObject")); + videoPlaybackHalfPlayer->play(); + } } paused = false; @@ -2977,6 +3273,9 @@ void homeform::StopRequested() { void homeform::Stop() { QSettings settings; + + m_startRequested = false; + qDebug() << QStringLiteral("Stop pressed - paused") << paused << QStringLiteral("stopped") << stopped; if (stopped) { @@ -2986,7 +3285,9 @@ void homeform::Stop() { #ifdef Q_OS_IOS // due to #857 - bluetoothManager->getInnerTemplateManager()->reinit(); + if (!settings.value(QZSettings::peloton_companion_workout_ocr, QZSettings::default_companion_peloton_workout_ocr) + .toBool()) + this->innerTemplateManager->reinit(); #endif if (settings.value(QZSettings::tts_enabled, QZSettings::default_tts_enabled).toBool()) @@ -3142,6 +3443,7 @@ void homeform::update() { double ftpSetting = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble(); double unit_conversion = 1.0; double meter_feet_conversion = 1.0; + double cm_inches_conversion = 1.0; bool power5s = settings.value(QZSettings::power_avg_5s, QZSettings::default_power_avg_5s).toBool(); uint8_t treadmill_pid_heart_zone = settings.value(QZSettings::treadmill_pid_heart_zone, QZSettings::default_treadmill_pid_heart_zone) @@ -3158,6 +3460,7 @@ void homeform::update() { if (miles) { unit_conversion = 0.621371; meter_feet_conversion = 3.28084; + cm_inches_conversion = 0.393701; } emit signalChanged(signal()); @@ -3184,6 +3487,21 @@ void homeform::update() { moving_time->setValue(bluetoothManager->device()->movingTime().toString(QStringLiteral("h:mm:ss"))); if (trainProgram) { + // sync the video with the zwo workout file + if (videoVisible() == true && !bluetoothManager->device()->currentCordinate().isValid()) { + QObject *rootObject = engine->rootObjects().constFirst(); + auto *videoPlaybackHalf = rootObject->findChild(QStringLiteral("videoplaybackhalf")); + auto videoPlaybackHalfPlayer = + qvariant_cast(videoPlaybackHalf->property("mediaObject")); + double videoTimeStampSeconds = (double)videoPlaybackHalfPlayer->position() / 1000.0; + QTime videoCurrent = QTime(0, 0, videoTimeStampSeconds); + int delta = trainProgram->totalElapsedTime().secsTo(videoCurrent); + if (qAbs(delta) > 1) { + videoPlaybackHalfPlayer->setPosition(QTime(0, 0, 0).secsTo(trainProgram->totalElapsedTime()) * + 1000.0); + } + } + peloton_offset->setValue(QString::number(trainProgram->offsetElapsedTime()) + QStringLiteral(" sec.")); peloton_remaining->setValue(trainProgram->remainingTime().toString("h:mm:ss")); peloton_remaining->setSecondLine(QString::number(trainProgram->offsetElapsedTime()) + @@ -3209,6 +3527,16 @@ void homeform::update() { nextRows->setValue(QStringLiteral("HR") + QString::number(next.HRmin) + QStringLiteral("-") + QString::number(next.HRmax) + QStringLiteral(" ") + next.duration.toString(QStringLiteral("mm:ss"))); + else if (next.speed != -1 && next.inclination != -1) + nextRows->setValue(QStringLiteral("S") + QString::number(next.speed) + QStringLiteral("I") + + QString::number(next.inclination) + QStringLiteral(" ") + + next.duration.toString(QStringLiteral("mm:ss"))); + else if (next.speed != -1) + nextRows->setValue(QStringLiteral("S") + QString::number(next.speed) + QStringLiteral(" ") + + next.duration.toString(QStringLiteral("mm:ss"))); + else if (next.inclination != -200) + nextRows->setValue(QStringLiteral("I") + QString::number(next.inclination) + QStringLiteral(" ") + + next.duration.toString(QStringLiteral("mm:ss"))); else if (next.power != -1) { double ftpPerc = (next.power / ftpSetting) * 100.0; uint8_t ftpZone = 1; @@ -3279,7 +3607,22 @@ void homeform::update() { wattKg->setSecondLine( QStringLiteral("AVG: ") + QString::number(bluetoothManager->device()->wattKg().average(), 'f', 1) + QStringLiteral("MAX: ") + QString::number(bluetoothManager->device()->wattKg().max(), 'f', 1)); - datetime->setValue(QTime::currentTime().toString(QStringLiteral("hh:mm:ss"))); + QLocale locale = QLocale::system(); + + // Format the time based on the locale + QString timeFormat = locale.timeFormat(QLocale::ShortFormat); + bool usesAMPMFormat = timeFormat.toUpper().contains("A"); + QDateTime currentTime = QDateTime::currentDateTime(); + + QString formattedTime; + if (usesAMPMFormat) { + // The locale uses 12-hour format with AM/PM + formattedTime = currentTime.toString("h:mm:ss AP"); + } else { + // The locale uses 24-hour format + formattedTime = currentTime.toString("H:mm:ss"); + } + datetime->setValue(formattedTime); if (power5s) watts = bluetoothManager->device()->wattsMetric().average5s(); else @@ -3333,7 +3676,8 @@ void homeform::update() { pace = 0; } - strideLength = ((treadmill *)bluetoothManager->device())->currentStrideLength().value(); + strideLength = + ((treadmill *)bluetoothManager->device())->currentStrideLength().value() * cm_inches_conversion; groundContact = ((treadmill *)bluetoothManager->device())->currentGroundContact().value(); verticalOscillation = ((treadmill *)bluetoothManager->device())->currentVerticalOscillation().value(); inclination = ((treadmill *)bluetoothManager->device())->currentInclination().value(); @@ -3383,30 +3727,51 @@ void homeform::update() { QStringLiteral(" MAX: ") + QString::number(((treadmill *)bluetoothManager->device())->currentVerticalOscillation().max(), 'f', 0)); - if (bluetoothManager->device()->currentSpeed().value() < 9) { - speed->setValueFontColor(QStringLiteral("white")); - this->pace->setValueFontColor(QStringLiteral("white")); - } else if (bluetoothManager->device()->currentSpeed().value() < 10) { - speed->setValueFontColor(QStringLiteral("limegreen")); - this->pace->setValueFontColor(QStringLiteral("limegreen")); - } else if (bluetoothManager->device()->currentSpeed().value() < 11) { - speed->setValueFontColor(QStringLiteral("gold")); - this->pace->setValueFontColor(QStringLiteral("gold")); - } else if (bluetoothManager->device()->currentSpeed().value() < 12) { - speed->setValueFontColor(QStringLiteral("orange")); - this->pace->setValueFontColor(QStringLiteral("orange")); - } else if (bluetoothManager->device()->currentSpeed().value() < 13) { - speed->setValueFontColor(QStringLiteral("darkorange")); - this->pace->setValueFontColor(QStringLiteral("darkorange")); - } else if (bluetoothManager->device()->currentSpeed().value() < 14) { - speed->setValueFontColor(QStringLiteral("orangered")); - this->pace->setValueFontColor(QStringLiteral("orangered")); + // if there is no training program, the color is based on presets + if (!trainProgram || trainProgram->currentRow().speed == -1) { + if (bluetoothManager->device()->currentSpeed().value() < 9) { + speed->setValueFontColor(QStringLiteral("white")); + this->pace->setValueFontColor(QStringLiteral("white")); + } else if (bluetoothManager->device()->currentSpeed().value() < 10) { + speed->setValueFontColor(QStringLiteral("limegreen")); + this->pace->setValueFontColor(QStringLiteral("limegreen")); + } else if (bluetoothManager->device()->currentSpeed().value() < 11) { + speed->setValueFontColor(QStringLiteral("gold")); + this->pace->setValueFontColor(QStringLiteral("gold")); + } else if (bluetoothManager->device()->currentSpeed().value() < 12) { + speed->setValueFontColor(QStringLiteral("orange")); + this->pace->setValueFontColor(QStringLiteral("orange")); + } else if (bluetoothManager->device()->currentSpeed().value() < 13) { + speed->setValueFontColor(QStringLiteral("darkorange")); + this->pace->setValueFontColor(QStringLiteral("darkorange")); + } else if (bluetoothManager->device()->currentSpeed().value() < 14) { + speed->setValueFontColor(QStringLiteral("orangered")); + this->pace->setValueFontColor(QStringLiteral("orangered")); + } else { + speed->setValueFontColor(QStringLiteral("red")); + this->pace->setValueFontColor(QStringLiteral("red")); + } + bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } else { - speed->setValueFontColor(QStringLiteral("red")); - this->pace->setValueFontColor(QStringLiteral("red")); + if (bluetoothManager->device()->currentSpeed().value() <= trainProgram->currentRow().upper_speed && + bluetoothManager->device()->currentSpeed().value() >= trainProgram->currentRow().lower_speed) { + this->target_zone->setValueFontColor(QStringLiteral("limegreen")); + this->pace->setValueFontColor(QStringLiteral("limegreen")); + } else if (bluetoothManager->device()->currentSpeed().value() <= + (trainProgram->currentRow().upper_speed + 0.2) && + bluetoothManager->device()->currentSpeed().value() >= + (trainProgram->currentRow().lower_speed - 0.2)) { + this->target_zone->setValueFontColor(QStringLiteral("orange")); + this->pace->setValueFontColor(QStringLiteral("orange")); + } else { + this->target_zone->setValueFontColor(QStringLiteral("red")); + this->pace->setValueFontColor(QStringLiteral("red")); + } + bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } - bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); + this->target_pace->setValue( + ((treadmill *)bluetoothManager->device())->lastRequestedPace().toString(QStringLiteral("m:ss"))); this->target_speed->setValue(QString::number( ((treadmill *)bluetoothManager->device())->lastRequestedSpeed().value() * unit_conversion, 'f', 1)); this->target_speed->setSecondLine(QString::number(bluetoothManager->device()->difficult() * 100.0, 'f', 0) + @@ -3463,7 +3828,10 @@ void homeform::update() { this->target_power->setValue( QString::number(((bike *)bluetoothManager->device())->lastRequestedPower().value(), 'f', 0)); this->resistance->setValue(QString::number(resistance, 'f', 0)); - this->gears->setValue(QString::number(((bike *)bluetoothManager->device())->gears())); + if (settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble() == 1.0) + this->gears->setValue(QString::number(((bike *)bluetoothManager->device())->gears())); + else + this->gears->setValue(QString::number(((bike *)bluetoothManager->device())->gears(), 'f', 1)); this->resistance->setSecondLine( QStringLiteral("AVG: ") + @@ -3496,6 +3864,14 @@ void homeform::update() { this->steeringAngle->setValue( QString::number(((bike *)bluetoothManager->device())->currentSteeringAngle().value(), 'f', 1)); + if ((!trainProgram || (trainProgram && !trainProgram->isStarted())) && + !((bike *)bluetoothManager->device())->ergModeSupportedAvailableByHardware() && + ((bike *)bluetoothManager->device())->lastRequestedPower().value() > 0 && m_overridePower) { + qDebug() << QStringLiteral("using target power tile for ERG workout manually"); + ((bike *)bluetoothManager->device()) + ->changePower(((bike *)bluetoothManager->device())->lastRequestedPower().value()); + } + } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { if (bluetoothManager->device()->currentSpeed().value()) { pace = 10000 / (((rower *)bluetoothManager->device())->currentPace().second() + @@ -3517,6 +3893,57 @@ void homeform::update() { ((rower *)bluetoothManager->device())->averagePace().toString(QStringLiteral("m:ss")) + QStringLiteral(" MAX: ") + ((rower *)bluetoothManager->device())->maxPace().toString(QStringLiteral("m:ss"))); + this->target_pace->setValue( + ((rower *)bluetoothManager->device())->lastRequestedPace().toString(QStringLiteral("m:ss"))); + if (trainProgram) { + this->target_pace->setSecondLine(((rower *)bluetoothManager->device()) + ->speedToPace(trainProgram->currentRow().lower_speed) + .toString(QStringLiteral("m:ss")) + + " - " + + ((rower *)bluetoothManager->device()) + ->speedToPace(trainProgram->currentRow().upper_speed) + .toString(QStringLiteral("m:ss"))); + + if (((rower *)bluetoothManager->device())->lastRequestedCadence().value() > 0) { + if (bluetoothManager->device()->currentSpeed().value() <= trainProgram->currentRow().upper_speed && + bluetoothManager->device()->currentSpeed().value() >= trainProgram->currentRow().lower_speed) { + this->target_zone->setValueFontColor(QStringLiteral("limegreen")); + this->pace->setValueFontColor(QStringLiteral("limegreen")); + } else if (bluetoothManager->device()->currentSpeed().value() <= + (trainProgram->currentRow().upper_speed + 0.2) && + bluetoothManager->device()->currentSpeed().value() >= + (trainProgram->currentRow().lower_speed - 0.2)) { + this->target_zone->setValueFontColor(QStringLiteral("orange")); + this->pace->setValueFontColor(QStringLiteral("orange")); + } else { + this->target_zone->setValueFontColor(QStringLiteral("red")); + this->pace->setValueFontColor(QStringLiteral("red")); + } + } else { + this->target_zone->setValueFontColor(QStringLiteral("white")); + this->pace->setValueFontColor(QStringLiteral("white")); + } + switch (trainProgram->currentRow().pace_intensity) { + case 0: + this->target_zone->setValue(tr("Rec.")); + break; + case 1: + this->target_zone->setValue(tr("Easy")); + break; + case 2: + this->target_zone->setValue(tr("Moder.")); + break; + case 3: + this->target_zone->setValue(tr("Chall.")); + break; + case 4: + this->target_zone->setValue(tr("Max")); + break; + default: + this->target_zone->setValue(tr("N/A")); + break; + } + } odometer->setValue(QString::number(bluetoothManager->device()->odometer() * 1000.0, 'f', 0)); resistance = ((rower *)bluetoothManager->device())->currentResistance().value(); peloton_resistance = ((rower *)bluetoothManager->device())->pelotonResistance().value(); @@ -3529,6 +3956,9 @@ void homeform::update() { this->strokesLength->setValue( QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().value(), 'f', 1)); + this->target_speed->setValue(QString::number( + ((rower *)bluetoothManager->device())->lastRequestedSpeed().value() * unit_conversion, 'f', 1)); + this->peloton_resistance->setValue(QString::number(peloton_resistance, 'f', 0)); this->target_resistance->setValue( QString::number(((rower *)bluetoothManager->device())->lastRequestedResistance().value(), 'f', 0)); @@ -3564,31 +3994,45 @@ void homeform::update() { QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().average(), 'f', 1) + QStringLiteral(" MAX: ") + QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().max(), 'f', 1)); - if (bluetoothManager->device()->currentSpeed().value() < 4) { - speed->setValueFontColor(QStringLiteral("white")); - this->pace->setValueFontColor(QStringLiteral("white")); - } else if (bluetoothManager->device()->currentSpeed().value() < 5) { - speed->setValueFontColor(QStringLiteral("limegreen")); - this->pace->setValueFontColor(QStringLiteral("limegreen")); - } else if (bluetoothManager->device()->currentSpeed().value() < 5.5) { - speed->setValueFontColor(QStringLiteral("gold")); - this->pace->setValueFontColor(QStringLiteral("gold")); - } else if (bluetoothManager->device()->currentSpeed().value() < 6) { - speed->setValueFontColor(QStringLiteral("orange")); - this->pace->setValueFontColor(QStringLiteral("orange")); - } else if (bluetoothManager->device()->currentSpeed().value() < 6.5) { - speed->setValueFontColor(QStringLiteral("darkorange")); - this->pace->setValueFontColor(QStringLiteral("darkorange")); - } else if (bluetoothManager->device()->currentSpeed().value() < 7) { - speed->setValueFontColor(QStringLiteral("orangered")); - this->pace->setValueFontColor(QStringLiteral("orangered")); - } else { - speed->setValueFontColor(QStringLiteral("red")); - this->pace->setValueFontColor(QStringLiteral("red")); + + // if there is no training program, the color is based on presets + if (!trainProgram || trainProgram->currentRow().speed == -1) { + if (bluetoothManager->device()->currentSpeed().value() < 8) { + speed->setValueFontColor(QStringLiteral("white")); + this->pace->setValueFontColor(QStringLiteral("white")); + } else if (bluetoothManager->device()->currentSpeed().value() < 10) { + speed->setValueFontColor(QStringLiteral("limegreen")); + this->pace->setValueFontColor(QStringLiteral("limegreen")); + } else if (bluetoothManager->device()->currentSpeed().value() < 11) { + speed->setValueFontColor(QStringLiteral("gold")); + this->pace->setValueFontColor(QStringLiteral("gold")); + } else if (bluetoothManager->device()->currentSpeed().value() < 12) { + speed->setValueFontColor(QStringLiteral("orange")); + this->pace->setValueFontColor(QStringLiteral("orange")); + } else if (bluetoothManager->device()->currentSpeed().value() < 13) { + speed->setValueFontColor(QStringLiteral("darkorange")); + this->pace->setValueFontColor(QStringLiteral("darkorange")); + } else if (bluetoothManager->device()->currentSpeed().value() < 14) { + speed->setValueFontColor(QStringLiteral("orangered")); + this->pace->setValueFontColor(QStringLiteral("orangered")); + } else { + speed->setValueFontColor(QStringLiteral("red")); + this->pace->setValueFontColor(QStringLiteral("red")); + } + bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } - bluetoothManager->device()->currentSpeed().setColor(speed->valueFontColor()); } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) { + if (((elliptical *)bluetoothManager->device())->currentSpeed().value() > 2) + this->pace->setValue( + ((elliptical *)bluetoothManager->device())->currentPace().toString(QStringLiteral("m:ss"))); + else + this->pace->setValue("N/A"); + this->pace->setSecondLine( + QStringLiteral("AVG: ") + + ((elliptical *)bluetoothManager->device())->averagePace().toString(QStringLiteral("m:ss")) + + QStringLiteral(" MAX: ") + + ((elliptical *)bluetoothManager->device())->maxPace().toString(QStringLiteral("m:ss"))); odometer->setValue(QString::number(bluetoothManager->device()->odometer() * unit_conversion, 'f', 2)); resistance = ((elliptical *)bluetoothManager->device())->currentResistance().value(); peloton_resistance = ((elliptical *)bluetoothManager->device())->pelotonResistance().value(); @@ -3641,6 +4085,18 @@ void homeform::update() { if (trainProgram) { int8_t lower_requested_peloton_resistance = trainProgram->currentRow().lower_requested_peloton_resistance; int8_t upper_requested_peloton_resistance = trainProgram->currentRow().upper_requested_peloton_resistance; + double lower_requested_peloton_resistance_to_bike_resistance = 0; + if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + lower_requested_peloton_resistance_to_bike_resistance = + ((bike *)bluetoothManager->device())->pelotonToBikeResistance(lower_requested_peloton_resistance); + else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) + lower_requested_peloton_resistance_to_bike_resistance = + ((rower *)bluetoothManager->device())->pelotonToBikeResistance(lower_requested_peloton_resistance); + else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) + lower_requested_peloton_resistance_to_bike_resistance = + ((elliptical *)bluetoothManager->device()) + ->pelotonToEllipticalResistance(lower_requested_peloton_resistance); + if (lower_requested_peloton_resistance != -1) { this->target_peloton_resistance->setSecondLine( QStringLiteral("MIN: ") + QString::number(lower_requested_peloton_resistance, 'f', 0) + @@ -3655,7 +4111,10 @@ void homeform::update() { .toBool()) { if (lower_requested_peloton_resistance == -1) { this->peloton_resistance->setValueFontColor(QStringLiteral("white")); - } else if (((int8_t)qRound(peloton_resistance)) < lower_requested_peloton_resistance) { + } else if (resistance < lower_requested_peloton_resistance_to_bike_resistance) { + // we need to compare the real resistance and not the peloton resistance because most of the bikes + // have a 1:3 conversion so this compare will be always true even if the actual resistance is the + // same #1608 this->peloton_resistance->setValueFontColor(QStringLiteral("red")); } else if (((int8_t)qRound(peloton_resistance)) <= upper_requested_peloton_resistance) { this->peloton_resistance->setValueFontColor(QStringLiteral("limegreen")); @@ -3791,7 +4250,8 @@ void homeform::update() { QString::number(ftpPerc, 'f', 0) + QStringLiteral("%")); if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE || - bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { + (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING && + (!trainProgram || trainProgram->currentRow().pace_intensity == -1))) { if (requestedPerc < 56) { requestedMinW = QString::number(0, 'f', 0); @@ -4009,9 +4469,11 @@ void homeform::update() { #ifdef Q_OS_ANDROID if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() && KeepAwakeHelper::antObject(false)) { - KeepAwakeHelper::antObject(false)->callMethod( - "setCadenceSpeedPower", "(FII)V", (float)bluetoothManager->device()->currentSpeed().value(), (int)watts, - (int)cadence); + double v = bluetoothManager->device()->currentSpeed().value(); + v *= settings.value(QZSettings::ant_speed_gain, QZSettings::default_ant_speed_gain).toDouble(); + v += settings.value(QZSettings::ant_speed_offset, QZSettings::default_ant_speed_offset).toDouble(); + KeepAwakeHelper::antObject(false)->callMethod("setCadenceSpeedPower", "(FII)V", (float)v, (int)watts, + (int)cadence); } #endif @@ -4287,7 +4749,7 @@ void homeform::update() { bool fromTrainProgram = trainProgram && trainProgram->currentRow().HRmin > 0 && trainProgram->currentRow().HRmax > 0; double maxSpeed = 30; - double minSpeed = 30; + double minSpeed = 0; int8_t maxResistance = 100; if (fromTrainProgram) { @@ -4298,10 +4760,10 @@ void homeform::update() { last_seconds_pid_heart_zone = seconds; - uint8_t hrmin = + int16_t hrmin = settings.value(QZSettings::treadmill_pid_heart_min, QZSettings::default_treadmill_pid_heart_min) .toInt(); - uint8_t hrmax = + int16_t hrmax = settings.value(QZSettings::treadmill_pid_heart_max, QZSettings::default_treadmill_pid_heart_max) .toInt(); if (fromTrainProgram) { @@ -4318,20 +4780,23 @@ void homeform::update() { } } + if (hrmax == 0 || hrmax == -1) + hrmax = 220; + if (!stopped && !paused && bluetoothManager->device()->currentHeart().value() && bluetoothManager->device()->currentSpeed().value() > 0.0f) { if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) { const double step = 0.2; double currentSpeed = ((treadmill *)bluetoothManager->device())->currentSpeed().value(); - if (hrmax < bluetoothManager->device()->currentHeart().value() && - minSpeed <= currentSpeed + step) { + if (hrmax < bluetoothManager->device()->currentHeart().average5s() && + minSpeed <= currentSpeed - step) { ((treadmill *)bluetoothManager->device()) ->changeSpeedAndInclination( currentSpeed - step, ((treadmill *)bluetoothManager->device())->currentInclination().value()); pid_heart_zone_small_inc_counter = 0; - } else if (hrmin > bluetoothManager->device()->currentHeart().value() && + } else if (hrmin > bluetoothManager->device()->currentHeart().average5s() && maxSpeed >= currentSpeed + step) { ((treadmill *)bluetoothManager->device()) ->changeSpeedAndInclination( @@ -4339,7 +4804,8 @@ void homeform::update() { currentSpeed + step, ((treadmill *)bluetoothManager->device())->currentInclination().value()); pid_heart_zone_small_inc_counter = 0; - } else if (maxSpeed >= currentSpeed + step) { + } else if (maxSpeed >= currentSpeed + step && + hrmax < bluetoothManager->device()->currentHeart().average5s()) { pid_heart_zone_small_inc_counter++; if (pid_heart_zone_small_inc_counter > 6) { ((treadmill *)bluetoothManager->device()) @@ -4354,10 +4820,10 @@ void homeform::update() { const int step = 1; resistance_t currentResistance = ((bike *)bluetoothManager->device())->currentResistance().value(); - if (hrmax < bluetoothManager->device()->currentHeart().value()) { + if (hrmax < bluetoothManager->device()->currentHeart().average5s()) { ((bike *)bluetoothManager->device())->changeResistance(currentResistance - step); - } else if (hrmin > bluetoothManager->device()->currentHeart().value() && + } else if (hrmin > bluetoothManager->device()->currentHeart().average5s() && maxResistance >= currentResistance + step) { ((bike *)bluetoothManager->device())->changeResistance(currentResistance + step); @@ -4367,10 +4833,10 @@ void homeform::update() { const int step = 1; resistance_t currentResistance = ((rower *)bluetoothManager->device())->currentResistance().value(); - if (hrmax < bluetoothManager->device()->currentHeart().value()) { + if (hrmax < bluetoothManager->device()->currentHeart().average5s()) { ((rower *)bluetoothManager->device())->changeResistance(currentResistance - step); - } else if (hrmin > bluetoothManager->device()->currentHeart().value()) { + } else if (hrmin > bluetoothManager->device()->currentHeart().average5s()) { ((rower *)bluetoothManager->device())->changeResistance(currentResistance + step); } @@ -4426,201 +4892,240 @@ void homeform::update() { if (!stopped && !paused) { if (settings.value(QZSettings::tts_enabled, QZSettings::default_tts_enabled).toBool()) { + static double tts_speed_played = 0; bool description = settings.value(QZSettings::tts_description_enabled, QZSettings::default_tts_description_enabled) .toBool(); - if (++tts_summary_count >= - settings.value(QZSettings::tts_summary_sec, QZSettings::default_tts_summary_sec).toInt() && - m_speech.state() == QTextToSpeech::Ready) { - tts_summary_count = 0; - - QString s; - if (settings.value(QZSettings::tts_act_speed, QZSettings::default_tts_act_speed).toBool()) - s.append((description ? tr(", speed ") : ",") + + if (m_speech.state() == QTextToSpeech::Ready) { + if (++tts_summary_count >= + settings.value(QZSettings::tts_summary_sec, QZSettings::default_tts_summary_sec).toInt()) { + tts_summary_count = 0; + + QString s; + if (settings.value(QZSettings::tts_act_speed, QZSettings::default_tts_act_speed).toBool()) + s.append( + (description ? tr(", speed ") : ",") + + (!miles ? QString::number(bluetoothManager->device()->currentSpeed().value(), 'f', 1) + + (description ? tr(" kilometers per hour") : "") + : QString::number(bluetoothManager->device()->currentSpeed().value() * + unit_conversion, + 'f', 1)) + + (description ? tr(" miles per hour") : "")); + if (settings.value(QZSettings::tts_avg_speed, QZSettings::default_tts_avg_speed).toBool()) + s.append((description ? tr(", Average speed ") : ",") + + (!miles ? QString::number(bluetoothManager->device()->currentSpeed().average(), + 'f', 1) + + (description ? tr("kilometers per hour") : "") + : QString::number(bluetoothManager->device()->currentSpeed().average() * + unit_conversion, + 'f', 1)) + + (description ? tr(" miles per hour") : "")); + if (settings.value(QZSettings::tts_max_speed, QZSettings::default_tts_max_speed).toBool()) + s.append((description ? tr(", Max speed ") : ",") + + (!miles + ? QString::number(bluetoothManager->device()->currentSpeed().max(), 'f', 1) + + (description ? tr(" kilometers per hour") : "") + : QString::number(bluetoothManager->device()->currentSpeed().max() * + unit_conversion, + 'f', 1)) + + (description ? tr(" miles per hour") : "")); + if (settings.value(QZSettings::tts_act_inclination, QZSettings::default_tts_act_inclination) + .toBool()) + s.append((description ? tr(", inclination ") : ",") + + QString::number(bluetoothManager->device()->currentInclination().value(), 'f', 1)); + if (settings.value(QZSettings::tts_act_cadence, QZSettings::default_tts_act_cadence).toBool()) + s.append((description ? tr(", cadence ") : ",") + + QString::number(bluetoothManager->device()->currentCadence().value(), 'f', 0)); + if (settings.value(QZSettings::tts_avg_cadence, QZSettings::default_tts_avg_cadence).toBool()) + s.append((description ? tr(", Average cadence ") : ",") + + QString::number(bluetoothManager->device()->currentCadence().average(), 'f', 0)); + if (settings.value(QZSettings::tts_max_cadence, QZSettings::default_tts_max_cadence /* true */) + .toBool()) + s.append((description ? tr(", Max cadence ") : ",") + + QString::number(bluetoothManager->device()->currentCadence().max())); + if (settings.value(QZSettings::tts_act_elevation, QZSettings::default_tts_act_elevation) + .toBool()) + s.append( + (description ? tr(", elevation ") : ",") + + (!miles ? QString::number(bluetoothManager->device()->elevationGain().value(), 'f', 1) + + (description ? tr(" meters") : "") + : QString::number(bluetoothManager->device()->elevationGain().value() * + meter_feet_conversion, + 'f', 1)) + + (description ? tr(" feet") : "")); + if (settings.value(QZSettings::tts_act_calories, QZSettings::default_tts_act_calories).toBool()) + s.append((description ? tr(", calories burned ") : ",") + + QString::number(bluetoothManager->device()->calories().value(), 'f', 0)); + if (settings.value(QZSettings::tts_act_odometer, QZSettings::default_tts_act_odometer).toBool()) + s.append((description ? tr(", distance ") : ",") + + (!miles ? QString::number(bluetoothManager->device()->odometer(), 'f', 1) + + (description ? tr("kilometers") : "") + : QString::number(bluetoothManager->device()->odometer() * unit_conversion, + 'f', 1)) + + (description ? tr(" miles") : "")); + if (settings.value(QZSettings::tts_act_target_pace, QZSettings::default_tts_act_target_pace) + .toBool()) { + if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) + s.append((description ? tr(", pace ") : ",") + ((rower *)bluetoothManager->device()) + ->lastRequestedPace() + .toString(QStringLiteral("m:ss"))); + else if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) + s.append((description ? tr(", pace ") : ",") + ((treadmill *)bluetoothManager->device()) + ->lastRequestedPace() + .toString(QStringLiteral("m:ss"))); + } + if (settings.value(QZSettings::tts_act_pace, QZSettings::default_tts_act_pace).toBool()) + s.append((description ? tr(", pace ") : ",") + + bluetoothManager->device()->currentPace().toString(QStringLiteral("m:ss"))); + if (settings.value(QZSettings::tts_avg_pace, QZSettings::default_tts_avg_pace).toBool()) + s.append((description ? tr(", pace ") : ",") + + bluetoothManager->device()->averagePace().toString(QStringLiteral("m:ss"))); + if (settings.value(QZSettings::tts_max_pace, QZSettings::default_tts_max_pace).toBool()) + s.append((description ? tr(", pace ") : ",") + + bluetoothManager->device()->maxPace().toString(QStringLiteral("m:ss"))); + if (settings.value(QZSettings::tts_act_resistance, QZSettings::default_tts_act_resistance) + .toBool()) + s.append((description ? tr(", resistance ") : ",") + + QString::number(bluetoothManager->device()->currentResistance().value(), 'f', 0)); + if (settings.value(QZSettings::tts_avg_resistance, QZSettings::default_tts_avg_resistance) + .toBool()) + s.append( + (description ? tr(", average resistance ") : ",") + + QString::number(bluetoothManager->device()->currentResistance().average(), 'f', 0)); + if (settings.value(QZSettings::tts_max_resistance, QZSettings::default_tts_max_resistance) + .toBool()) + s.append((description ? tr(", max resistance ") : ",") + + QString::number(bluetoothManager->device()->currentResistance().max(), 'f', 0)); + if (settings.value(QZSettings::tts_act_watt, QZSettings::default_tts_act_watt).toBool()) + s.append((description ? tr(", watt ") : ",") + + QString::number(bluetoothManager->device()->wattsMetric().value(), 'f', 0)); + if (settings.value(QZSettings::tts_avg_watt, QZSettings::default_tts_avg_watt).toBool()) + s.append((description ? tr(", average watt ") : ",") + + QString::number(bluetoothManager->device()->wattsMetric().average(), 'f', 0)); + if (settings.value(QZSettings::tts_max_watt, QZSettings::default_tts_max_watt).toBool()) + s.append((description ? tr(", max watt ") : ",") + + QString::number(bluetoothManager->device()->wattsMetric().max(), 'f', 0)); + if (settings.value(QZSettings::tts_act_ftp, QZSettings::default_tts_act_ftp /* true */) + .toBool()) + s.append((description ? tr(", ftp ") : ",") + QString::number(ftpZone, 'f', 1)); + if (settings.value(QZSettings::tts_act_heart, QZSettings::default_tts_act_heart).toBool()) + s.append((description ? tr(", heart rate ") : ",") + + QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0)); + if (settings.value(QZSettings::tts_avg_heart, QZSettings::default_tts_avg_heart).toBool()) + s.append((description ? tr(", average heart rate ") : ",") + + QString::number(bluetoothManager->device()->currentHeart().average(), 'f', 0)); + if (settings.value(QZSettings::tts_max_heart, QZSettings::default_tts_max_heart).toBool()) + s.append((description ? tr(", max heart rate ") : ",") + + QString::number(bluetoothManager->device()->currentHeart().max(), 'f', 0)); + if (settings.value(QZSettings::tts_act_jouls, QZSettings::default_tts_act_jouls).toBool()) + s.append((description ? tr(", jouls ") : ",") + + QString::number(bluetoothManager->device()->jouls().max(), 'f', 0)); + if (settings.value(QZSettings::tts_act_elapsed, QZSettings::default_tts_act_elapsed).toBool()) + s.append((description ? tr(", elapsed ") : ",") + + QString::number(bluetoothManager->device()->elapsedTime().minute()) + + (description ? tr(" minutes ") : "") + + QString::number(bluetoothManager->device()->elapsedTime().second()) + + (description ? tr(" seconds") : "")); + if (settings + .value(QZSettings::tts_act_peloton_resistance, + QZSettings::default_tts_act_peloton_resistance) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", peloton resistance ") : ",") + + QString::number(((bike *)bluetoothManager->device())->pelotonResistance().value(), + 'f', 0)); + if (settings + .value(QZSettings::tts_avg_peloton_resistance, + QZSettings::default_tts_avg_peloton_resistance) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", average peloton resistance ") : ",") + + QString::number( + ((bike *)bluetoothManager->device())->pelotonResistance().average(), 'f', 0)); + if (settings + .value(QZSettings::tts_max_peloton_resistance, + QZSettings::default_tts_max_peloton_resistance) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", max peloton resistance ") : ",") + + QString::number(((bike *)bluetoothManager->device())->pelotonResistance().max(), + 'f', 0)); + if (settings + .value(QZSettings::tts_act_target_peloton_resistance, + QZSettings::default_tts_act_target_peloton_resistance) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", target peloton resistance ") : ",") + + QString::number( + ((bike *)bluetoothManager->device())->lastRequestedPelotonResistance().value(), + 'f', 0)); + if (settings + .value(QZSettings::tts_act_target_cadence, QZSettings::default_tts_act_target_cadence) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", target cadence ") : ",") + + QString::number( + ((bike *)bluetoothManager->device())->lastRequestedCadence().value(), 'f', 0)); + if (settings.value(QZSettings::tts_act_target_power, QZSettings::default_tts_act_target_power) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", target power ") : ",") + + QString::number(((bike *)bluetoothManager->device())->lastRequestedPower().value(), + 'f', 0)); + if (settings.value(QZSettings::tts_act_target_zone, QZSettings::default_tts_act_target_zone) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) + s.append((description ? tr(", target zone ") : ",") + + QString::number(requestedZone, 'f', 1)); + if (settings.value(QZSettings::tts_act_target_speed, QZSettings::default_tts_act_target_speed) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) + s.append( + (description ? tr(", target speed ") : ",") + + (!miles ? QString::number( + ((treadmill *)bluetoothManager->device())->lastRequestedSpeed().value(), + 'f', 1) + + (description ? tr(" kilometers per hour") : "") + : QString::number( + ((treadmill *)bluetoothManager->device())->lastRequestedSpeed().value() * + unit_conversion, + 'f', 1)) + + (description ? tr(" miles per hour") : "")); + if (settings + .value(QZSettings::tts_act_target_incline, QZSettings::default_tts_act_target_incline) + .toBool() && + bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) + s.append((description ? tr(", target incline ") : ",") + + QString::number( + ((treadmill *)bluetoothManager->device())->lastRequestedInclination().value(), + 'f', 1)); + if (settings.value(QZSettings::tts_act_watt_kg, QZSettings::default_tts_act_watt_kg).toBool()) + s.append((description ? tr(", watt for kilograms ") : ",") + + QString::number(bluetoothManager->device()->wattKg().value(), 'f', 1)); + if (settings.value(QZSettings::tts_avg_watt_kg, QZSettings::default_tts_avg_watt_kg).toBool()) + s.append((description ? tr(", average watt for kilograms") : ",") + + QString::number(bluetoothManager->device()->wattKg().average(), 'f', 1)); + if (settings.value(QZSettings::tts_max_watt_kg, QZSettings::default_tts_max_watt_kg).toBool()) + s.append((description ? tr(", max watt for kilograms") : ",") + + QString::number(bluetoothManager->device()->wattKg().max(), 'f', 1)); + + qDebug() << "tts" << s; + m_speech.say(s); + } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL && + bluetoothManager->device()->currentSpeed().value() != tts_speed_played && + settings.value(QZSettings::tts_act_speed, QZSettings::default_tts_act_speed).toBool()) { + tts_speed_played = bluetoothManager->device()->currentSpeed().value(); + QString s; + s.append((description ? tr("speed changed to") : "") + (!miles ? QString::number(bluetoothManager->device()->currentSpeed().value(), 'f', 1) + (description ? tr(" kilometers per hour") : "") : QString::number(bluetoothManager->device()->currentSpeed().value() * unit_conversion, 'f', 1)) + (description ? tr(" miles per hour") : "")); - if (settings.value(QZSettings::tts_avg_speed, QZSettings::default_tts_avg_speed).toBool()) - s.append((description ? tr(", Average speed ") : ",") + - (!miles - ? QString::number(bluetoothManager->device()->currentSpeed().average(), 'f', 1) + - (description ? tr("kilometers per hour") : "") - : QString::number(bluetoothManager->device()->currentSpeed().average() * - unit_conversion, - 'f', 1)) + - (description ? tr(" miles per hour") : "")); - if (settings.value(QZSettings::tts_max_speed, QZSettings::default_tts_max_speed).toBool()) - s.append( - (description ? tr(", Max speed ") : ",") + - (!miles ? QString::number(bluetoothManager->device()->currentSpeed().max(), 'f', 1) + - (description ? tr(" kilometers per hour") : "") - : QString::number( - bluetoothManager->device()->currentSpeed().max() * unit_conversion, 'f', 1)) + - (description ? tr(" miles per hour") : "")); - if (settings.value(QZSettings::tts_act_inclination, QZSettings::default_tts_act_inclination) - .toBool()) - s.append((description ? tr(", inclination ") : ",") + - QString::number(bluetoothManager->device()->currentInclination().value(), 'f', 1)); - if (settings.value(QZSettings::tts_act_cadence, QZSettings::default_tts_act_cadence).toBool()) - s.append((description ? tr(", cadence ") : ",") + - QString::number(bluetoothManager->device()->currentCadence().value(), 'f', 0)); - if (settings.value(QZSettings::tts_avg_cadence, QZSettings::default_tts_avg_cadence).toBool()) - s.append((description ? tr(", Average cadence ") : ",") + - QString::number(bluetoothManager->device()->currentCadence().average(), 'f', 0)); - if (settings.value(QZSettings::tts_max_cadence, QZSettings::default_tts_max_cadence /* true */) - .toBool()) - s.append((description ? tr(", Max cadence ") : ",") + - QString::number(bluetoothManager->device()->currentCadence().max())); - if (settings.value(QZSettings::tts_act_elevation, QZSettings::default_tts_act_elevation).toBool()) - s.append((description ? tr(", elevation ") : ",") + - (!miles - ? QString::number(bluetoothManager->device()->elevationGain().value(), 'f', 1) + - (description ? tr(" meters") : "") - : QString::number(bluetoothManager->device()->elevationGain().value() * - meter_feet_conversion, - 'f', 1)) + - (description ? tr(" feet") : "")); - if (settings.value(QZSettings::tts_act_calories, QZSettings::default_tts_act_calories).toBool()) - s.append((description ? tr(", calories burned ") : ",") + - QString::number(bluetoothManager->device()->calories().value(), 'f', 0)); - if (settings.value(QZSettings::tts_act_odometer, QZSettings::default_tts_act_odometer).toBool()) - s.append((description ? tr(", distance ") : ",") + - (!miles ? QString::number(bluetoothManager->device()->odometer(), 'f', 1) + - (description ? tr("kilometers") : "") - : QString::number(bluetoothManager->device()->odometer() * unit_conversion, - 'f', 1)) + - (description ? tr(" miles") : "")); - if (settings.value(QZSettings::tts_act_pace, QZSettings::default_tts_act_pace).toBool()) - s.append((description ? tr(", pace ") : ",") + - bluetoothManager->device()->currentPace().toString(QStringLiteral("m:ss"))); - if (settings.value(QZSettings::tts_avg_pace, QZSettings::default_tts_avg_pace).toBool()) - s.append((description ? tr(", pace ") : ",") + - bluetoothManager->device()->averagePace().toString(QStringLiteral("m:ss"))); - if (settings.value(QZSettings::tts_max_pace, QZSettings::default_tts_max_pace).toBool()) - s.append((description ? tr(", pace ") : ",") + - bluetoothManager->device()->maxPace().toString(QStringLiteral("m:ss"))); - if (settings.value(QZSettings::tts_act_resistance, QZSettings::default_tts_act_resistance).toBool()) - s.append((description ? tr(", resistance ") : ",") + - QString::number(bluetoothManager->device()->currentResistance().value(), 'f', 0)); - if (settings.value(QZSettings::tts_avg_resistance, QZSettings::default_tts_avg_resistance).toBool()) - s.append((description ? tr(", average resistance ") : ",") + - QString::number(bluetoothManager->device()->currentResistance().average(), 'f', 0)); - if (settings.value(QZSettings::tts_max_resistance, QZSettings::default_tts_max_resistance).toBool()) - s.append((description ? tr(", max resistance ") : ",") + - QString::number(bluetoothManager->device()->currentResistance().max(), 'f', 0)); - if (settings.value(QZSettings::tts_act_watt, QZSettings::default_tts_act_watt).toBool()) - s.append((description ? tr(", watt ") : ",") + - QString::number(bluetoothManager->device()->wattsMetric().value(), 'f', 0)); - if (settings.value(QZSettings::tts_avg_watt, QZSettings::default_tts_avg_watt).toBool()) - s.append((description ? tr(", average watt ") : ",") + - QString::number(bluetoothManager->device()->wattsMetric().average(), 'f', 0)); - if (settings.value(QZSettings::tts_max_watt, QZSettings::default_tts_max_watt).toBool()) - s.append((description ? tr(", max watt ") : ",") + - QString::number(bluetoothManager->device()->wattsMetric().max(), 'f', 0)); - if (settings.value(QZSettings::tts_act_ftp, QZSettings::default_tts_act_ftp /* true */).toBool()) - s.append((description ? tr(", ftp ") : ",") + QString::number(ftpZone, 'f', 1)); - if (settings.value(QZSettings::tts_act_heart, QZSettings::default_tts_act_heart).toBool()) - s.append((description ? tr(", heart rate ") : ",") + - QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0)); - if (settings.value(QZSettings::tts_avg_heart, QZSettings::default_tts_avg_heart).toBool()) - s.append((description ? tr(", average heart rate ") : ",") + - QString::number(bluetoothManager->device()->currentHeart().average(), 'f', 0)); - if (settings.value(QZSettings::tts_max_heart, QZSettings::default_tts_max_heart).toBool()) - s.append((description ? tr(", max heart rate ") : ",") + - QString::number(bluetoothManager->device()->currentHeart().max(), 'f', 0)); - if (settings.value(QZSettings::tts_act_jouls, QZSettings::default_tts_act_jouls).toBool()) - s.append((description ? tr(", jouls ") : ",") + - QString::number(bluetoothManager->device()->jouls().max(), 'f', 0)); - if (settings.value(QZSettings::tts_act_elapsed, QZSettings::default_tts_act_elapsed).toBool()) - s.append((description ? tr(", elapsed ") : ",") + - QString::number(bluetoothManager->device()->elapsedTime().minute()) + - (description ? tr(" minutes ") : "") + - QString::number(bluetoothManager->device()->elapsedTime().second()) + - (description ? tr(" seconds") : "")); - if (settings - .value(QZSettings::tts_act_peloton_resistance, - QZSettings::default_tts_act_peloton_resistance) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append( - (description ? tr(", peloton resistance ") : ",") + - QString::number(((bike *)bluetoothManager->device())->pelotonResistance().value(), 'f', 0)); - if (settings - .value(QZSettings::tts_avg_peloton_resistance, - QZSettings::default_tts_avg_peloton_resistance) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append((description ? tr(", average peloton resistance ") : ",") + - QString::number(((bike *)bluetoothManager->device())->pelotonResistance().average(), - 'f', 0)); - if (settings - .value(QZSettings::tts_max_peloton_resistance, - QZSettings::default_tts_max_peloton_resistance) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append( - (description ? tr(", max peloton resistance ") : ",") + - QString::number(((bike *)bluetoothManager->device())->pelotonResistance().max(), 'f', 0)); - if (settings - .value(QZSettings::tts_act_target_peloton_resistance, - QZSettings::default_tts_act_target_peloton_resistance) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append((description ? tr(", target peloton resistance ") : ",") + - QString::number( - ((bike *)bluetoothManager->device())->lastRequestedPelotonResistance().value(), - 'f', 0)); - if (settings.value(QZSettings::tts_act_target_cadence, QZSettings::default_tts_act_target_cadence) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append((description ? tr(", target cadence ") : ",") + - QString::number(((bike *)bluetoothManager->device())->lastRequestedCadence().value(), - 'f', 0)); - if (settings.value(QZSettings::tts_act_target_power, QZSettings::default_tts_act_target_power) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append((description ? tr(", target power ") : ",") + - QString::number(((bike *)bluetoothManager->device())->lastRequestedPower().value(), - 'f', 0)); - if (settings.value(QZSettings::tts_act_target_zone, QZSettings::default_tts_act_target_zone) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) - s.append((description ? tr(", target zone ") : ",") + QString::number(requestedZone, 'f', 1)); - if (settings.value(QZSettings::tts_act_target_speed, QZSettings::default_tts_act_target_speed) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) - s.append((description ? tr(", target speed ") : ",") + - (!miles ? QString::number( - ((treadmill *)bluetoothManager->device())->lastRequestedSpeed().value(), - 'f', 1) + - (description ? tr(" kilometers per hour") : "") - : QString::number( - ((treadmill *)bluetoothManager->device())->lastRequestedSpeed().value() * - unit_conversion, - 'f', 1)) + - (description ? tr(" miles per hour") : "")); - if (settings.value(QZSettings::tts_act_target_incline, QZSettings::default_tts_act_target_incline) - .toBool() && - bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) - s.append( - (description ? tr(", target incline ") : ",") + - QString::number( - ((treadmill *)bluetoothManager->device())->lastRequestedInclination().value(), 'f', 1)); - if (settings.value(QZSettings::tts_act_watt_kg, QZSettings::default_tts_act_watt_kg).toBool()) - s.append((description ? tr(", watt for kilograms ") : ",") + - QString::number(bluetoothManager->device()->wattKg().value(), 'f', 1)); - if (settings.value(QZSettings::tts_avg_watt_kg, QZSettings::default_tts_avg_watt_kg).toBool()) - s.append((description ? tr(", average watt for kilograms") : ",") + - QString::number(bluetoothManager->device()->wattKg().average(), 'f', 1)); - if (settings.value(QZSettings::tts_max_watt_kg, QZSettings::default_tts_max_watt_kg).toBool()) - s.append((description ? tr(", max watt for kilograms") : ",") + - QString::number(bluetoothManager->device()->wattKg().max(), 'f', 1)); - - qDebug() << "tts" << s; - m_speech.say(s); + qDebug() << "tts" << s; + m_speech.say(s); + } } } @@ -4641,6 +5146,21 @@ void homeform::update() { if (lapTrigger) { lapTrigger = false; } + +#ifndef Q_OS_IOS + if (iphone_socket && iphone_socket->state() == QAbstractSocket::ConnectedState) { + QString toSend = + "SENDER=PAD#HR=" + QString::number(bluetoothManager->device()->currentHeart().value()) + + "#KCAL=" + QString::number(bluetoothManager->device()->calories().value()) + + "#BCAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) + + "#SPD=" + QString::number(bluetoothManager->device()->currentSpeed().value()) + + "#PWR=" + QString::number(bluetoothManager->device()->wattsMetric().value()) + + "#CAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) + + "#ODO=" + QString::number(bluetoothManager->device()->odometer()) + "#"; + int write = iphone_socket->write(toSend.toLocal8Bit(), toSend.length()); + qDebug() << "iphone_socket send " << write << toSend; + } +#endif } emit workoutStartDateChanged(workoutStartDate()); } @@ -4671,7 +5191,6 @@ bool homeform::getLap() { void homeform::trainprogram_open_clicked(const QUrl &fileName) { qDebug() << QStringLiteral("trainprogram_open_clicked") << fileName; - stravaWorkoutName = QFileInfo(fileName.fileName()).baseName(); QFile file(QQmlFile::urlToLocalFileOrQrc(fileName)); qDebug() << file.fileName(); @@ -4684,7 +5203,34 @@ void homeform::trainprogram_open_clicked(const QUrl &fileName) { if (trainProgram) { delete trainProgram; } + trainProgram = trainprogram::load(file.fileName(), bluetoothManager); + + QString movieName = file.fileName().left(file.fileName().length() - 3) + "mp4"; + if (QFile::exists(movieName)) { + qDebug() << movieName << QStringLiteral("exist!"); + movieFileName = QUrl::fromLocalFile(movieName); + emit videoPathChanged(movieFileName); + setVideoIconVisible(true); + setVideoRate(1); + trainingProgram()->setVideoAvailable(true); + } else { + qDebug() << movieName << QStringLiteral("doesn't exist!"); + movieFileName = ""; + setVideoIconVisible(false); + trainingProgram()->setVideoAvailable(false); + } + + stravaWorkoutName = QFileInfo(fileName.fileName()).baseName(); + stravaPelotonInstructorName = QStringLiteral(""); + emit workoutNameChanged(workoutName()); + emit instructorNameChanged(instructorName()); + + QSettings settings; + if (settings.value(QZSettings::top_bar_enabled, QZSettings::default_top_bar_enabled).toBool()) { + m_info = workoutName(); + emit infoChanged(m_info); + } } trainProgramSignals(); @@ -4754,9 +5300,14 @@ void homeform::fit_save_clicked() { QString filename = path + QDateTime::currentDateTime().toString().replace(QStringLiteral(":"), QStringLiteral("_")) + QStringLiteral(".fit"); + + QString workoutName = ""; + if (!stravaPelotonActivityName.isEmpty() && !stravaPelotonInstructorName.isEmpty()) + workoutName = stravaPelotonActivityName + " - " + stravaPelotonInstructorName; + qfit::save(filename, Session, dev->deviceType(), qobject_cast(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE, - stravaPelotonWorkoutType); + stravaPelotonWorkoutType, workoutName, dev->bluetoothDevice.name()); lastFitFileSaved = filename; QSettings settings; @@ -4963,6 +5514,8 @@ void homeform::strava_refreshtoken() { // oops, no dice if (reply->error() != 0) { qDebug() << QStringLiteral("Got error") << reply->errorString().toStdString().c_str(); + setToastRequested("Strava Auth Failed!"); + emit toastRequestedChanged(toastRequested()); return; } @@ -4984,6 +5537,9 @@ void homeform::strava_refreshtoken() { settings.setValue(QZSettings::strava_accesstoken, access_token); settings.setValue(QZSettings::strava_refreshtoken, refresh_token); settings.setValue(QZSettings::strava_lastrefresh, QDateTime::currentDateTime()); + + setToastRequested("Strava Login OK!"); + emit toastRequestedChanged(toastRequested()); } bool homeform::strava_upload_file(const QByteArray &data, const QString &remotename) { @@ -5017,6 +5573,10 @@ bool homeform::strava_upload_file(const QByteArray &data, const QString &remoten activityNamePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(QStringLiteral("form-data; name=\"name\""))); + QString prefix = QStringLiteral(""); + if (settings.value(QZSettings::strava_date_prefix, QZSettings::default_strava_date_prefix).toBool()) + prefix = " " + QDate::currentDate().toString(Qt::TextDate); + // use metadata config if the user selected it QString activityName = QStringLiteral(" ") + settings.value(QZSettings::strava_suffix, QZSettings::default_strava_suffix).toString(); @@ -5029,11 +5589,11 @@ bool homeform::strava_upload_file(const QByteArray &data, const QString &remoten pelotonHandler->current_ride_id; } else { if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) { - activityName = QStringLiteral("Run") + activityName; + activityName = prefix + QStringLiteral("Run") + activityName; } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { - activityName = QStringLiteral("Row") + activityName; + activityName = prefix + QStringLiteral("Row") + activityName; } else { - activityName = QStringLiteral("Ride") + activityName; + activityName = prefix + QStringLiteral("Ride") + activityName; } } activityNamePart.setHeader(QNetworkRequest::ContentTypeHeader, @@ -5096,6 +5656,8 @@ bool homeform::strava_upload_file(const QByteArray &data, const QString &remoten void homeform::errorOccurredUploadStrava(QNetworkReply::NetworkError code) { qDebug() << QStringLiteral("strava upload error!") << code; + setToastRequested("Strava Upload Failed!"); + emit toastRequestedChanged(toastRequested()); } void homeform::writeFileCompleted() { @@ -5108,6 +5670,9 @@ void homeform::writeFileCompleted() { // NOTE: clazy-unused-non-trivial-variable qDebug() << "reply:" << response; + + setToastRequested("Strava Upload Completed!"); + emit toastRequestedChanged(toastRequested()); } void homeform::onStravaGranted() { @@ -5327,6 +5892,14 @@ void homeform::setVideoIconVisible(bool value) { emit videoIconVisibleChanged(m_VideoIconVisible); } +bool homeform::chartIconVisible() { return m_ChartIconVisible; } + +void homeform::setChartIconVisible(bool value) { + + m_ChartIconVisible = value; + emit chartIconVisibleChanged(m_ChartIconVisible); +} + int homeform::videoPosition() { return m_VideoPosition; } void homeform::setVideoPosition(int value) { @@ -5345,6 +5918,13 @@ void homeform::setVideoRate(double value) { void homeform::smtpError(SmtpClient::SmtpError e) { qDebug() << QStringLiteral("SMTP ERROR") << e; } +QByteArray homeform::currentPelotonImage() { + if (pelotonHandler && pelotonHandler->current_image_downloaded && + !pelotonHandler->current_image_downloaded->downloadedData().isEmpty()) + return pelotonHandler->current_image_downloaded->downloadedData(); + return QByteArray(); +} + void homeform::sendMail() { QSettings settings; @@ -5470,8 +6050,30 @@ void homeform::sendMail() { QStringLiteral("Moving Time: ") + bluetoothManager->device()->movingTime().toString() + QStringLiteral("\n"); textMessage += QStringLiteral("Weight Loss (") + weightLossUnit + "): " + QString::number(WeightLoss, 'f', 2) + QStringLiteral("\n"); - textMessage += QStringLiteral("Estimated VO2Max: ") + QString::number(metric::calculateVO2Max(&Session), 'f', 1) + + textMessage += QStringLiteral("Estimated VO2Max: ") + QString::number(metric::calculateVO2Max(&Session), 'f', 0) + QStringLiteral("\n"); + double peak = metric::powerPeak(&Session, 5); + double weightKg = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + textMessage += QStringLiteral("5 Seconds Power: ") + QString::number(peak, 'f', 0) + + QStringLiteral("W ") + QString::number(peak/weightKg, 'f', 1) + QStringLiteral("W/Kg\n"); + peak = metric::powerPeak(&Session, 60); + textMessage += QStringLiteral("1 Minute Power: ") + QString::number(peak, 'f', 0) + + QStringLiteral("W ") + QString::number(peak/weightKg, 'f', 1) + QStringLiteral("W/Kg\n"); + peak = metric::powerPeak(&Session, 5 * 60); + textMessage += QStringLiteral("5 Minutes Power: ") + QString::number(peak, 'f', 0) + + QStringLiteral("W ") + QString::number(peak/weightKg, 'f', 1) + QStringLiteral("W/Kg\n"); + + // FTP + double ftpSetting = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble(); + peak = (metric::powerPeak(&Session, 20 * 60) * 0.95) * 0.95; + textMessage += QStringLiteral("Estimated FTP: ") + QString::number(peak, 'f', 0) + + QStringLiteral("W "); + if(peak > ftpSetting) { + textMessage += QStringLiteral(" FTP IMPROVED +") + QString::number(peak - ftpSetting, 'f', 0) + + QStringLiteral("W!"); + } + textMessage += QStringLiteral("\n"); + if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { textMessage += QStringLiteral("Average Cadence: ") + QString::number(((bike *)bluetoothManager->device())->currentCadence().average(), 'f', 0) + @@ -5540,6 +6142,10 @@ void homeform::sendMail() { } } +#ifdef SMTP_SERVER + textMessage += QStringLiteral("\n\nSMTP server: ") + QString(STRINGIFY(SMTP_SERVER)); +#endif + text.setText(textMessage); message.addPart(&text); @@ -5564,6 +6170,19 @@ void homeform::sendMail() { fit->setContentType(QStringLiteral("application/octet-stream")); message.addPart(fit); } + + if (!lastTrainProgramFileSaved.isEmpty()) { + + // Create a MimeInlineFile object for each image + MimeInlineFile *xml = new MimeInlineFile((new QFile(lastTrainProgramFileSaved))); + + // An unique content id must be setted + xml->setContentId(lastTrainProgramFileSaved); + xml->setContentType(QStringLiteral("application/octet-stream")); + message.addPart(xml); + lastTrainProgramFileSaved = ""; + } + if (pelotonHandler && pelotonHandler->current_image_downloaded && !pelotonHandler->current_image_downloaded->downloadedData().isEmpty()) { @@ -5872,9 +6491,9 @@ void homeform::changeTimestamp(QTime source, QTime actual) { // Video is started now, calculate and set the Rate if (!videoMustBeReset) { // calculate and set the new Video Rate - double rate = trainProgram->TimeRateFromGPX(((double)QTime(0, 0, 0).msecsTo(source)) / 1000.0, - videoTimeStampSeconds, - bluetoothManager->device()->currentSpeed().average5s()); + double rate = trainProgram->TimeRateFromGPX( + ((double)QTime(0, 0, 0).msecsTo(source)) / 1000.0, videoTimeStampSeconds, + bluetoothManager->device()->currentSpeed().average5s(), recordingFactor); rate = rate / ((double)(recordingFactor)); setVideoRate(rate); } else { diff --git a/src/homeform.h b/src/homeform.h index 640c05245..a426d45f0 100644 --- a/src/homeform.h +++ b/src/homeform.h @@ -6,6 +6,9 @@ #include "fit_profile.hpp" #include "gpx.h" #include "peloton.h" +#include "qmdnsengine/browser.h" +#include "qmdnsengine/cache.h" +#include "qmdnsengine/resolver.h" #include "screencapture.h" #include "sessionline.h" #include "smtpclient/src/SmtpMime" @@ -21,6 +24,17 @@ #include #include +#if __has_include("secret.h") +#include "secret.h" +#else +#define STRAVA_SECRET_KEY test +#if defined(WIN32) +#pragma message("DEFINE STRAVA_SECRET_KEY!!!") +#else +#warning "DEFINE STRAVA_SECRET_KEY!!!" +#endif +#endif + class DataObject : public QObject { Q_OBJECT @@ -134,6 +148,9 @@ class homeform : public QObject { Q_PROPERTY(bool mapsVisible READ mapsVisible NOTIFY mapsVisibleChanged WRITE setMapsVisible) Q_PROPERTY(bool videoIconVisible READ videoIconVisible NOTIFY videoIconVisibleChanged WRITE setVideoIconVisible) Q_PROPERTY(bool videoVisible READ videoVisible NOTIFY videoVisibleChanged WRITE setVideoVisible) + Q_PROPERTY(bool chartIconVisible READ chartIconVisible NOTIFY chartIconVisibleChanged WRITE setChartIconVisible) + Q_PROPERTY( + bool chartFooterVisible READ chartFooterVisible NOTIFY chartFooterVisibleChanged WRITE setChartFooterVisible) Q_PROPERTY(QUrl videoPath READ videoPath NOTIFY videoPathChanged) Q_PROPERTY(int videoPosition READ videoPosition NOTIFY videoPositionChanged WRITE setVideoPosition) Q_PROPERTY(double videoRate READ videoRate NOTIFY videoRateChanged WRITE setVideoRate) @@ -153,6 +170,7 @@ class homeform : public QObject { Q_PROPERTY(bool autoResistance READ autoResistance NOTIFY autoResistanceChanged WRITE setAutoResistance) Q_PROPERTY(bool stopRequested READ stopRequested NOTIFY stopRequestedChanged WRITE setStopRequestedChanged) Q_PROPERTY(bool startRequested READ startRequested NOTIFY startRequestedChanged WRITE setStartRequestedChanged) + Q_PROPERTY(QString toastRequested READ toastRequested NOTIFY toastRequestedChanged WRITE setToastRequested) // workout preview Q_PROPERTY(int preview_workout_points READ preview_workout_points NOTIFY previewWorkoutPointsChanged) @@ -161,6 +179,7 @@ class homeform : public QObject { Q_PROPERTY(QString previewWorkoutTags READ previewWorkoutTags NOTIFY previewWorkoutTagsChanged) Q_PROPERTY(bool currentCoordinateValid READ currentCoordinateValid) + Q_PROPERTY(bool trainProgramLoadedWithVideo READ trainProgramLoadedWithVideo) Q_PROPERTY(QString getStravaAuthUrl READ getStravaAuthUrl NOTIFY stravaAuthUrlChanged) Q_PROPERTY(bool stravaWebVisible READ stravaWebVisible NOTIFY stravaWebVisibleChanged) @@ -168,6 +187,7 @@ class homeform : public QObject { public: static homeform *singleton() { return m_singleton; } + QByteArray currentPelotonImage(); Q_INVOKABLE void save_screenshot() { QString path = getWritableAppDir(); @@ -348,20 +368,22 @@ class homeform : public QObject { return QLatin1String(""); } } + QString workoutNameBasedOnBluetoothDevice() { + if (bluetoothManager->device() && bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { + return QStringLiteral("Ride"); + } else if (bluetoothManager->device() && bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { + return QStringLiteral("Row"); + } else { + return QStringLiteral("Run"); + } + } QString workoutName() { if (!stravaPelotonActivityName.isEmpty()) { return stravaPelotonActivityName; } else if (!stravaWorkoutName.isEmpty()) { return stravaWorkoutName; } else { - if (bluetoothManager->device() && bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) { - return QStringLiteral("Ride"); - } else if (bluetoothManager->device() && - bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { - return QStringLiteral("Row"); - } else { - return QStringLiteral("Run"); - } + return workoutNameBasedOnBluetoothDevice(); } } QString instructorName() { return stravaPelotonInstructorName; } @@ -369,12 +391,15 @@ class homeform : public QObject { int pzpLogin() { return m_pzpLoginState; } void setPelotonAskStart(bool value) { m_pelotonAskStart = value; } QString pelotonProvider() { return m_pelotonProvider; } + QString toastRequested() { return m_toastRequested; } void setPelotonProvider(const QString &value) { m_pelotonProvider = value; } bool generalPopupVisible(); bool licensePopupVisible(); bool mapsVisible(); bool videoIconVisible(); bool videoVisible() { return m_VideoVisible; } + bool chartIconVisible(); + bool chartFooterVisible() { return m_ChartFooterVisible; } int videoPosition(); double videoRate(); double currentSpeed() { @@ -406,14 +431,20 @@ class homeform : public QObject { } void setLicensePopupVisible(bool value); void setVideoIconVisible(bool value); + void setChartIconVisible(bool value); void setVideoVisible(bool value) { m_VideoVisible = value; emit videoVisibleChanged(m_VideoVisible); } + void setChartFooterVisible(bool value) { + m_ChartFooterVisible = value; + emit chartFooterVisibleChanged(m_ChartFooterVisible); + } void setVideoPosition(int position); // on startup void videoSeekPosition(int ms); // in realtime void setVideoRate(double rate); void setMapsVisible(bool value); + void setToastRequested(QString value) { m_toastRequested = value; } void setGeneralPopupVisible(bool value); int workout_sample_points() { return Session.count(); } int preview_workout_points(); @@ -518,12 +549,16 @@ class homeform : public QObject { return false; } + bool trainProgramLoadedWithVideo() { return (trainProgram && trainProgram->videoAvailable); } + QString getStravaAuthUrl() { return stravaAuthUrl; } bool stravaWebVisible() { return stravaAuthWebVisible; } trainprogram *trainingProgram() { return trainProgram; } private: static homeform *m_singleton; + TemplateInfoSenderBuilder *userTemplateManager = nullptr; + TemplateInfoSenderBuilder *innerTemplateManager = nullptr; QList dataList; QList Session; bluetooth *bluetoothManager; @@ -543,6 +578,8 @@ class homeform : public QObject { bool m_MapsVisible = false; bool m_VideoIconVisible = false; bool m_VideoVisible = false; + bool m_ChartFooterVisible = false; + bool m_ChartIconVisible = false; int m_VideoPosition = 0; double m_VideoRate = 1; QOAuth2AuthorizationCodeFlow *strava = nullptr; @@ -556,6 +593,7 @@ class homeform : public QObject { peloton *pelotonHandler = nullptr; bool m_pelotonAskStart = false; QString m_pelotonProvider = ""; + QString m_toastRequested = ""; int m_pelotonLoginState = -1; int m_pzpLoginState = -1; QString stravaPelotonActivityName; @@ -570,12 +608,14 @@ class homeform : public QObject { QString pelotonAbortedInstructor = QStringLiteral(""); QString lastFitFileSaved = QLatin1String(""); + QString lastTrainProgramFileSaved = QLatin1String(""); QList chartImagesFilenames; bool m_autoresistance = true; bool m_stopRequested = false; bool m_startRequested = false; + bool m_overridePower = false; DataObject *speed; DataObject *inclination; @@ -603,6 +643,7 @@ class homeform : public QObject { DataObject *target_power; DataObject *target_zone; DataObject *target_speed; + DataObject *target_pace; DataObject *target_incline; DataObject *ftp; DataObject *lapElapsed; @@ -679,6 +720,16 @@ class homeform : public QObject { bool floating_open = false; #endif +#ifndef Q_OS_IOS + QMdnsEngine::Browser *iphone_browser = nullptr; + QMdnsEngine::Resolver *iphone_resolver = nullptr; + QMdnsEngine::Server iphone_server; + QMdnsEngine::Cache iphone_cache; + QTcpSocket *iphone_socket = nullptr; + QMdnsEngine::Service iphone_service; + QHostAddress iphone_address; +#endif + public slots: void aboutToQuit(); void saveSettings(const QUrl &filename); @@ -702,6 +753,7 @@ class homeform : public QObject { void keyMediaPrevious(); void keyMediaNext(); void floatingOpen(); + void openFloatingWindowBrowser(); void deviceFound(const QString &name); void deviceConnected(QBluetoothDeviceInfo b); void ftmsAccessoryConnected(smartspin2k *d); @@ -739,6 +791,9 @@ class homeform : public QObject { void pelotonOffset_Plus(); void pelotonOffset_Minus(); int pelotonOffset() { return (trainProgram ? trainProgram->offsetElapsedTime() : 0); } + void bluetoothDeviceConnected(bluetoothdevice *b); + void bluetoothDeviceDisconnected(); + void onToastRequested(QString message); #if defined(Q_OS_WIN) || (defined(Q_OS_MAC) && !defined(Q_OS_IOS)) || (defined(Q_OS_ANDROID) && defined(LICENSE)) void licenseReply(QNetworkReply *reply); @@ -765,6 +820,7 @@ class homeform : public QObject { void changeLabelHelp(bool value); void changePelotonAskStart(bool value); void changePelotonProvider(QString value); + void toastRequestedChanged(QString value); void generalPopupVisibleChanged(bool value); void licensePopupVisibleChanged(bool value); void videoIconVisibleChanged(bool value); @@ -772,6 +828,8 @@ class homeform : public QObject { void videoPositionChanged(int value); void videoPathChanged(QUrl value); void videoRateChanged(double value); + void chartIconVisibleChanged(bool value); + void chartFooterVisibleChanged(bool value); void currentSpeedChanged(double value); void mapsVisibleChanged(bool value); void autoResistanceChanged(bool value); @@ -790,6 +848,8 @@ class homeform : public QObject { void stravaWebVisibleChanged(bool value); void workoutEventStateChanged(bluetoothdevice::WORKOUT_EVENT_STATE state); + + void heartRate(uint8_t heart); }; #endif // HOMEFORM_H diff --git a/src/horizongr7bike.cpp b/src/horizongr7bike.cpp index c5cd1d1a4..00806a947 100644 --- a/src/horizongr7bike.cpp +++ b/src/horizongr7bike.cpp @@ -1,6 +1,5 @@ #include "horizongr7bike.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -10,9 +9,9 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -36,6 +35,11 @@ void horizongr7bike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QEventLoop loop; QTimer timeout; + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + if (gattFTMSService) { if (wait_for_response) { connect(gattFTMSService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); @@ -45,7 +49,7 @@ void horizongr7bike::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); } else if (customService && customWriteChar.isValid()) { if (wait_for_response) { connect(customService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); @@ -55,14 +59,14 @@ void horizongr7bike::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - customService->writeCharacteristic(customWriteChar, QByteArray((const char *)data, data_len)); + customService->writeCharacteristic(customWriteChar, *writeBuffer); } else { qDebug() << "writeCharacteristic error!"; return; } if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -72,7 +76,7 @@ void horizongr7bike::writeCharacteristic(uint8_t *data, uint8_t data_len, const void horizongr7bike::forceResistance(resistance_t requestResistance) { // if the FTMS is connected, the ftmsCharacteristicChanged event will do all the stuff because it's a FTMS bike - if (virtualBike->connected()) + if (this->VirtualDevice()->connected()) return; uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -145,7 +149,8 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara QSettings settings; QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); - bool disable_hr_frommachinery = settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); static bool firstPacket = false; union flags { @@ -181,37 +186,27 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara Resistance = newValue.at(12); emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); - if (Heart.value() > 0) { - int avgP = ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() * - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()) - - (settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble() * - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble())) / - (settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble() - - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble()) + - (Heart.value() * ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() - - settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble()) / - (settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble() - - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()))); - if (avgP < 50) { - avgP = 50; - } - m_watt = avgP; - emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); - } + m_watt = wattFromHR(false); + emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * - 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in // kg * 3.5) / 200 ) / 60 if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { - Speed = Cadence.value() * settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio).toDouble(); + Speed = + Cadence.value() * + settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio) + .toDouble(); } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); @@ -234,7 +229,9 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara (uint16_t)((uint8_t)newValue.at(index)))) / 100.0; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } index += 2; emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); @@ -258,7 +255,10 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara Cadence = (((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)))) / 2.0) * - settings.value(QZSettings::horizon_gr7_cadence_multiplier, QZSettings::default_horizon_gr7_cadence_multiplier).toDouble(); + settings + .value(QZSettings::horizon_gr7_cadence_multiplier, + QZSettings::default_horizon_gr7_cadence_multiplier) + .toDouble(); } index += 2; emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); @@ -269,7 +269,10 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara avgCadence = (((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)))) / 2.0) * - settings.value(QZSettings::horizon_gr7_cadence_multiplier, QZSettings::default_horizon_gr7_cadence_multiplier).toDouble(); + settings + .value(QZSettings::horizon_gr7_cadence_multiplier, + QZSettings::default_horizon_gr7_cadence_multiplier) + .toDouble(); index += 2; emit debug(QStringLiteral("Current Average Cadence: ") + QString::number(avgCadence)); } @@ -381,22 +384,14 @@ void horizongr7bike::characteristicChanged(const QLowEnergyCharacteristic &chara if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && (!Flags.heartRate || Heart.value() == 0 || disable_hr_frommachinery)) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -509,7 +504,7 @@ void horizongr7bike::stateChanged(QLowEnergyService::ServiceState state) { btinit(); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -517,11 +512,14 @@ void horizongr7bike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -531,12 +529,13 @@ void horizongr7bike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&horizongr7bike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &horizongr7bike::changeInclination); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &horizongr7bike::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -549,7 +548,12 @@ void horizongr7bike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &c if (gattWriteCharControlPointId.isValid()) { qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' '); - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, b); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray(b); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); } } @@ -652,10 +656,6 @@ bool horizongr7bike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *horizongr7bike::VirtualBike() { return virtualBike; } - -void *horizongr7bike::VirtualDevice() { return VirtualBike(); } - uint16_t horizongr7bike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/horizongr7bike.h b/src/horizongr7bike.h index d4854270e..9ab88410f 100644 --- a/src/horizongr7bike.h +++ b/src/horizongr7bike.h @@ -38,21 +38,17 @@ class horizongr7bike : public bike { public: horizongr7bike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); void btinit(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(resistance_t requestResistance); QTimer *refresh; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; diff --git a/src/horizontreadmill.cpp b/src/horizontreadmill.cpp index d1313868a..c1b03f68d 100644 --- a/src/horizontreadmill.cpp +++ b/src/horizontreadmill.cpp @@ -1,7 +1,7 @@ #include "horizontreadmill.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -12,9 +12,9 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" #include using namespace std::chrono_literals; @@ -60,10 +60,15 @@ void horizontreadmill::writeCharacteristic(QLowEnergyService *service, QLowEnerg timeout.singleShot(3000, &loop, SLOT(quit())); } - service->writeCharacteristic(characteristic, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + service->writeCharacteristic(characteristic, *writeBuffer); if (!disable_log) - qDebug() << " >> " << QByteArray((const char *)data, data_len).toHex(' ') << " // " << info; + qDebug() << " >> " << writeBuffer->toHex(' ') << " // " << info; loop.exec(); } @@ -805,6 +810,8 @@ void horizontreadmill::btinit() { initDone = true; } +float horizontreadmill::float_one_point_round(float value) { return ((float)((int)(value * 10))) / 10; } + void horizontreadmill::update() { if (m_control->state() == QLowEnergyController::UnconnectedState) { @@ -828,7 +835,10 @@ void horizontreadmill::update() { settings.value(QZSettings::horizon_treadmill_7_8, QZSettings::default_horizon_treadmill_7_8).toBool(); bool horizon_paragon_x = settings.value(QZSettings::horizon_paragon_x, QZSettings::default_horizon_paragon_x).toBool(); - update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())); + bool power_sensor = !(settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))); + update_metrics(!power_sensor, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())); // updating the treadmill console every second if (sec1Update++ == (500 / refresh->interval())) { @@ -838,9 +848,11 @@ void horizontreadmill::update() { } if (requestSpeed != -1) { - bool minSpeed = fabs(requestSpeed - currentSpeed().value()) >= minStepSpeed(); + bool minSpeed = + fabs(requestSpeed - float_one_point_round(currentSpeed().value())) >= (minStepSpeed() - 0.09); bool forceSpeedNeed = checkIfForceSpeedNeeding(requestSpeed); - qDebug() << "requestSpeed=" << requestSpeed << minSpeed << forceSpeedNeed; + qDebug() << "requestSpeed=" << requestSpeed << minSpeed << forceSpeedNeed + << float_one_point_round(currentSpeed().value()); if (requestSpeed != currentSpeed().value() && minSpeed && requestSpeed >= 0 && requestSpeed <= 22 && forceSpeedNeed) { emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); @@ -852,9 +864,10 @@ void horizontreadmill::update() { qDebug() << "requestInclination=" << requestInclination; if (requestInclination < 0) requestInclination = 0; - else if (((int)requestInclination) != requestInclination) { // it has decimal + else { // the treadmill accepts only .5 steps - requestInclination = floor(requestInclination) + 0.5; + requestInclination = std::llround(requestInclination * 2) / 2.0; + qDebug() << "requestInclination after rounding=" << requestInclination; } if (requestInclination != currentInclination().value() && requestInclination >= 0 && requestInclination <= 15) { @@ -927,6 +940,13 @@ void horizontreadmill::update() { writeCharacteristic(gattCustomService, gattWriteCharCustomService, initData03_paragon, sizeof(initData03_paragon), QStringLiteral("starting"), false, true); } + } else if (gattFTMSService) { + uint8_t write[] = {FTMS_REQUEST_CONTROL}; + writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), + "requestControl", false, false); + write[0] = {FTMS_START_RESUME}; + writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), + "start simulation", false, false); } horizonPaused = false; lastStart = QDateTime::currentMSecsSinceEpoch(); @@ -976,6 +996,20 @@ void horizontreadmill::update() { writeCharacteristic(gattCustomService, gattWriteCharCustomService, write, sizeof(write), QStringLiteral("stopping"), false, true); } + } else if (gattFTMSService) { + if (requestPause == -1) { + uint8_t writeS[] = {FTMS_STOP_PAUSE, 0x01}; + + writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, writeS, sizeof(writeS), + QStringLiteral("stop"), false, true); + } else { + requestPause = -1; + Speed = 0; // forcing the speed to be sure, maybe I could remove this + uint8_t writeS[] = {FTMS_STOP_PAUSE, 0x02}; + + writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, writeS, sizeof(writeS), + QStringLiteral("stop"), false, true); + } } lastStop = QDateTime::currentMSecsSinceEpoch(); @@ -1138,8 +1172,96 @@ void horizontreadmill::forceIncline(double requestIncline) { false, false); uint8_t writeS[] = {FTMS_SET_TARGET_INCLINATION, 0x00, 0x00}; - writeS[1] = ((int16_t)(requestIncline * 10.0)) & 0xFF; - writeS[2] = ((int16_t)(requestIncline * 10.0)) >> 8; + if (kettler_treadmill) { + int16_t r = ((int16_t)(requestIncline * 10.0)); + + if (r < 0) + r = 0; + else if (r > 100) // max 10% inclination + r = 100; + + // send: 1/0 a 14 1e 28 32 3c 46 50 5a 64 6e 78 82 8c 96 + // recv: 0 5 a 14 19 1e 28 2d 32 3c 41 46 50 55 5a 64 + // recv: 0 5 10 20 25 30 40 45 50 60 65 70 80 85 90 100 + + /* + * Kinomap | TM + 01.0% | 00.5% + 02.0% | 01.0% + 03.0% | 02.0% + 04.0% | 02.5% + 05.0% | 03.0% + 06.0% | 04.0% + 07.0% | 04.5% + 08.0% | 05.0% + 09.0% | 06.0% + 10.0% | 06.5% + 11.0% | 07.0% + 12.0% | 08.0% + 13.0% | 08.5% + 14.0% | 09.0% + 15.0% | 10.0% + */ + + QHash conversion; + QHash conversion1; + conversion[0] = 0; + conversion1[0] = 0; + conversion[5] = 0x05; + conversion1[5] = 0; + conversion[10] = 0x14; + conversion1[10] = 0; + conversion[15] = 0x15; + conversion1[15] = 0; + conversion[20] = 0x1e; + conversion1[20] = 0; + conversion[25] = 0x25; + conversion1[25] = 0; + conversion[30] = 0x32; + conversion1[30] = 0; + conversion[35] = 0x35; + conversion1[35] = 0; + conversion[40] = 0x3c; + conversion1[40] = 0; + conversion[45] = 0x45; + conversion1[45] = 0; + conversion[50] = 0x50; + conversion1[50] = 0; + conversion[55] = 0x55; + conversion1[55] = 0; + conversion[60] = 0x5a; + conversion1[60] = 0; + conversion[65] = 0x65; + conversion1[65] = 0; + conversion[70] = 0x6e; + conversion1[70] = 0; + conversion[75] = 0x75; + conversion1[75] = 0; + conversion[80] = 0x78; + conversion1[80] = 0; + conversion[85] = 0x85; + conversion1[85] = 0; + conversion[90] = 0x8c; + conversion1[90] = 0; + conversion[95] = 0x95; + conversion1[95] = 0; + conversion[100] = 0x96; + conversion1[100] = 0x00; + conversion[105] = 0x05; + conversion1[105] = 0x01; + conversion[110] = 0x10; + conversion1[110] = 0x01; + conversion[115] = 0x15; + conversion1[115] = 0x01; + conversion[120] = 0x20; + conversion1[120] = 0x01; + + writeS[1] = conversion[r]; + writeS[2] = conversion1[r]; + } else { + writeS[1] = ((int16_t)(requestIncline * 10.0)) & 0xFF; + writeS[2] = ((int16_t)(requestIncline * 10.0)) >> 8; + } writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, writeS, sizeof(writeS), QStringLiteral("forceIncline"), false, false); @@ -1273,11 +1395,11 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); distanceEval = true; - } else if (characteristic.uuid() == QBluetoothUuid((quint16)0xFFF4) && (uint8_t)newValue.at(0) == 0x55 && - (uint8_t)newValue.at(1) == 0xAA && (uint8_t)newValue.at(2) == 0x00 && (uint8_t)newValue.at(3) == 0x00 && - (uint8_t)newValue.at(4) == 0x03 && (uint8_t)newValue.at(5) == 0x03 && (uint8_t)newValue.at(6) == 0x01 && - (uint8_t)newValue.at(7) == 0x00 && (uint8_t)newValue.at(8) == 0xf0 && (uint8_t)newValue.at(9) == 0xe1 && - (uint8_t)newValue.at(10) == 0x00) { + } else if (characteristic.uuid() == QBluetoothUuid((quint16)0xFFF4) && newValue.length() > 10 && + (uint8_t)newValue.at(0) == 0x55 && (uint8_t)newValue.at(1) == 0xAA && (uint8_t)newValue.at(2) == 0x00 && + (uint8_t)newValue.at(3) == 0x00 && (uint8_t)newValue.at(4) == 0x03 && (uint8_t)newValue.at(5) == 0x03 && + (uint8_t)newValue.at(6) == 0x01 && (uint8_t)newValue.at(7) == 0x00 && (uint8_t)newValue.at(8) == 0xf0 && + (uint8_t)newValue.at(9) == 0xe1 && (uint8_t)newValue.at(10) == 0x00) { Speed = 0; horizonPaused = true; @@ -1602,17 +1724,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { if (heart == 0.0 || settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { Heart = heart; @@ -1733,7 +1845,7 @@ void horizontreadmill::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -1750,15 +1862,17 @@ void horizontreadmill::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &horizontreadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &horizontreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &horizontreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } } firstStateChanged = 1; @@ -1805,11 +1919,24 @@ void horizontreadmill::serviceScanDone(void) { firstStateChanged = 0; auto services_list = m_control->services(); QBluetoothUuid ftmsService((quint16)0x1826); + QBluetoothUuid CustomService((quint16)0xFFF0); + for (const QBluetoothUuid &s : qAsConst(services_list)) { - gattCommunicationChannelService.append(m_control->createServiceObject(s)); - connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, - &horizontreadmill::stateChanged); - gattCommunicationChannelService.constLast()->discoverDetails(); +#ifdef Q_OS_WIN + if (s == ftmsService || s == CustomService) +#endif + { + qDebug() << s << "discovering..."; + gattCommunicationChannelService.append(m_control->createServiceObject(s)); + connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, + &horizontreadmill::stateChanged); + gattCommunicationChannelService.constLast()->discoverDetails(); + } +#ifdef Q_OS_WIN + else { + qDebug() << s << "NOT discovering!"; + } +#endif } } @@ -1841,6 +1968,9 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { if (device.name().toUpper().startsWith(QStringLiteral("MOBVOI TM"))) { mobvoi_treadmill = true; qDebug() << QStringLiteral("MOBVOI TM workaround ON!"); + } else if (device.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL"))) { + kettler_treadmill = true; + qDebug() << QStringLiteral("KETTLER TREADMILL workaround ON!"); } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); @@ -1884,10 +2014,6 @@ bool horizontreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *horizontreadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *horizontreadmill::VirtualDevice() { return VirtualTreadmill(); } - void horizontreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { @@ -2547,5 +2673,11 @@ void horizontreadmill::testProfileCRC() { assert(initData7_6[9] == (confirm >> 8)); } -double horizontreadmill::minStepInclination() { return 0.5; } +double horizontreadmill::minStepInclination() { + if (kettler_treadmill) + return 1.0; + else + return 0.5; +} + double horizontreadmill::minStepSpeed() { return 0.1; } diff --git a/src/horizontreadmill.h b/src/horizontreadmill.h index 5273edac6..1ec9664c3 100644 --- a/src/horizontreadmill.h +++ b/src/horizontreadmill.h @@ -29,8 +29,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -40,17 +38,14 @@ class horizontreadmill : public treadmill { Q_OBJECT public: horizontreadmill(bool noWriteResistance, bool noHeartService); - bool connected(); + bool connected() override; void forceSpeed(double requestSpeed); void forceIncline(double requestIncline); - double minStepInclination(); - double minStepSpeed(); + double minStepInclination() override; + double minStepSpeed() override; - void *VirtualTreadmill(); - void *VirtualDevice(); - - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; private: void writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic characteristic, uint8_t *data, @@ -60,8 +55,6 @@ class horizontreadmill : public treadmill { void btinit(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; @@ -94,11 +87,13 @@ class horizontreadmill : public treadmill { int32_t messageID = 0; bool mobvoi_treadmill = false; + bool kettler_treadmill = false; void testProfileCRC(); void updateProfileCRC(); int GenerateCRC_CCITT(uint8_t *PUPtr8, int PU16_Count, int crcStart = 65535); bool checkIfForceSpeedNeeding(double requestSpeed); + float float_one_point_round(float value); // profiles uint8_t initData7[20] = {0x55, 0xaa, 0x02, 0x00, 0x01, 0x16, 0xdb, 0x02, 0xed, 0xc2, diff --git a/src/iconceptbike.cpp b/src/iconceptbike.cpp index 098e6d913..a9e5f33d3 100644 --- a/src/iconceptbike.cpp +++ b/src/iconceptbike.cpp @@ -1,4 +1,6 @@ #include "iconceptbike.h" +#include "keepawakehelper.h" +#include "virtualbike.h" #include #include #include @@ -27,6 +29,7 @@ void iconceptbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { discoveryAgent = new QBluetoothServiceDiscoveryAgent(this); connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this, &iconceptbike::serviceDiscovered); + connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::finished, this, &iconceptbike::serviceFinished); // Start a discovery qDebug() << QStringLiteral("iconceptbike::deviceDiscovered"); @@ -35,6 +38,19 @@ void iconceptbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { } } +void iconceptbike::serviceFinished() { + qDebug() << QStringLiteral("iconceptbike::serviceFinished") << socket; + if (socket) { +#ifdef Q_OS_ANDROID + socket->setPreferredSecurityFlags(QBluetooth::NoSecurity); +#endif + + emit debug(QStringLiteral("Create socket")); + socket->connectToService(serialPortService); + emit debug(QStringLiteral("ConnectToService done")); + } +} + // In your local slot, read information about the found devices void iconceptbike::serviceDiscovered(const QBluetoothServiceInfo &service) { // this treadmill has more serial port, just the first one is the right one. @@ -48,10 +64,12 @@ void iconceptbike::serviceDiscovered(const QBluetoothServiceInfo &service) { emit debug(QStringLiteral("Found new service: ") + service.serviceName() + '(' + service.serviceUuid().toString() + ')'); - if (service.serviceName().startsWith(QStringLiteral("SerialPort")) || - service.serviceName().startsWith(QStringLiteral("Serial Port"))) { + if ((service.serviceName().startsWith(QStringLiteral("SerialPort")) || + service.serviceName().startsWith(QStringLiteral("Serial Port"))) && + // android 13 workaround + service.serviceUuid() == QBluetoothUuid(QStringLiteral("00001101-0000-1000-8000-00805f9b34fb"))) { emit debug(QStringLiteral("Serial port service found")); - discoveryAgent->stop(); + // discoveryAgent->stop(); // could lead to a crash? serialPortService = service; socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); @@ -61,14 +79,6 @@ void iconceptbike::serviceDiscovered(const QBluetoothServiceInfo &service) { connect(socket, &QBluetoothSocket::disconnected, this, &iconceptbike::disconnected); connect(socket, QOverload::of(&QBluetoothSocket::error), this, &iconceptbike::onSocketErrorOccurred); - -#ifdef Q_OS_ANDROID - socket->setPreferredSecurityFlags(QBluetooth::NoSecurity); -#endif - - emit debug(QStringLiteral("Create socket")); - socket->connectToService(serialPortService); - emit debug(QStringLiteral("ConnectToService done")); } } } @@ -78,13 +88,15 @@ void iconceptbike::update() { if (initDone) { // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualBike) { + if (!firstStateChanged && !hasVirtualDevice()) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, true); + auto virtualBike = new virtualbike(this, true); connect(virtualBike, &virtualbike::changeInclination, this, &iconceptbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -92,22 +104,26 @@ void iconceptbike::update() { // ******************************************************************************************************** if (requestResistance != -1) { - if (requestResistance > 32) { - requestResistance = 32; + if (requestResistance > 12) { + requestResistance = 12; } else if (requestResistance < 1) { requestResistance = 1; } + char resValues[] = {0x08, 0x0a, 0x0b, 0x0d, 0x0e, 0x10, 0x11, 0x13, 0x14, 0x16, 0x17, 0x18}; char res[] = {0x55, 0x11, 0x01, 0x12}; - res[3] = requestResistance; + res[3] = resValues[requestResistance - 1]; + qDebug() << QStringLiteral(">>") << QByteArray(res, sizeof(res)).toHex(' '); socket->write(res, sizeof(res)); + Resistance = requestResistance; requestResistance = -1; + } else { + const char poll[] = {0x55, 0x17, 0x01, 0x01}; + qDebug() << QStringLiteral(">>") << QByteArray(poll, sizeof(poll)).toHex(' '); + socket->write(poll, sizeof(poll)); + emit debug(QStringLiteral("write poll")); } - const char poll[] = {0x55, 0x17, 0x01, 0x01}; - socket->write(poll, sizeof(poll)); - emit debug(QStringLiteral("write poll")); - - update_metrics(true, watts()); + update_metrics(false, watts()); } } @@ -128,6 +144,7 @@ void iconceptbike::rfCommConnected() { const uint8_t init6[] = {0x55, 0x11, 0x01, 0x01}; const uint8_t init7[] = {0x55, 0x0a, 0x01, 0x01}; const uint8_t init8[] = {0x55, 0x07, 0x01, 0xff}; + const uint8_t init9[] = {0x55, 0x11, 0x01, 0x08}; socket->write((char *)init1, sizeof(init1)); qDebug() << QStringLiteral(" init1 write"); @@ -154,6 +171,9 @@ void iconceptbike::rfCommConnected() { QThread::msleep(600); socket->write((char *)init8, sizeof(init8)); qDebug() << QStringLiteral(" init8 write"); + QThread::msleep(600); + socket->write((char *)init9, sizeof(init9)); + qDebug() << QStringLiteral(" init9 write"); initDone = true; // requestStart = 1; @@ -169,24 +189,91 @@ void iconceptbike::readSocket() { qDebug() << QStringLiteral(" << ") + line.toHex(' '); if (line.length() == 16) { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool bh_spada_2_watt = + settings.value(QZSettings::bh_spada_2_watt, QZSettings::default_bh_spada_2_watt).toBool(); elapsed = GetElapsedTimeFromPacket(line); Distance = GetDistanceFromPacket(line); KCal = GetCaloriesFromPacket(line); - Speed = GetSpeedFromPacket(line); + if (bh_spada_2_watt) { + m_watt = GetWattFromPacket(line); + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = GetSpeedFromPacket(line) / 2.0; + } else { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + if (watts()) + KCal += + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / + ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg + //* 3.5) / 200 ) / 60 + } else { + Speed = GetSpeedFromPacket(line); + } Cadence = (uint8_t)line.at(13); // Heart = GetHeartRateFromPacket(line); + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + Heart = (uint8_t)KeepAwakeHelper::heart(); + } else +#endif + { + if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { + update_hr_from_external(); + } + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + + // these useless lines are needed to calculate the AVG resistance and AVG peloton resistance since + // echelon just send the resistance values when it changes + Resistance = Resistance.value(); + m_pelotonResistance = m_pelotonResistance.value(); + emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); emit debug(QStringLiteral("Current cadence: ") + QString::number(Cadence.value())); // emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value())); emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts()); } } } double iconceptbike::GetSpeedFromPacket(const QByteArray &packet) { - double convertedData = ((double)((double)((uint8_t)packet.at(9))) + ((double)packet.at(10))) / 100.0; + double convertedData = ((double)(((double)((uint8_t)packet.at(9))) * 256) + ((double)packet.at(10))) / 100.0; + return convertedData; +} + +double iconceptbike::GetWattFromPacket(const QByteArray &packet) { + double convertedData = ((double)(((double)((uint8_t)packet.at(14))) * 256) + ((double)packet.at(15))); return convertedData; } @@ -209,6 +296,9 @@ void iconceptbike::onSocketErrorOccurred(QBluetoothSocket::SocketError error) { emit debug(QStringLiteral("onSocketErrorOccurred ") + QString::number(error)); } -void *iconceptbike::VirtualBike() { return virtualBike; } - -void *iconceptbike::VirtualDevice() { return VirtualBike(); } +uint16_t iconceptbike::watts() { + if (currentCadence().value() == 0) { + return 0; + } + return m_watt.value(); +} diff --git a/src/iconceptbike.h b/src/iconceptbike.h index fe83873c2..a2ef8f25d 100644 --- a/src/iconceptbike.h +++ b/src/iconceptbike.h @@ -28,20 +28,18 @@ #include #include "bike.h" -#include "virtualbike.h" class iconceptbike : public bike { Q_OBJECT public: explicit iconceptbike(); - void *VirtualBike(); - void *VirtualDevice(); public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); private slots: void serviceDiscovered(const QBluetoothServiceInfo &service); + void serviceFinished(); void readSocket(); void rfCommConnected(); void onSocketErrorOccurred(QBluetoothSocket::SocketError); @@ -52,8 +50,6 @@ class iconceptbike : public bike { QBluetoothServiceInfo serialPortService; QBluetoothSocket *socket = nullptr; - virtualbike *virtualBike = nullptr; - QTimer *refresh; bool initDone = false; uint8_t firstStateChanged = 0; @@ -62,6 +58,15 @@ class iconceptbike : public bike { uint16_t GetDistanceFromPacket(const QByteArray &packet); uint16_t GetCaloriesFromPacket(const QByteArray &packet); double GetSpeedFromPacket(const QByteArray &packet); + double GetWattFromPacket(const QByteArray &packet); + + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + uint16_t watts() override; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif signals: void disconnected(); diff --git a/src/iconceptelliptical.cpp b/src/iconceptelliptical.cpp new file mode 100644 index 000000000..20db59beb --- /dev/null +++ b/src/iconceptelliptical.cpp @@ -0,0 +1,301 @@ +#include "iconceptelliptical.h" +#include "keepawakehelper.h" +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +iconceptelliptical::iconceptelliptical(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain) { + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->bikeResistanceGain = bikeResistanceGain; + this->bikeResistanceOffset = bikeResistanceOffset; + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + initDone = false; + connect(refresh, &QTimer::timeout, this, &iconceptelliptical::update); + refresh->start(1s); +} + +void iconceptelliptical::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + + device.address().toString() + ')'); + { + bluetoothDevice = device; + + // Create a discovery agent and connect to its signals + discoveryAgent = new QBluetoothServiceDiscoveryAgent(this); + connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this, + &iconceptelliptical::serviceDiscovered); + connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::finished, this, &iconceptelliptical::serviceFinished); + + // Start a discovery + qDebug() << QStringLiteral("iconceptelliptical::deviceDiscovered"); + discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery); + return; + } +} + +void iconceptelliptical::serviceFinished() { + qDebug() << QStringLiteral("iconceptelliptical::serviceFinished") << socket; + if (socket) { +#ifdef Q_OS_ANDROID + socket->setPreferredSecurityFlags(QBluetooth::NoSecurity); +#endif + + emit debug(QStringLiteral("Create socket")); + socket->connectToService(serialPortService); + emit debug(QStringLiteral("ConnectToService done")); + } +} + +// In your local slot, read information about the found devices +void iconceptelliptical::serviceDiscovered(const QBluetoothServiceInfo &service) { + // this treadmill has more serial port, just the first one is the right one. + if (socket != nullptr) { + qDebug() << QStringLiteral("iconceptelliptical::serviceDiscovered socket already initialized"); + return; + } + + qDebug() << QStringLiteral("iconceptelliptical::serviceDiscovered") << service; + /*if (service.device().address() == bluetoothDevice.address())*/ { + emit debug(QStringLiteral("Found new service: ") + service.serviceName() + '(' + + service.serviceUuid().toString() + ')'); + + if (service.serviceName().startsWith(QStringLiteral("SerialPort")) || + service.serviceName().startsWith(QStringLiteral("Serial Port")) || + service.serviceUuid() == QBluetoothUuid(QStringLiteral("00001101-0000-1000-8000-00805f9b34fb"))) { + + emit debug(QStringLiteral("Serial port service found")); + // discoveryAgent->stop(); // could lead to a crash? + + serialPortService = service; + socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); + + connect(socket, &QBluetoothSocket::readyRead, this, &iconceptelliptical::readSocket); + connect(socket, &QBluetoothSocket::connected, this, QOverload<>::of(&iconceptelliptical::rfCommConnected)); + connect(socket, &QBluetoothSocket::disconnected, this, &iconceptelliptical::disconnected); + connect(socket, QOverload::of(&QBluetoothSocket::error), this, + &iconceptelliptical::onSocketErrorOccurred); + } + } +} + +void iconceptelliptical::update() { + QSettings settings; + + if (initDone) { + // ******************************************* virtual bike init ************************************* + QSettings settings; + if (!firstStateChanged && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); + if (virtual_device_enabled) { + if (!virtual_device_force_bike) { + debug("creating virtual treadmill interface..."); + auto virtualTreadmill = new virtualtreadmill(this, true); + connect(virtualTreadmill, &virtualtreadmill::debug, this, &iconceptelliptical::debug); + connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, + &iconceptelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + debug("creating virtual bike interface..."); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); + connect(virtualBike, &virtualbike::changeInclination, this, + &iconceptelliptical::changeInclinationRequested); + connect(virtualBike, &virtualbike::changeInclination, this, &iconceptelliptical::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); + } + firstStateChanged = 1; + } + } + // ******************************************************************************************************** + + if (requestResistance != -1) { + if (requestResistance > 12) { + requestResistance = 12; + } else if (requestResistance < 1) { + requestResistance = 1; + } + char resValues[] = {0x08, 0x0a, 0x0b, 0x0d, 0x0e, 0x10, 0x11, 0x13, 0x14, 0x16, 0x17, 0x18}; + char res[] = {0x55, 0x11, 0x01, 0x12}; + res[3] = resValues[requestResistance - 1]; + qDebug() << QStringLiteral(">>") << QByteArray(res, sizeof(res)).toHex(' '); + socket->write(res, sizeof(res)); + Resistance = requestResistance; + requestResistance = -1; + } else { + const char poll[] = {0x55, 0x17, 0x01, 0x01}; + qDebug() << QStringLiteral(">>") << QByteArray(poll, sizeof(poll)).toHex(' '); + socket->write(poll, sizeof(poll)); + emit debug(QStringLiteral("write poll")); + } + + update_metrics(false, watts()); + } +} + +void iconceptelliptical::rfCommConnected() { + emit debug(QStringLiteral("connected ") + socket->peerName()); + + const uint8_t init1[] = { + 0x55, 0x0c, 0x01, 0xff, 0x55, 0xbb, 0x01, 0xff, 0x55, 0x24, 0x01, 0xff, 0x55, 0x25, 0x01, 0xff, 0x55, 0x26, + 0x01, 0xff, 0x55, 0x27, 0x01, 0xff, 0x55, 0x02, 0x01, 0xff, 0x55, 0x03, 0x01, 0xff, 0x55, 0x04, 0x01, 0xff, + 0x55, 0x06, 0x01, 0xff, 0x55, 0x1f, 0x01, 0xff, 0x55, 0xa0, 0x01, 0xff, 0x55, 0xb0, 0x01, 0xff, 0x55, 0xb2, + 0x01, 0xff, 0x55, 0xb3, 0x01, 0xff, 0x55, 0xb4, 0x01, 0xff, 0x55, 0xb5, 0x01, 0xff, 0x55, 0xb6, 0x01, 0xff, + 0x55, 0xb7, 0x01, 0xff, 0x55, 0xb8, 0x01, 0xff, 0x55, 0xb9, 0x01, 0xff, 0x55, 0xba, 0x01, 0xff, 0x55, 0x0b, + 0x01, 0xff, 0x55, 0x18, 0x01, 0xff, 0x55, 0x19, 0x01, 0xff, 0x55, 0x1a, 0x01, 0xff, 0x55, 0x1b, 0x01, 0xff}; + const uint8_t init2[] = {0x55, 0x0a, 0x01, 0x02, 0x55, 0x17, 0x01, 0x01}; + const uint8_t init3[] = {0x55, 0x01, 0x06, 0x34, 0x01, 0x63, 0x00, 0xb4, 0x00}; + const uint8_t init3b[] = {0x55, 0x17, 0x01, 0x01}; + const uint8_t init4[] = {0x55, 0x15, 0x01, 0x00}; + const uint8_t init5[] = {0x55, 0x11, 0x01, 0x01}; + const uint8_t init6[] = {0x55, 0x0a, 0x01, 0x01, 0x55, 0x0a, 0x01, 0x01}; + const uint8_t init6a[] = {0x55, 0x07, 0x01, 0xff}; + + socket->write((char *)init1, sizeof(init1)); + qDebug() << QStringLiteral(" init1 write"); + QThread::msleep(2000); + readSocket(); + QThread::msleep(1000); + socket->write((char *)init2, sizeof(init2)); + qDebug() << QStringLiteral(" init2 write"); + QThread::msleep(1500); + readSocket(); + socket->write((char *)init3, sizeof(init3)); + qDebug() << QStringLiteral(" init3 write"); + QThread::msleep(700); + readSocket(); + socket->write((char *)init3b, sizeof(init3b)); + qDebug() << QStringLiteral(" init3b write"); + QThread::msleep(700); + readSocket(); + socket->write((char *)init4, sizeof(init4)); + qDebug() << QStringLiteral(" init4 write"); + QThread::msleep(700); + readSocket(); + socket->write((char *)init5, sizeof(init5)); + qDebug() << QStringLiteral(" init5 write"); + QThread::msleep(600); + readSocket(); + socket->write((char *)init3b, sizeof(init3b)); + qDebug() << QStringLiteral(" init3b write"); + QThread::msleep(400); + readSocket(); + socket->write((char *)init6, sizeof(init6)); + qDebug() << QStringLiteral(" init6 write"); + QThread::msleep(600); + readSocket(); + QThread::msleep(500); + socket->write((char *)init6a, sizeof(init6a)); + qDebug() << QStringLiteral(" init6a write"); + QThread::msleep(1000); + readSocket(); + + initDone = true; + // requestStart = 1; + emit connectedAndDiscovered(); +} + +void iconceptelliptical::readSocket() { + if (!socket) + return; + + while (socket->bytesAvailable()) { + QByteArray line = socket->readAll(); + qDebug() << QStringLiteral(" << ") + line.toHex(' '); + + if (line.length() == 16) { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + elapsed = GetElapsedTimeFromPacket(line); + Distance = GetDistanceFromPacket(line); + KCal = GetCaloriesFromPacket(line); + Speed = GetSpeedFromPacket(line); + Cadence = (uint8_t)line.at(13); + // Heart = GetHeartRateFromPacket(line); + + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + Heart = (uint8_t)KeepAwakeHelper::heart(); + } else +#endif + { + if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { + update_hr_from_external(); + } + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + /*bool cadence = + settings.value(QZSettings::treadmill_cadence_sensor, QZSettings::default_treadmill_cadence_sensor) + .toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualtreadmill_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualtreadmill_setHeartRate((uint8_t)metrics_override_heartrate()); + }*/ +#endif +#endif + + emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); + emit debug(QStringLiteral("Current cadence: ") + QString::number(Cadence.value())); + // emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value())); + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts()); + } + } +} + +double iconceptelliptical::GetSpeedFromPacket(const QByteArray &packet) { + double convertedData = ((double)(((double)((uint8_t)packet.at(9))) * 256) + ((double)packet.at(10))) / 100.0; + return convertedData; +} + +uint16_t iconceptelliptical::GetCaloriesFromPacket(const QByteArray &packet) { + uint16_t convertedData = (packet.at(7) << 8) | packet.at(8); + return convertedData; +} + +uint16_t iconceptelliptical::GetDistanceFromPacket(const QByteArray &packet) { + uint16_t convertedData = (packet.at(5) << 8) | packet.at(6); + return convertedData; +} + +uint16_t iconceptelliptical::GetElapsedTimeFromPacket(const QByteArray &packet) { + uint16_t convertedData = (packet.at(3) << 8) | packet.at(4); + return convertedData; +} + +void iconceptelliptical::onSocketErrorOccurred(QBluetoothSocket::SocketError error) { + emit debug(QStringLiteral("onSocketErrorOccurred ") + QString::number(error)); +} + +uint16_t iconceptelliptical::watts() { + if (currentCadence().value() == 0) { + return 0; + } + return m_watt.value(); +} + +void iconceptelliptical::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} diff --git a/src/iconceptelliptical.h b/src/iconceptelliptical.h new file mode 100644 index 000000000..eacffd728 --- /dev/null +++ b/src/iconceptelliptical.h @@ -0,0 +1,84 @@ +#ifndef iconceptelliptical_H +#define iconceptelliptical_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include + +#include "elliptical.h" +#include "virtualbike.h" +#include "virtualtreadmill.h" + +class iconceptelliptical : public elliptical { + Q_OBJECT + public: + explicit iconceptelliptical(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + + private slots: + void serviceDiscovered(const QBluetoothServiceInfo &service); + void serviceFinished(); + void readSocket(); + void rfCommConnected(); + void onSocketErrorOccurred(QBluetoothSocket::SocketError); + void update(); + void changeInclinationRequested(double grade, double percentage); + + private: + bool noWriteResistance = false; + bool noHeartService = false; + uint8_t bikeResistanceOffset = 4; + double bikeResistanceGain = 1.0; + + QBluetoothServiceDiscoveryAgent *discoveryAgent; + QBluetoothServiceInfo serialPortService; + QBluetoothSocket *socket = nullptr; + + QTimer *refresh; + bool initDone = false; + uint8_t firstStateChanged = 0; + + uint16_t GetElapsedTimeFromPacket(const QByteArray &packet); + uint16_t GetDistanceFromPacket(const QByteArray &packet); + uint16_t GetCaloriesFromPacket(const QByteArray &packet); + double GetSpeedFromPacket(const QByteArray &packet); + + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + uint16_t watts(); + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); +}; + +#endif // iconceptelliptical_H diff --git a/src/icons.qrc b/src/icons.qrc index e834437ed..40520d0a6 100644 --- a/src/icons.qrc +++ b/src/icons.qrc @@ -32,5 +32,6 @@ icons/maps-icon-16.png icons/video.png icons/mini-display.png + icons/btn_strava_connectwith_orange.png diff --git a/src/icons/btn_strava_connectwith_orange.png b/src/icons/btn_strava_connectwith_orange.png new file mode 100644 index 000000000..f8132f134 Binary files /dev/null and b/src/icons/btn_strava_connectwith_orange.png differ diff --git a/src/inner_templates/chartjs/bike.png b/src/inner_templates/chartjs/bike.png new file mode 100644 index 000000000..ad6c0ceef Binary files /dev/null and b/src/inner_templates/chartjs/bike.png differ diff --git a/src/inner_templates/chartjs/chart.htm b/src/inner_templates/chartjs/chart.htm index 329368c42..746686bc5 100644 --- a/src/inner_templates/chartjs/chart.htm +++ b/src/inner_templates/chartjs/chart.htm @@ -14,6 +14,7 @@ + + + + +
+ +
+ + diff --git a/src/inner_templates/chartjs/dochart.js b/src/inner_templates/chartjs/dochart.js index 80e297614..beb922c88 100644 --- a/src/inner_templates/chartjs/dochart.js +++ b/src/inner_templates/chartjs/dochart.js @@ -1,36 +1,37 @@ window.chartColors = { red: 'rgb(255, 29, 0)', - redt: 'rgb(255, 29, 0, 0.25)', + redt: 'rgb(255, 29, 0, 0.55)', orange: 'rgb(255, 159, 64)', - oranget: 'rgb(255, 159, 64, 0.25)', + oranget: 'rgb(255, 159, 64, 0.55)', darkorange: 'rgb(255, 140, 0)', - darkoranget: 'rgb(255, 140, 0, 0.25)', + darkoranget: 'rgb(255, 140, 0, 0.55)', orangered: 'rgb(255, 69, 0)', - orangeredt: 'rgb(255, 69, 0, 0.25)', + orangeredt: 'rgb(255, 69, 0, 0.55)', yellow: 'rgb(255, 205, 86)', - yellowt: 'rgb(255, 205, 86, 0.25)', + yellowt: 'rgb(255, 205, 86, 0.55)', green: 'rgb(75, 192, 192)', - greent: 'rgb(75, 192, 192, 0.25)', + greent: 'rgb(75, 192, 192, 0.55)', blue: 'rgb(54, 162, 235)', purple: 'rgb(153, 102, 255)', grey: 'rgb(201, 203, 207)', - greyt: 'rgb(201, 203, 207, 0.25)', + greyt: 'rgb(201, 203, 207, 0.55)', white: 'rgb(255, 255, 255)', - whitet: 'rgb(255, 255, 255, 0.25)', + whitet: 'rgb(255, 255, 255, 0.55)', limegreen: 'rgb(50, 205, 50)', - limegreent: 'rgb(50, 205, 50, 0.25)', + limegreent: 'rgb(50, 205, 50, 0.55)', gold: 'rgb(255, 215, 0)', - goldt: 'rgb(255, 215, 0, 0.25)', + goldt: 'rgb(255, 215, 0, 0.55)', black: 'rgb(0, 0, 0)', - blackt: 'rgb(0, 0, 0, 0.25)', + blackt: 'rgb(0, 0, 0, 0.55)', lightsteelblue: 'rgb(176,192,222)', - lightsteelbluet: 'rgb(176,192,222, 0.25)', + lightsteelbluet: 'rgb(176,192,222, 0.55)', }; var ftp = 200; var ftpZones = []; var maxHeartRate = 190; var heartZones = []; +var miles = 1; function process_arr(arr) { let watts = []; @@ -54,6 +55,12 @@ function process_arr(arr) { let watts_max = 0; let heart_avg = 0; let heart_max = 0; + let jouls = 0; + let deviceType = 0; + let cadence_avg = 0; + let peloton_resistance_avg = 0; + let calories = 0; + let distance = 0; saveScreenshot[0] = false; saveScreenshot[1] = false; saveScreenshot[2] = false; @@ -61,6 +68,7 @@ function process_arr(arr) { saveScreenshot[4] = false; saveScreenshot[5] = false; saveScreenshot[6] = false; + saveScreenshot[7] = false; distributionPowerZones[0] = 0; distributionPowerZones[1] = 0; distributionPowerZones[2] = 0; @@ -89,6 +97,12 @@ function process_arr(arr) { watts_max = el.watts_max; heart_avg = el.heart_avg; heart_max = el.heart_max; + jouls = el.jouls; + deviceType = el.deviceType; + peloton_resistance_avg = el.peloton_resistance_avg; + cadence_avg = el.cadence_avg; + distance = el.distance; + calories = el.calories; maxEl = time; wattel.x = time; wattel.y = el.watts; @@ -144,11 +158,29 @@ function process_arr(arr) { $('.workoutName').text(workoutName); $('.workoutStartDate').text(workoutStartDate); $('.instructorName').text((instructorName)); + if(instructorName.length === 0) { + if(deviceType === 1) + $('.workout_image').attr("src","run.png"); + else if(deviceType === 3) + $('.workout_image').attr("src","row.png"); + else if(deviceType === 4) + $('.workout_image').attr("src","elliptical.png"); + else + $('.workout_image').attr("src","bike.png"); + } + $('.workout_image').attr("crossOrigin","anonymous"); $('.watts_avg').text('Watt AVG: ' + Math.floor(watts_avg)); $('.watts_max').text('Watt MAX: ' + watts_max); $('.heart_avg').text('Heart Rate AVG: ' + Math.floor(heart_avg)); $('.heart_max').text('Heart Rate MAX: ' + heart_max); + $('.summary_watts_avg').text(Math.floor(watts_avg) + ' W'); + $('.summary_jouls').text(Math.floor(jouls / 1000.0) + ' kJ'); + $('.summary_calories').text(Math.floor(calories) + ' kcal'); + $('.summary_distance').text(Math.floor(distance * miles) + (miles === 1 ? ' km' : ' mi')); + $('.summary_cadence_avg').text(Math.floor(cadence_avg) + ' rpm'); + $('.summary_resistance_avg').text(Math.floor(peloton_resistance_avg) + ' lvl'); + const backgroundFill = { id: 'custom_canvas_background_color', beforeDraw: (chart) => { @@ -173,7 +205,7 @@ function process_arr(arr) { data: watts, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, segment: { borderColor: ctx => ctx.p0.parsed.y < ftpZones[0] && ctx.p1.parsed.y < ftpZones[0] ? window.chartColors.grey : ctx.p0.parsed.y < ftpZones[1] && ctx.p1.parsed.y < ftpZones[1] ? window.chartColors.limegreen : @@ -191,13 +223,41 @@ function process_arr(arr) { data: reqpower, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, }, ] }, options: { animation: { onComplete: function() { + if(saveScreenshot[7] === false) { + var watt_badge = document.getElementById('watt_badge'); + + // Capture the containers using html2canvas + html2canvas(watt_badge).then(function(canvas1) { + + // Convert the merged canvas to a PNG image + var image = canvas1.toDataURL('image/png'); + + let el = new MainWSQueueElement({ + msg: 'savechart', + content: { + name: 'power_badge', + image: image + } + }, function(msg) { + if (msg.msg === 'R_savechart') { + return msg.content; + } + return null; + }, 15000, 3); + el.enqueue().catch(function(err) { + console.error('Error is ' + err); + }); + }); + } + saveScreenshot[7] = true; + if(saveScreenshot[0]) return; saveScreenshot[0] = true; @@ -226,6 +286,11 @@ function process_arr(arr) { plugins: { title:{ display:true, + backgroundColor: "#1d2330", + padding: { + top: 2, + bottom: 2 + }, text:'Watt' }, tooltips: { @@ -333,7 +398,7 @@ function process_arr(arr) { text: 'Watt' }, min: 0, - max: (watts_max > ftpZones[3] * 2 ? watts_max + 10 : ftpZones[3] * 2), + max: (watts_max > ftpZones[4] + 10 ? watts_max + 10 : ftpZones[4] + 10), ticks: { stepSize: 1, autoSkip: false, @@ -370,7 +435,7 @@ function process_arr(arr) { data: heart, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, segment: { borderColor: ctx => ctx.p0.parsed.y < heartZones[0] && ctx.p1.parsed.y < heartZones[0] ? window.chartColors.lightsteelblue : ctx.p0.parsed.y < heartZones[1] && ctx.p1.parsed.y < heartZones[1] ? window.chartColors.green : @@ -533,7 +598,7 @@ function process_arr(arr) { data: resistance, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, backgroundColor: window.chartColors.red, borderColor: window.chartColors.red, }, @@ -543,7 +608,7 @@ function process_arr(arr) { data: reqresistance, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, backgroundColor: window.chartColors.black, borderColor: window.chartColors.black, }, @@ -650,7 +715,7 @@ function process_arr(arr) { data: pelotonresistance, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, backgroundColor: window.chartColors.red, borderColor: window.chartColors.red, }, @@ -660,7 +725,7 @@ function process_arr(arr) { data: pelotonreqresistance, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, backgroundColor: window.chartColors.black, borderColor: window.chartColors.black, }, @@ -769,7 +834,7 @@ function process_arr(arr) { data: cadence, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, }, { backgroundColor: window.chartColors.black, @@ -779,7 +844,7 @@ function process_arr(arr) { data: reqcadence, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, }, ] }, @@ -966,7 +1031,7 @@ function process_arr(arr) { data: speed, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, }, { backgroundColor: window.chartColors.green, @@ -976,7 +1041,7 @@ function process_arr(arr) { data: inclination, fill: false, pointRadius: 0, - borderWidth: 1, + borderWidth: 2, }, ] }, @@ -1073,7 +1138,7 @@ function process_arr(arr) { function dochart_init() { onSettingsOK = true; - keys_arr = ['ftp', 'age', 'heart_rate_zone1', 'heart_rate_zone2', 'heart_rate_zone3', 'heart_rate_zone4', 'heart_max_override_enable', 'heart_max_override_value'] + keys_arr = ['ftp', 'miles_unit', 'age', 'heart_rate_zone1', 'heart_rate_zone2', 'heart_rate_zone3', 'heart_rate_zone4', 'heart_max_override_enable', 'heart_max_override_value'] let el = new MainWSQueueElement({ msg: 'getsettings', content: { @@ -1118,6 +1183,9 @@ function dochart_init() { } else if (key === 'heart_rate_zone4') { heart_rate_zone4 = msg.content[key]; heartZones[3] = Math.round(maxHeartRate * (msg.content[key] / 100)); + } else if (key === 'miles_unit') { + if(msg.content[key] === true || msg.content[key] === 'true') + miles = 0.621371; } } if(heart_max_override_enable) { @@ -1134,6 +1202,19 @@ function dochart_init() { el.enqueue().then(onSettingsOK).catch(function(err) { console.error('Error is ' + err); }) + + let getPelotonImage = new MainWSQueueElement({ + msg: 'getpelotonimage', + }, function(msg) { + if (msg.msg === 'R_getpelotonimage' && msg.content.length > 0) { + $('.workout_image').attr("src","data:image/png;base64," + msg.content); + } + return null; + }, 15000, 1); + getPelotonImage.enqueue().catch(function(err) { + console.error('Error is ' + err); + }); + el = new MainWSQueueElement({ msg: 'getsessionarray' }, function(msg) { @@ -1162,7 +1243,7 @@ $(window).on('load', function () { heartZones[0] = 110; heartZones[1] = 130; heartZones[2] = 150; - heartZones[3] = 170; + heartZones[3] = 170; arr = [{'watts': 50, 'req_power': 150, 'elapsed_s':0,'elapsed_m':0,'elapsed_h':0, 'heart':90, 'resistance': 10, 'req_resistance': 15, 'cadence': 80, 'req_cadence': 90, 'speed': 10, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, {'watts': 60, 'req_power': 150, 'elapsed_s':1,'elapsed_m':1,'elapsed_h':0, 'heart':92, 'resistance': 11, 'req_resistance': 30, 'cadence': 90, 'req_cadence': 100, 'speed': 8, 'inclination': 2, 'peloton_resistance': 20, 'peloton_req_resistance': 25}, @@ -1183,7 +1264,7 @@ $(window).on('load', function () { {'watts': 266, 'req_power': 170, 'elapsed_s':4,'elapsed_m':16,'elapsed_h':0, 'heart':120, 'resistance': 11, 'req_resistance': 35, 'cadence': 80, 'req_cadence': 60, 'speed': 10, 'inclination': 10, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, {'watts': 351, 'req_power': 170, 'elapsed_s':5,'elapsed_m':17,'elapsed_h':0, 'heart':112, 'resistance': 22, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 60, 'speed': 5, 'inclination': 9, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, {'watts': 322, 'req_power': 130, 'elapsed_s':6,'elapsed_m':18,'elapsed_h':0, 'heart':90, 'resistance': 25, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 96, 'speed': 10, 'inclination': 5, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, - {'watts': 257, 'req_power': 130, 'elapsed_s':7,'elapsed_m':19,'elapsed_h':0, 'heart':120, 'resistance': 10, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 97, 'speed': 10, 'inclination': 1, 'workoutName': '45min Power Zone Ride', 'workoutStartDate': '20/12/2021', 'instructorName': "Roberto Viola", 'watts_avg': 200, 'watts_max' : 250, 'heart_avg': 120, 'heart_max' : 150}, + {'watts': 257, 'req_power': 130, 'elapsed_s':7,'elapsed_m':19,'elapsed_h':0, 'heart':120, 'resistance': 10, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 97, 'speed': 10, 'inclination': 1, 'workoutName': '45min Power Zone Ride', 'workoutStartDate': '20/12/2021', 'instructorName': "Robin Arzon", 'watts_avg': 200, 'watts_max' : 351, 'heart_avg': 120, 'heart_max' : 150, 'jouls': 138000, 'calories': 950, 'distance': 11, 'cadence_avg': 65, 'peloton_resistance_avg': 22, 'deviceType': 1}, ] process_arr(arr); }); diff --git a/src/inner_templates/chartjs/dochartlive.js b/src/inner_templates/chartjs/dochartlive.js new file mode 100644 index 000000000..4260aa517 --- /dev/null +++ b/src/inner_templates/chartjs/dochartlive.js @@ -0,0 +1,530 @@ +window.chartColors = { + red: 'rgb(255, 29, 0)', + redt: 'rgb(255, 29, 0, 0.55)', + orange: 'rgb(255, 159, 64)', + oranget: 'rgb(255, 159, 64, 0.55)', + darkorange: 'rgb(255, 140, 0)', + darkoranget: 'rgb(255, 140, 0, 0.55)', + orangered: 'rgb(255, 69, 0)', + orangeredt: 'rgb(255, 69, 0, 0.55)', + yellow: 'rgb(255, 205, 86)', + yellowt: 'rgb(255, 205, 86, 0.55)', + green: 'rgb(75, 192, 192)', + greent: 'rgb(75, 192, 192, 0.55)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)', + greyt: 'rgb(201, 203, 207, 0.55)', + white: 'rgb(255, 255, 255)', + whitet: 'rgb(255, 255, 255, 0.55)', + limegreen: 'rgb(50, 205, 50)', + limegreent: 'rgb(50, 205, 50, 0.55)', + gold: 'rgb(255, 215, 0)', + goldt: 'rgb(255, 215, 0, 0.55)', + black: 'rgb(0, 0, 0)', + blackt: 'rgb(0, 0, 0, 0.55)', + lightsteelblue: 'rgb(176,192,222)', + lightsteelbluet: 'rgb(176,192,222, 0.55)', +}; + +var ftp = 200; +var ftpZones = []; +var maxHeartRate = 190; +var heartZones = []; +var miles = 1; +var powerChart = null; + +function process_trainprogram(arr) { + let powerWorkout = false; + let elapsed = 0; + + for (let el of arr.list) { + if(el.power != -1) { + powerWorkout = true; + for (i=0; i { + const ctx = chart.canvas.getContext('2d'); + ctx.save(); + ctx.globalCompositeOperation = 'destination-over'; + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, chart.width, chart.height); + ctx.restore(); + } + }; + + let config = { + type: 'line', + plugins: [backgroundFill], + data: { + datasets: [{ + label: 'Watts', + backgroundColor: window.chartColors.red, + borderColor: window.chartColors.red, + cubicInterpolationMode: 'monotone', + data: watts, + fill: false, + pointRadius: 0, + borderWidth: 2, + segment: { + borderColor: ctx => ctx.p0.parsed.y < ftpZones[0] && ctx.p1.parsed.y < ftpZones[0] ? window.chartColors.grey : + ctx.p0.parsed.y < ftpZones[1] && ctx.p1.parsed.y < ftpZones[1] ? window.chartColors.limegreen : + ctx.p0.parsed.y < ftpZones[2] && ctx.p1.parsed.y < ftpZones[2] ? window.chartColors.gold : + ctx.p0.parsed.y < ftpZones[3] && ctx.p1.parsed.y < ftpZones[3] ? window.chartColors.orange : + ctx.p0.parsed.y < ftpZones[4] && ctx.p1.parsed.y < ftpZones[4] ? window.chartColors.darkorange : + ctx.p0.parsed.y < ftpZones[5] && ctx.p1.parsed.y < ftpZones[5] ? window.chartColors.orangered : + window.chartColors.red, + } + }, { + label: 'Req. Watts', + backgroundColor: window.chartColors.black, + borderColor: window.chartColors.black, + //cubicInterpolationMode: 'monotone', + data: reqpower, + fill: false, + pointRadius: 0, + borderWidth: 2, + }, + ] + }, + options: { + responsive: true, + aspectRatio: div.width / div.height, + grid: { + zeroLineColor: 'rgba(0,255,0,1)' + }, + plugins: { + /* + title:{ + display:true, + backgroundColor: "#1d2330", + padding: { + top: 2, + bottom: 2 + }, + text:'Watt' + },*/ + tooltips: { + mode: 'index', + intersect: false, + }, + legend: { + display: false + }, + annotation: { + annotations: { + box1: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: 0, + yMax: ftpZones[0], + backgroundColor: "#d6d6d620" + }, + box2: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: ftpZones[0], + yMax: ftpZones[1], + backgroundColor: window.chartColors.limegreent, + }, + box3: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: ftpZones[1], + yMax: ftpZones[2], + backgroundColor: window.chartColors.goldt, + }, + box4: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: ftpZones[2], + yMax: ftpZones[3], + backgroundColor: window.chartColors.oranget, + }, + box5: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: ftpZones[3], + yMax: ftpZones[4], + backgroundColor: window.chartColors.darkoranget, + }, + box6: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: ftpZones[4], + yMax: ftpZones[5], + backgroundColor: window.chartColors.orangeredt, + }, + box7: { + // Indicates the type of annotation + type: 'box', + xMin: 0, + //xMax: maxEl, + yMin: ftpZones[5], + yMax: (watts_max > ftpZones[3] * 2 ? watts_max + 10 : ftpZones[3] * 2), + backgroundColor: window.chartColors.redt, + }, + } + } + }, + hover: { + mode: 'nearest', + intersect: true + }, + scales: { + x: { + type: 'linear', + display: true, + title: { + display: false, + text: 'Time' + }, + ticks: { + // Include a dollar sign in the ticks + callback: function(value, index, values) { + return value !== 0 ? Math.floor(value / 3600).toString().padStart(2, "0") + ":" + Math.floor((value / 60) - (Math.floor(value / 3600) * 60)).toString().padStart(2, "0") : ""; + }, + padding: -20, + //stepSize: 300, + align: "end", + }, + //max: maxEl, + }, + y: { + display: true, + title: { + display: false, + text: 'Watt' + }, + min: 0, + max: (watts_max > ftpZones[4] + 10 ? watts_max + 10 : ftpZones[4] + 10), + ticks: { + stepSize: 1, + autoSkip: false, + callback: value => [ftpZones[0] * 0.8, ftpZones[0], ftpZones[1], ftpZones[2], ftpZones[3], ftpZones[4], ftpZones[5]].includes(value) ? + value === ftpZones[0] * 0.8 ? 'zone 1' : + value === ftpZones[0] ? 'zone 2' : + value === ftpZones[1] ? 'zone 3' : + value === ftpZones[2] ? 'zone 4' : + value === ftpZones[3] ? 'zone 5' : + value === ftpZones[4] ? 'zone 6' : + value === ftpZones[5] ? 'zone 7' : undefined : undefined, + color: 'black', + padding: -50, + align: 'end', + z: 1, + } + } + } + } + }; + powerChart = new Chart(ctx, config); + + refresh(); +} + +function refresh() { + el = new MainWSQueueElement({ + msg: null + }, function(msg) { + if (msg.msg === 'workout') { + return msg.content; + } + return null; + }, 2000, 1); + el.enqueue().then(process_workout).catch(function(err) { + console.error('Error is ' + err); + refresh(); + }); +} + +function process_workout(arr) { + powerChart.data.datasets[0].data.push({x: arr.elapsed_s + (arr.elapsed_m * 60) + (arr.elapsed_h * 3600), y: arr.watts}); + powerChart.update(); + refresh(); +} + +function dochart_init() { + onSettingsOK = true; + keys_arr = ['ftp', 'miles_unit', 'age', 'heart_rate_zone1', 'heart_rate_zone2', 'heart_rate_zone3', 'heart_rate_zone4', 'heart_max_override_enable', 'heart_max_override_value'] + let el = new MainWSQueueElement({ + msg: 'getsettings', + content: { + keys: keys_arr + } + }, function(msg) { + if (msg.msg === 'R_getsettings') { + var heart_max_override_enable = false; + var heart_max_override_value = 195; + var heart_rate_zone1 = 0; + var heart_rate_zone2 = 0; + var heart_rate_zone3 = 0; + var heart_rate_zone4 = 0; + + for (let key of keys_arr) { + if (msg.content[key] === undefined) + return null; + if (key === 'ftp') { + ftp = msg.content[key]; + ftpZones[0] = Math.round(ftp * 0.55); + ftpZones[1] = Math.round(ftp * 0.75); + ftpZones[2] = Math.round(ftp * 0.90); + ftpZones[3] = Math.round(ftp * 1.05); + ftpZones[4] = Math.round(ftp * 1.20); + ftpZones[5] = Math.round(ftp * 1.50); + } else if (key === 'age') { + age = msg.content[key]; + maxHeartRate = 220 - age; + } else if (key === 'heart_max_override_enable') { + heart_max_override_enable = msg.content[key]; + } else if (key === 'heart_max_override_value') { + heart_max_override_value = msg.content[key]; + } else if (key === 'heart_rate_zone1') { + heart_rate_zone1 = msg.content[key]; + heartZones[0] = Math.round(maxHeartRate * (msg.content[key] / 100)); + } else if (key === 'heart_rate_zone2') { + heart_rate_zone2 = msg.content[key]; + heartZones[1] = Math.round(maxHeartRate * (msg.content[key] / 100)); + } else if (key === 'heart_rate_zone3') { + heart_rate_zone3 = msg.content[key]; + heartZones[2] = Math.round(maxHeartRate * (msg.content[key] / 100)); + } else if (key === 'heart_rate_zone4') { + heart_rate_zone4 = msg.content[key]; + heartZones[3] = Math.round(maxHeartRate * (msg.content[key] / 100)); + } else if (key === 'miles_unit') { + if(msg.content[key] === true || msg.content[key] === 'true') + miles = 0.621371; + } + } + if(heart_max_override_enable) { + maxHeartRate = heart_max_override_value; + heartZones[0] = Math.round(maxHeartRate * (heart_rate_zone1 / 100)); + heartZones[1] = Math.round(maxHeartRate * (heart_rate_zone2 / 100)); + heartZones[2] = Math.round(maxHeartRate * (heart_rate_zone3 / 100)); + heartZones[3] = Math.round(maxHeartRate * (heart_rate_zone4 / 100)); + } + return msg.content; + } + return null; + }, 5000, 3); + el.enqueue().then(onSettingsOK).catch(function(err) { + console.error('Error is ' + err); + }) + + el = new MainWSQueueElement({ + msg: 'getsessionarray' + }, function(msg) { + if (msg.msg === 'R_getsessionarray') { + return msg.content; + } + return null; + }, 15000, 3); + el.enqueue().then(process_arr).catch(function(err) { + console.error('Error is ' + err); + }); + + el = new MainWSQueueElement({ + msg: 'gettrainingprogram' + }, function(msg) { + if (msg.msg === 'R_gettrainingprogram') { + return msg.content; + } + return null; + }, 15000, 3); + el.enqueue().then(process_trainprogram).catch(function(err) { + console.error('Error is ' + err); + }); +} + + +$(window).on('load', function () { + dochart_init(); return; + + // DEBUG + ftpZones[0] = Math.round(ftp * 0.55); + ftpZones[1] = Math.round(ftp * 0.75); + ftpZones[2] = Math.round(ftp * 0.90); + ftpZones[3] = Math.round(ftp * 1.05); + ftpZones[4] = Math.round(ftp * 1.20); + ftpZones[5] = Math.round(ftp * 1.50); + + heartZones[0] = 110; + heartZones[1] = 130; + heartZones[2] = 150; + heartZones[3] = 170; + + arr = [{'watts': 50, 'req_power': 150, 'elapsed_s':0,'elapsed_m':0,'elapsed_h':0, 'heart':90, 'resistance': 10, 'req_resistance': 15, 'cadence': 80, 'req_cadence': 90, 'speed': 10, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 60, 'req_power': 150, 'elapsed_s':1,'elapsed_m':1,'elapsed_h':0, 'heart':92, 'resistance': 11, 'req_resistance': 30, 'cadence': 90, 'req_cadence': 100, 'speed': 8, 'inclination': 2, 'peloton_resistance': 20, 'peloton_req_resistance': 25}, + {'watts': 70, 'req_power': 170, 'elapsed_s':2,'elapsed_m':2,'elapsed_h':0, 'heart':110, 'resistance': 12, 'req_resistance': 40, 'cadence': 100, 'req_cadence': 90, 'speed': 9, 'inclination': 2.5, 'peloton_resistance': 30, 'peloton_req_resistance': 35}, + {'watts': 140, 'req_power': 170, 'elapsed_s':3,'elapsed_m':3,'elapsed_h':0, 'heart':115, 'resistance': 16, 'req_resistance': 41, 'cadence': 90, 'req_cadence': 95, 'speed': 11, 'inclination': 1, 'peloton_resistance': 40, 'peloton_req_resistance': 45}, + {'watts': 130, 'req_power': 170, 'elapsed_s':4,'elapsed_m':4,'elapsed_h':0, 'heart':130, 'resistance': 18, 'req_resistance': 43, 'cadence': 95, 'req_cadence': 95, 'speed': 10, 'inclination': 4, 'peloton_resistance': 50, 'peloton_req_resistance': 55}, + {'watts': 160, 'req_power': 170, 'elapsed_s':5,'elapsed_m':5,'elapsed_h':0, 'heart':135, 'resistance': 22, 'req_resistance': 43, 'cadence': 95, 'req_cadence': 95, 'speed': 12, 'inclination': 1, 'peloton_resistance': 60, 'peloton_req_resistance': 15}, + {'watts': 180, 'req_power': 130, 'elapsed_s':6,'elapsed_m':6,'elapsed_h':0, 'heart':140, 'resistance': 31, 'req_resistance': 43, 'cadence': 95, 'req_cadence': 90, 'speed': 10, 'inclination': 3, 'peloton_resistance': 70, 'peloton_req_resistance': 15}, + {'watts': 120, 'req_power': 130, 'elapsed_s':7,'elapsed_m':7,'elapsed_h':0, 'heart':150, 'resistance': 18, 'req_resistance': 35, 'cadence': 95, 'req_cadence': 80, 'speed': 10, 'inclination': 4, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 190, 'req_power': 150, 'elapsed_s':1,'elapsed_m':8,'elapsed_h':0, 'heart':155, 'resistance': 17, 'req_resistance': 35, 'cadence': 95, 'req_cadence': 80, 'speed': 13, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 195, 'req_power': 170, 'elapsed_s':2,'elapsed_m':9,'elapsed_h':0, 'heart':165, 'resistance': 19, 'req_resistance': 30, 'cadence': 80, 'req_cadence': 80, 'speed': 12, 'inclination': 3, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 200, 'req_power': 170, 'elapsed_s':3,'elapsed_m':10,'elapsed_h':0, 'heart':153, 'resistance': 20, 'req_resistance': 25, 'cadence': 90, 'req_cadence': 90, 'speed': 10, 'inclination': 2, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 206, 'req_power': 170, 'elapsed_s':4,'elapsed_m':11,'elapsed_h':0, 'heart':152, 'resistance': 21, 'req_resistance': 35, 'cadence': 90, 'req_cadence': 90, 'speed': 12, 'inclination': 7, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 211, 'req_power': 170, 'elapsed_s':5,'elapsed_m':12,'elapsed_h':0, 'heart':180, 'resistance': 25, 'req_resistance': 35, 'cadence': 90, 'req_cadence': 70, 'speed': 10, 'inclination': 10, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 222, 'req_power': 130, 'elapsed_s':6,'elapsed_m':13,'elapsed_h':0, 'heart':182, 'resistance': 31, 'req_resistance': 35, 'cadence': 80, 'req_cadence': 70, 'speed': 7, 'inclination': 12, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 237, 'req_power': 130, 'elapsed_s':7,'elapsed_m':14,'elapsed_h':0, 'heart':160, 'resistance': 20, 'req_resistance': 50, 'cadence': 90, 'req_cadence': 70, 'speed': 6, 'inclination': 1, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 250, 'req_power': 170, 'elapsed_s':3,'elapsed_m':15,'elapsed_h':0, 'heart':115, 'resistance': 20, 'req_resistance': 50, 'cadence': 90, 'req_cadence': 90, 'speed': 10, 'inclination': 14, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 266, 'req_power': 170, 'elapsed_s':4,'elapsed_m':16,'elapsed_h':0, 'heart':120, 'resistance': 11, 'req_resistance': 35, 'cadence': 80, 'req_cadence': 60, 'speed': 10, 'inclination': 10, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 351, 'req_power': 170, 'elapsed_s':5,'elapsed_m':17,'elapsed_h':0, 'heart':112, 'resistance': 22, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 60, 'speed': 5, 'inclination': 9, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 322, 'req_power': 130, 'elapsed_s':6,'elapsed_m':18,'elapsed_h':0, 'heart':90, 'resistance': 25, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 96, 'speed': 10, 'inclination': 5, 'peloton_resistance': 10, 'peloton_req_resistance': 15}, + {'watts': 257, 'req_power': 130, 'elapsed_s':7,'elapsed_m':19,'elapsed_h':0, 'heart':120, 'resistance': 10, 'req_resistance': 23, 'cadence': 80, 'req_cadence': 97, 'speed': 10, 'inclination': 1, 'workoutName': '45min Power Zone Ride', 'workoutStartDate': '20/12/2021', 'instructorName': "Robin Arzon", 'watts_avg': 200, 'watts_max' : 351, 'heart_avg': 120, 'heart_max' : 150, 'jouls': 138000, 'calories': 950, 'distance': 11, 'cadence_avg': 65, 'peloton_resistance_avg': 22, 'deviceType': 1}, + ] + process_arr(arr); +}); + +$(document).ready(function () { + $('#loading').hide(); +}); diff --git a/src/inner_templates/chartjs/elliptical.png b/src/inner_templates/chartjs/elliptical.png new file mode 100644 index 000000000..d27ba3761 Binary files /dev/null and b/src/inner_templates/chartjs/elliptical.png differ diff --git a/src/inner_templates/chartjs/html2canvas.min.js b/src/inner_templates/chartjs/html2canvas.min.js new file mode 100644 index 000000000..0f585de61 --- /dev/null +++ b/src/inner_templates/chartjs/html2canvas.min.js @@ -0,0 +1,20 @@ +/*! + * html2canvas 1.3.2 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ +!function(A,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(A="undefined"!=typeof globalThis?globalThis:A||self).html2canvas=e()}(this,function(){"use strict"; +/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */var B=function(A,e){return(B=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(A,e){A.__proto__=e}||function(A,e){for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(A[t]=e[t])})(A,e)};function A(A,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function t(){this.constructor=A}B(A,e),A.prototype=null===e?Object.create(e):(t.prototype=e.prototype,new t)}var h=function(){return(h=Object.assign||function(A){for(var e,t=1,B=arguments.length;ts[0]&&e[1]>10),s%1024+56320)),(r+1===t||16384>5],this.data[e=(e<<2)+(31&A)];if(A<=65535)return e=this.index[2048+(A-55296>>5)],this.data[e=(e<<2)+(31&A)];if(A>11)],e=this.index[e+=A>>5&63],this.data[e=(e<<2)+(31&A)];if(A<=1114111)return this.data[this.highValueIndex]}return this.errorValue},i);function i(A,e,t,B,r,n){this.initialValue=A,this.errorValue=e,this.highStart=t,this.highValueIndex=B,this.index=r,this.data=n}function w(A,e,t,B){var r=B[t];if(Array.isArray(A)?-1!==A.indexOf(r):A===r)for(var n=t;n<=B.length;){if((o=B[++n])===e)return 1;if(o!==p)break}if(r===p)for(n=t;0>4,i[o++]=(15&t)<<4|B>>2,i[o++]=(3&B)<<6|63&r;return n}(l="KwAAAAAAAAAACA4AUD0AADAgAAACAAAAAAAIABAAGABAAEgAUABYAGAAaABgAGgAYgBqAF8AZwBgAGgAcQB5AHUAfQCFAI0AlQCdAKIAqgCyALoAYABoAGAAaABgAGgAwgDKAGAAaADGAM4A0wDbAOEA6QDxAPkAAQEJAQ8BFwF1AH0AHAEkASwBNAE6AUIBQQFJAVEBWQFhAWgBcAF4ATAAgAGGAY4BlQGXAZ8BpwGvAbUBvQHFAc0B0wHbAeMB6wHxAfkBAQIJAvEBEQIZAiECKQIxAjgCQAJGAk4CVgJeAmQCbAJ0AnwCgQKJApECmQKgAqgCsAK4ArwCxAIwAMwC0wLbAjAA4wLrAvMC+AIAAwcDDwMwABcDHQMlAy0DNQN1AD0DQQNJA0kDSQNRA1EDVwNZA1kDdQB1AGEDdQBpA20DdQN1AHsDdQCBA4kDkQN1AHUAmQOhA3UAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AKYDrgN1AHUAtgO+A8YDzgPWAxcD3gPjA+sD8wN1AHUA+wMDBAkEdQANBBUEHQQlBCoEFwMyBDgEYABABBcDSARQBFgEYARoBDAAcAQzAXgEgASIBJAEdQCXBHUAnwSnBK4EtgS6BMIEyAR1AHUAdQB1AHUAdQCVANAEYABgAGAAYABgAGAAYABgANgEYADcBOQEYADsBPQE/AQEBQwFFAUcBSQFLAU0BWQEPAVEBUsFUwVbBWAAYgVgAGoFcgV6BYIFigWRBWAAmQWfBaYFYABgAGAAYABgAKoFYACxBbAFuQW6BcEFwQXHBcEFwQXPBdMF2wXjBeoF8gX6BQIGCgYSBhoGIgYqBjIGOgZgAD4GRgZMBmAAUwZaBmAAYABgAGAAYABgAGAAYABgAGAAYABgAGIGYABpBnAGYABgAGAAYABgAGAAYABgAGAAYAB4Bn8GhQZgAGAAYAB1AHcDFQSLBmAAYABgAJMGdQA9A3UAmwajBqsGqwaVALMGuwbDBjAAywbSBtIG1QbSBtIG0gbSBtIG0gbdBuMG6wbzBvsGAwcLBxMHAwcbByMHJwcsBywHMQcsB9IGOAdAB0gHTgfSBkgHVgfSBtIG0gbSBtIG0gbSBtIG0gbSBiwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdgAGAALAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdbB2MHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB2kH0gZwB64EdQB1AHUAdQB1AHUAdQB1AHUHfQdgAIUHjQd1AHUAlQedB2AAYAClB6sHYACzB7YHvgfGB3UAzgfWBzMB3gfmB1EB7gf1B/0HlQENAQUIDQh1ABUIHQglCBcDLQg1CD0IRQhNCEEDUwh1AHUAdQBbCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIcAh3CHoIMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIgggwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAALAcsBywHLAcsBywHLAcsBywHLAcsB4oILAcsB44I0gaWCJ4Ipgh1AHUAqgiyCHUAdQB1AHUAdQB1AHUAdQB1AHUAtwh8AXUAvwh1AMUIyQjRCNkI4AjoCHUAdQB1AO4I9gj+CAYJDgkTCS0HGwkjCYIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiAAIAAAAFAAYABgAGIAXwBgAHEAdQBFAJUAogCyAKAAYABgAEIA4ABGANMA4QDxAMEBDwE1AFwBLAE6AQEBUQF4QkhCmEKoQrhCgAHIQsAB0MLAAcABwAHAAeDC6ABoAHDCwMMAAcABwAHAAdDDGMMAAcAB6MM4wwjDWMNow3jDaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAEjDqABWw6bDqABpg6gAaABoAHcDvwOPA+gAaABfA/8DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DpcPAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcAB9cPKwkyCToJMAB1AHUAdQBCCUoJTQl1AFUJXAljCWcJawkwADAAMAAwAHMJdQB2CX4JdQCECYoJjgmWCXUAngkwAGAAYABxAHUApgn3A64JtAl1ALkJdQDACTAAMAAwADAAdQB1AHUAdQB1AHUAdQB1AHUAowYNBMUIMAAwADAAMADICcsJ0wnZCRUE4QkwAOkJ8An4CTAAMAB1AAAKvwh1AAgKDwoXCh8KdQAwACcKLgp1ADYKqAmICT4KRgowADAAdQB1AE4KMAB1AFYKdQBeCnUAZQowADAAMAAwADAAMAAwADAAMAAVBHUAbQowADAAdQC5CXUKMAAwAHwBxAijBogEMgF9CoQKiASMCpQKmgqIBKIKqgquCogEDQG2Cr4KxgrLCjAAMADTCtsKCgHjCusK8Qr5CgELMAAwADAAMAB1AIsECQsRC3UANAEZCzAAMAAwADAAMAB1ACELKQswAHUANAExCzkLdQBBC0kLMABRC1kLMAAwADAAMAAwADAAdQBhCzAAMAAwAGAAYABpC3ELdwt/CzAAMACHC4sLkwubC58Lpwt1AK4Ltgt1APsDMAAwADAAMAAwADAAMAAwAL4LwwvLC9IL1wvdCzAAMADlC+kL8Qv5C/8LSQswADAAMAAwADAAMAAwADAAMAAHDDAAMAAwADAAMAAODBYMHgx1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1ACYMMAAwADAAdQB1AHUALgx1AHUAdQB1AHUAdQA2DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AD4MdQBGDHUAdQB1AHUAdQB1AEkMdQB1AHUAdQB1AFAMMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQBYDHUAdQB1AF8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUA+wMVBGcMMAAwAHwBbwx1AHcMfwyHDI8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAYABgAJcMMAAwADAAdQB1AJ8MlQClDDAAMACtDCwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB7UMLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AA0EMAC9DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAsBywHLAcsBywHLAcsBywHLQcwAMEMyAwsBywHLAcsBywHLAcsBywHLAcsBywHzAwwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1ANQM2QzhDDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMABgAGAAYABgAGAAYABgAOkMYADxDGAA+AwADQYNYABhCWAAYAAODTAAMAAwADAAFg1gAGAAHg37AzAAMAAwADAAYABgACYNYAAsDTQNPA1gAEMNPg1LDWAAYABgAGAAYABgAGAAYABgAGAAUg1aDYsGVglhDV0NcQBnDW0NdQ15DWAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAlQCBDZUAiA2PDZcNMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAnw2nDTAAMAAwADAAMAAwAHUArw23DTAAMAAwADAAMAAwADAAMAAwADAAMAB1AL8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQDHDTAAYABgAM8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA1w11ANwNMAAwAD0B5A0wADAAMAAwADAAMADsDfQN/A0EDgwOFA4wABsOMAAwADAAMAAwADAAMAAwANIG0gbSBtIG0gbSBtIG0gYjDigOwQUuDsEFMw7SBjoO0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGQg5KDlIOVg7SBtIGXg5lDm0OdQ7SBtIGfQ6EDooOjQ6UDtIGmg6hDtIG0gaoDqwO0ga0DrwO0gZgAGAAYADEDmAAYAAkBtIGzA5gANIOYADaDokO0gbSBt8O5w7SBu8O0gb1DvwO0gZgAGAAxA7SBtIG0gbSBtIGYABgAGAAYAAED2AAsAUMD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHJA8sBywHLAcsBywHLAccDywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywPLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAc0D9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHPA/SBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gYUD0QPlQCVAJUAMAAwADAAMACVAJUAlQCVAJUAlQCVAEwPMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA//8EAAQABAAEAAQABAAEAAQABAANAAMAAQABAAIABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQACgATABcAHgAbABoAHgAXABYAEgAeABsAGAAPABgAHABLAEsASwBLAEsASwBLAEsASwBLABgAGAAeAB4AHgATAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABYAGwASAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWAA0AEQAeAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAFAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJABYAGgAbABsAGwAeAB0AHQAeAE8AFwAeAA0AHgAeABoAGwBPAE8ADgBQAB0AHQAdAE8ATwAXAE8ATwBPABYAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAFAATwBAAE8ATwBPAEAATwBQAFAATwBQAB4AHgAeAB4AHgAeAB0AHQAdAB0AHgAdAB4ADgBQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgBQAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAkACQAJAAkACQAJAAkABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAFAAHgAeAB4AKwArAFAAUABQAFAAGABQACsAKwArACsAHgAeAFAAHgBQAFAAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUAAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAYAA0AKwArAB4AHgAbACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAB4ABAAEAB4ABAAEABMABAArACsAKwArACsAKwArACsAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAKwArACsAKwBWAFYAVgBWAB4AHgArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AGgAaABoAGAAYAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQAEwAEACsAEwATAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABLAEsASwBLAEsASwBLAEsASwBLABoAGQAZAB4AUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABMAUAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABABQAFAABAAEAB4ABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUAAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAFAABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQAUABQAB4AHgAYABMAUAArACsABAAbABsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAFAABAAEAAQABAAEAFAABAAEAAQAUAAEAAQABAAEAAQAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArACsAHgArAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAUAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEAA0ADQBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUAArACsAKwBQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABABQACsAKwArACsAKwArACsAKwAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUAAaABoAUABQAFAAUABQAEwAHgAbAFAAHgAEACsAKwAEAAQABAArAFAAUABQAFAAUABQACsAKwArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQACsAUABQACsAKwAEACsABAAEAAQABAAEACsAKwArACsABAAEACsAKwAEAAQABAArACsAKwAEACsAKwArACsAKwArACsAUABQAFAAUAArAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLAAQABABQAFAAUAAEAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAArACsAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AGwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAKwArACsAKwArAAQABAAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAAQAUAArAFAAUABQAFAAUABQACsAKwArAFAAUABQACsAUABQAFAAUAArACsAKwBQAFAAKwBQACsAUABQACsAKwArAFAAUAArACsAKwBQAFAAUAArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArAAQABAAEAAQABAArACsAKwAEAAQABAArAAQABAAEAAQAKwArAFAAKwArACsAKwArACsABAArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAHgAeAB4AHgAeAB4AGwAeACsAKwArACsAKwAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAUABQAFAAKwArACsAKwArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwAOAFAAUABQAFAAUABQAFAAHgBQAAQABAAEAA4AUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAKwArAAQAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAKwArACsAKwArACsAUAArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAFAABAAEAAQABAAEAAQABAArAAQABAAEACsABAAEAAQABABQAB4AKwArACsAKwBQAFAAUAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQABoAUABQAFAAUABQAFAAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQACsAUAArACsAUABQAFAAUABQAFAAUAArACsAKwAEACsAKwArACsABAAEAAQABAAEAAQAKwAEACsABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArAAQABAAeACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAXAAqACoAKgAqACoAKgAqACsAKwArACsAGwBcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAeAEsASwBLAEsASwBLAEsASwBLAEsADQANACsAKwArACsAKwBcAFwAKwBcACsAXABcAFwAXABcACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAXAArAFwAXABcAFwAXABcAFwAXABcAFwAKgBcAFwAKgAqACoAKgAqACoAKgAqACoAXAArACsAXABcAFwAXABcACsAXAArACoAKgAqACoAKgAqACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwBcAFwAXABcAFAADgAOAA4ADgAeAA4ADgAJAA4ADgANAAkAEwATABMAEwATAAkAHgATAB4AHgAeAAQABAAeAB4AHgAeAB4AHgBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQAFAADQAEAB4ABAAeAAQAFgARABYAEQAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAAQABAAEAAQADQAEAAQAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAA0ADQAeAB4AHgAeAB4AHgAEAB4AHgAeAB4AHgAeACsAHgAeAA4ADgANAA4AHgAeAB4AHgAeAAkACQArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgBcAEsASwBLAEsASwBLAEsASwBLAEsADQANAB4AHgAeAB4AXABcAFwAXABcAFwAKgAqACoAKgBcAFwAXABcACoAKgAqAFwAKgAqACoAXABcACoAKgAqACoAKgAqACoAXABcAFwAKgAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqAFwAKgBLAEsASwBLAEsASwBLAEsASwBLACoAKgAqACoAKgAqAFAAUABQAFAAUABQACsAUAArACsAKwArACsAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAKwBQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsABAAEAAQAHgANAB4AHgAeAB4AHgAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUAArACsADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWABEAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQANAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAANAA0AKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUAArAAQABAArACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqAA0ADQAVAFwADQAeAA0AGwBcACoAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwAeAB4AEwATAA0ADQAOAB4AEwATAB4ABAAEAAQACQArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAHgArACsAKwATABMASwBLAEsASwBLAEsASwBLAEsASwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAXABcAFwAXABcACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAXAArACsAKwAqACoAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsAHgAeAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKwAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKwArAAQASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACoAKgAqACoAKgAqACoAXAAqACoAKgAqACoAKgArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABABQAFAAUABQAFAAUABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwANAA0AHgANAA0ADQANAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwAeAB4AHgAeAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArAA0ADQANAA0ADQBLAEsASwBLAEsASwBLAEsASwBLACsAKwArAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUAAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAAQAUABQAFAAUABQAFAABABQAFAABAAEAAQAUAArACsAKwArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQACsAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAFAAUABQACsAHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQACsAKwAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQACsAHgAeAB4AHgAeAB4AHgAOAB4AKwANAA0ADQANAA0ADQANAAkADQANAA0ACAAEAAsABAAEAA0ACQANAA0ADAAdAB0AHgAXABcAFgAXABcAFwAWABcAHQAdAB4AHgAUABQAFAANAAEAAQAEAAQABAAEAAQACQAaABoAGgAaABoAGgAaABoAHgAXABcAHQAVABUAHgAeAB4AHgAeAB4AGAAWABEAFQAVABUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ADQAeAA0ADQANAA0AHgANAA0ADQAHAB4AHgAeAB4AKwAEAAQABAAEAAQABAAEAAQABAAEAFAAUAArACsATwBQAFAAUABQAFAAHgAeAB4AFgARAE8AUABPAE8ATwBPAFAAUABQAFAAUAAeAB4AHgAWABEAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArABsAGwAbABsAGwAbABsAGgAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGgAbABsAGwAbABoAGwAbABoAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAHgAeAFAAGgAeAB0AHgBQAB4AGgAeAB4AHgAeAB4AHgAeAB4AHgBPAB4AUAAbAB4AHgBQAFAAUABQAFAAHgAeAB4AHQAdAB4AUAAeAFAAHgBQAB4AUABPAFAAUAAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgBQAFAAUABQAE8ATwBQAFAAUABQAFAATwBQAFAATwBQAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAUABQAFAATwBPAE8ATwBPAE8ATwBPAE8ATwBQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABPAB4AHgArACsAKwArAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHQAdAB4AHgAeAB0AHQAeAB4AHQAeAB4AHgAdAB4AHQAbABsAHgAdAB4AHgAeAB4AHQAeAB4AHQAdAB0AHQAeAB4AHQAeAB0AHgAdAB0AHQAdAB0AHQAeAB0AHgAeAB4AHgAeAB0AHQAdAB0AHgAeAB4AHgAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHgAeAB0AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAeAB0AHQAdAB0AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAdAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAWABEAHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAWABEAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AHQAdAB0AHgAeAB0AHgAeAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlAB4AHQAdAB4AHgAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AJQAlAB0AHQAlAB4AJQAlACUAIAAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAdAB0AHQAeAB0AJQAdAB0AHgAdAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAdAB0AHQAdACUAHgAlACUAJQAdACUAJQAdAB0AHQAlACUAHQAdACUAHQAdACUAJQAlAB4AHQAeAB4AHgAeAB0AHQAlAB0AHQAdAB0AHQAdACUAJQAlACUAJQAdACUAJQAgACUAHQAdACUAJQAlACUAJQAlACUAJQAeAB4AHgAlACUAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AFwAXABcAFwAXABcAHgATABMAJQAeAB4AHgAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARABYAEQAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAEAAQABAAeAB4AKwArACsAKwArABMADQANAA0AUAATAA0AUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUAANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAA0ADQANAA0ADQANAA0ADQAeAA0AFgANAB4AHgAXABcAHgAeABcAFwAWABEAFgARABYAEQAWABEADQANAA0ADQATAFAADQANAB4ADQANAB4AHgAeAB4AHgAMAAwADQANAA0AHgANAA0AFgANAA0ADQANAA0ADQANAA0AHgANAB4ADQANAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArAA0AEQARACUAJQBHAFcAVwAWABEAFgARABYAEQAWABEAFgARACUAJQAWABEAFgARABYAEQAWABEAFQAWABEAEQAlAFcAVwBXAFcAVwBXAFcAVwBXAAQABAAEAAQABAAEACUAVwBXAFcAVwA2ACUAJQBXAFcAVwBHAEcAJQAlACUAKwBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBRAFcAUQBXAFEAVwBXAFcAVwBXAFcAUQBXAFcAVwBXAFcAVwBRAFEAKwArAAQABAAVABUARwBHAFcAFQBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBRAFcAVwBXAFcAVwBXAFEAUQBXAFcAVwBXABUAUQBHAEcAVwArACsAKwArACsAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwAlACUAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACsAKwArACsAKwArACsAKwArACsAKwArAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBPAE8ATwBPAE8ATwBPAE8AJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADQATAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABLAEsASwBLAEsASwBLAEsASwBLAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAABAAEAAQABAAeAAQABAAEAAQABAAEAAQABAAEAAQAHgBQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAeAA0ADQANAA0ADQArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAAQAUABQAFAABABQAFAAUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAeAB4AHgAeAAQAKwArACsAUABQAFAAUABQAFAAHgAeABoAHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADgAOABMAEwArACsAKwArACsAKwArACsABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwANAA0ASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUAAeAB4AHgBQAA4AUABQAAQAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArAB4AWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYACsAKwArAAQAHgAeAB4AHgAeAB4ADQANAA0AHgAeAB4AHgArAFAASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArAB4AHgBcAFwAXABcAFwAKgBcAFwAXABcAFwAXABcAFwAXABcAEsASwBLAEsASwBLAEsASwBLAEsAXABcAFwAXABcACsAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAFAAUABQAAQAUABQAFAAUABQAFAAUABQAAQABAArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAHgANAA0ADQBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAXAAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAKgAqACoAXABcACoAKgBcAFwAXABcAFwAKgAqAFwAKgBcACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcACoAKgBQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAA0ADQBQAFAAUAAEAAQAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQADQAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAVABVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBUAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVACsAKwArACsAKwArACsAKwArACsAKwArAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAKwArACsAKwBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAKwArACsAKwAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAKwArACsAKwArAFYABABWAFYAVgBWAFYAVgBWAFYAVgBWAB4AVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgArAFYAVgBWAFYAVgArAFYAKwBWAFYAKwBWAFYAKwBWAFYAVgBWAFYAVgBWAFYAVgBWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAEQAWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAaAB4AKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAGAARABEAGAAYABMAEwAWABEAFAArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACUAJQAlACUAJQAWABEAFgARABYAEQAWABEAFgARABYAEQAlACUAFgARACUAJQAlACUAJQAlACUAEQAlABEAKwAVABUAEwATACUAFgARABYAEQAWABEAJQAlACUAJQAlACUAJQAlACsAJQAbABoAJQArACsAKwArAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAcAKwATACUAJQAbABoAJQAlABYAEQAlACUAEQAlABEAJQBXAFcAVwBXAFcAVwBXAFcAVwBXABUAFQAlACUAJQATACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXABYAJQARACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAWACUAEQAlABYAEQARABYAEQARABUAVwBRAFEAUQBRAFEAUQBRAFEAUQBRAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcARwArACsAVwBXAFcAVwBXAFcAKwArAFcAVwBXAFcAVwBXACsAKwBXAFcAVwBXAFcAVwArACsAVwBXAFcAKwArACsAGgAbACUAJQAlABsAGwArAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAAQAB0AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsADQANAA0AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAA0AUABQAFAAUAArACsAKwArAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwArAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwBQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAUABQAFAAUABQAAQABAAEACsABAAEACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAKwBQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAA0ADQANAA0ADQANAA0ADQAeACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAArACsAKwArAFAAUABQAFAAUAANAA0ADQANAA0ADQAUACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsADQANAA0ADQANAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArAAQABAANACsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAB4AHgAeAB4AHgArACsAKwArACsAKwAEAAQABAAEAAQABAAEAA0ADQAeAB4AHgAeAB4AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsASwBLAEsASwBLAEsASwBLAEsASwANAA0ADQANAFAABAAEAFAAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAeAA4AUAArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAADQANAB4ADQAEAAQABAAEAB4ABAAEAEsASwBLAEsASwBLAEsASwBLAEsAUAAOAFAADQANAA0AKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAANAA0AHgANAA0AHgAEACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAA0AKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsABAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsABAAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAUAArACsAKwArACsAKwAEACsAKwArACsAKwBQAFAAUABQAFAABAAEACsAKwAEAAQABAAEAAQABAAEACsAKwArAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAAQABABQAFAAUABQAA0ADQANAA0AHgBLAEsASwBLAEsASwBLAEsASwBLAA0ADQArAB4ABABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUAAeAFAAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABAAEAAQADgANAA0AEwATAB4AHgAeAA0ADQANAA0ADQANAA0ADQANAA0ADQANAA0ADQANAFAAUABQAFAABAAEACsAKwAEAA0ADQAeAFAAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKwArACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBcAFwADQANAA0AKgBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAKwArAFAAKwArAFAAUABQAFAAUABQAFAAUAArAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQAKwAEAAQAKwArAAQABAAEAAQAUAAEAFAABAAEAA0ADQANACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABABQAA4AUAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAFAABAAEAAQABAAOAB4ADQANAA0ADQAOAB4ABAArACsAKwArACsAKwArACsAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAA0ADQANAFAADgAOAA4ADQANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAAQABAAEAFAADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAOABMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAArACsAKwAEACsABAAEACsABAAEAAQABAAEAAQABABQAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAaABoAGgAaAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABIAEgAQwBDAEMAUABQAFAAUABDAFAAUABQAEgAQwBIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABDAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAJAAkACQAJAAkACQAJABYAEQArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwANAA0AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAANACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAA0ADQANAB4AHgAeAB4AHgAeAFAAUABQAFAADQAeACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAA0AHgAeACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAARwBHABUARwAJACsAKwArACsAKwArACsAKwArACsAKwAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUQBRAFEAKwArACsAKwArACsAKwArACsAKwArACsAKwBRAFEAUQBRACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAHgAEAAQADQAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQABAAEAAQABAAeAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQAHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAKwArAFAAKwArAFAAUAArACsAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUAArAFAAUABQAFAAUABQAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAHgAeAFAAUABQAFAAUAArAFAAKwArACsAUABQAFAAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeACsAKwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4ABAAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAHgAeAA0ADQANAA0AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArAAQABAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwBQAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArABsAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAB4AHgAeAB4ABAAEAAQABAAEAAQABABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArABYAFgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAGgBQAFAAUAAaAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUAArACsAKwArACsAKwBQACsAKwArACsAUAArAFAAKwBQACsAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUAArAFAAKwBQACsAUAArAFAAUAArAFAAKwArAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAKwBQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8AJQAlACUAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB4AHgAeACUAJQAlAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAlACUAJQAlACUAHgAlACUAJQAlACUAIAAgACAAJQAlACAAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACEAIQAhACEAIQAlACUAIAAgACUAJQAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAIAAlACUAJQAlACAAIAAgACUAIAAgACAAJQAlACUAJQAlACUAJQAgACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAlAB4AJQAeACUAJQAlACUAJQAgACUAJQAlACUAHgAlAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACAAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABcAFwAXABUAFQAVAB4AHgAeAB4AJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAgACUAJQAgACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAIAAgACUAJQAgACAAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACAAIAAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACAAIAAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAA=="),f=Array.isArray(u)?function(A){for(var e=A.length,t=[],B=0;B=this._value.length?-1:this._value[A]},NA.prototype.consumeUnicodeRangeToken=function(){for(var A=[],e=this.consumeCodePoint();QA(e)&&A.length<6;)A.push(e),e=this.consumeCodePoint();for(var t=!1;63===e&&A.length<6;)A.push(e),e=this.consumeCodePoint(),t=!0;if(t)return{type:30,start:parseInt(g.apply(void 0,A.map(function(A){return 63===A?48:A})),16),end:parseInt(g.apply(void 0,A.map(function(A){return 63===A?70:A})),16)};var B=parseInt(g.apply(void 0,A),16);if(45===this.peekCodePoint(0)&&QA(this.peekCodePoint(1))){this.consumeCodePoint();for(var e=this.consumeCodePoint(),r=[];QA(e)&&r.length<6;)r.push(e),e=this.consumeCodePoint();return{type:30,start:B,end:parseInt(g.apply(void 0,r),16)}}return{type:30,start:B,end:B}},NA.prototype.consumeIdentLikeToken=function(){var A=this.consumeName();return"url"===A.toLowerCase()&&40===this.peekCodePoint(0)?(this.consumeCodePoint(),this.consumeUrlToken()):40===this.peekCodePoint(0)?(this.consumeCodePoint(),{type:19,value:A}):{type:20,value:A}},NA.prototype.consumeUrlToken=function(){var A=[];if(this.consumeWhiteSpace(),-1===this.peekCodePoint(0))return{type:22,value:""};var e,t=this.peekCodePoint(0);if(39===t||34===t){t=this.consumeStringToken(this.consumeCodePoint());return 0===t.type&&(this.consumeWhiteSpace(),-1===this.peekCodePoint(0)||41===this.peekCodePoint(0))?(this.consumeCodePoint(),{type:22,value:t.value}):(this.consumeBadUrlRemnants(),KA)}for(;;){var B=this.consumeCodePoint();if(-1===B||41===B)return{type:22,value:g.apply(void 0,A)};if(cA(B))return this.consumeWhiteSpace(),-1===this.peekCodePoint(0)||41===this.peekCodePoint(0)?(this.consumeCodePoint(),{type:22,value:g.apply(void 0,A)}):(this.consumeBadUrlRemnants(),KA);if(34===B||39===B||40===B||(0<=(e=B)&&e<=8||11===e||14<=e&&e<=31||127===e))return this.consumeBadUrlRemnants(),KA;if(92===B){if(!wA(B,this.peekCodePoint(0)))return this.consumeBadUrlRemnants(),KA;A.push(this.consumeEscapedCodePoint())}else A.push(B)}},NA.prototype.consumeWhiteSpace=function(){for(;cA(this.peekCodePoint(0));)this.consumeCodePoint()},NA.prototype.consumeBadUrlRemnants=function(){for(;;){var A=this.consumeCodePoint();if(41===A||-1===A)return;wA(A,this.peekCodePoint(0))&&this.consumeEscapedCodePoint()}},NA.prototype.consumeStringSlice=function(A){for(var e="";0>8,B=255&A>>16,A=255&A>>24;return e<255?"rgba("+A+","+B+","+t+","+e/255+")":"rgb("+A+","+B+","+t+")"}function Be(A,e){if(17===A.type)return A.number;if(16!==A.type)return 0;var t=3===e?1:255;return 3===e?A.number/100*t:Math.round(A.number/100*t)}var re=function(A,e){return 11===e&&12===A.type||(28===e&&29===A.type||2===e&&3===A.type)},ne={type:17,number:0,flags:4},se={type:16,number:50,flags:4},oe={type:16,number:100,flags:4},ie=function(A,e){if(16===A.type)return A.number/100*e;if(VA(A))switch(A.unit){case"rem":case"em":return 16*A.number;default:return A.number}return A.number},Qe=function(A,e){if(15===e.type)switch(e.unit){case"deg":return Math.PI*e.number/180;case"grad":return Math.PI/200*e.number;case"rad":return e.number;case"turn":return 2*Math.PI*e.number}throw new Error("Unsupported angle type")},ce=function(A){return Math.PI*A/180},ae=function(A,e){if(18===e.type){var t=ue[e.name];if(void 0===t)throw new Error('Attempting to parse an unsupported color function "'+e.name+'"');return t(A,e.values)}if(5===e.type){if(3===e.value.length){var B=e.value.substring(0,1),r=e.value.substring(1,2),n=e.value.substring(2,3);return ge(parseInt(B+B,16),parseInt(r+r,16),parseInt(n+n,16),1)}if(4===e.value.length){var B=e.value.substring(0,1),r=e.value.substring(1,2),n=e.value.substring(2,3),s=e.value.substring(3,4);return ge(parseInt(B+B,16),parseInt(r+r,16),parseInt(n+n,16),parseInt(s+s,16)/255)}if(6===e.value.length){B=e.value.substring(0,2),r=e.value.substring(2,4),n=e.value.substring(4,6);return ge(parseInt(B,16),parseInt(r,16),parseInt(n,16),1)}if(8===e.value.length){B=e.value.substring(0,2),r=e.value.substring(2,4),n=e.value.substring(4,6),s=e.value.substring(6,8);return ge(parseInt(B,16),parseInt(r,16),parseInt(n,16),parseInt(s,16)/255)}}if(20===e.type){e=he[e.value.toUpperCase()];if(void 0!==e)return e}return he.TRANSPARENT},ge=function(A,e,t,B){return(A<<24|e<<16|t<<8|Math.round(255*B)<<0)>>>0},we=function(A,e){e=e.filter(YA);if(3===e.length){var t=e.map(Be),B=t[0],r=t[1],t=t[2];return ge(B,r,t,1)}if(4!==e.length)return 0;e=e.map(Be),B=e[0],r=e[1],t=e[2],e=e[3];return ge(B,r,t,e)};function Ce(A,e,t){return t<0&&(t+=1),1<=t&&--t,t<1/6?(e-A)*t*6+A:t<.5?e:t<2/3?6*(e-A)*(2/3-t)+A:A}function Ue(A,e){return ae(A,GA.create(e).parseComponentValue())}var le,Fe=function(A,e){var t=e.filter(YA),B=t[0],r=t[1],n=t[2],e=t[3],t=(17===B.type?ce(B.number):Qe(A,B))/(2*Math.PI),A=qA(r)?r.number/100:0,B=qA(n)?n.number/100:0,r=void 0!==e&&qA(e)?ie(e,1):1;if(0==A)return ge(255*B,255*B,255*B,1);n=B<=.5?B*(1+A):B+A-B*A,e=2*B-n,A=Ce(e,n,t+1/3),B=Ce(e,n,t),t=Ce(e,n,t-1/3);return ge(255*A,255*B,255*t,r)},ue={hsl:Fe,hsla:Fe,rgb:we,rgba:we},he={ALICEBLUE:4042850303,ANTIQUEWHITE:4209760255,AQUA:16777215,AQUAMARINE:2147472639,AZURE:4043309055,BEIGE:4126530815,BISQUE:4293182719,BLACK:255,BLANCHEDALMOND:4293643775,BLUE:65535,BLUEVIOLET:2318131967,BROWN:2771004159,BURLYWOOD:3736635391,CADETBLUE:1604231423,CHARTREUSE:2147418367,CHOCOLATE:3530104575,CORAL:4286533887,CORNFLOWERBLUE:1687547391,CORNSILK:4294499583,CRIMSON:3692313855,CYAN:16777215,DARKBLUE:35839,DARKCYAN:9145343,DARKGOLDENROD:3095837695,DARKGRAY:2846468607,DARKGREEN:6553855,DARKGREY:2846468607,DARKKHAKI:3182914559,DARKMAGENTA:2332068863,DARKOLIVEGREEN:1433087999,DARKORANGE:4287365375,DARKORCHID:2570243327,DARKRED:2332033279,DARKSALMON:3918953215,DARKSEAGREEN:2411499519,DARKSLATEBLUE:1211993087,DARKSLATEGRAY:793726975,DARKSLATEGREY:793726975,DARKTURQUOISE:13554175,DARKVIOLET:2483082239,DEEPPINK:4279538687,DEEPSKYBLUE:12582911,DIMGRAY:1768516095,DIMGREY:1768516095,DODGERBLUE:512819199,FIREBRICK:2988581631,FLORALWHITE:4294635775,FORESTGREEN:579543807,FUCHSIA:4278255615,GAINSBORO:3705462015,GHOSTWHITE:4177068031,GOLD:4292280575,GOLDENROD:3668254975,GRAY:2155905279,GREEN:8388863,GREENYELLOW:2919182335,GREY:2155905279,HONEYDEW:4043305215,HOTPINK:4285117695,INDIANRED:3445382399,INDIGO:1258324735,IVORY:4294963455,KHAKI:4041641215,LAVENDER:3873897215,LAVENDERBLUSH:4293981695,LAWNGREEN:2096890111,LEMONCHIFFON:4294626815,LIGHTBLUE:2916673279,LIGHTCORAL:4034953471,LIGHTCYAN:3774873599,LIGHTGOLDENRODYELLOW:4210742015,LIGHTGRAY:3553874943,LIGHTGREEN:2431553791,LIGHTGREY:3553874943,LIGHTPINK:4290167295,LIGHTSALMON:4288707327,LIGHTSEAGREEN:548580095,LIGHTSKYBLUE:2278488831,LIGHTSLATEGRAY:2005441023,LIGHTSLATEGREY:2005441023,LIGHTSTEELBLUE:2965692159,LIGHTYELLOW:4294959359,LIME:16711935,LIMEGREEN:852308735,LINEN:4210091775,MAGENTA:4278255615,MAROON:2147483903,MEDIUMAQUAMARINE:1724754687,MEDIUMBLUE:52735,MEDIUMORCHID:3126187007,MEDIUMPURPLE:2473647103,MEDIUMSEAGREEN:1018393087,MEDIUMSLATEBLUE:2070474495,MEDIUMSPRINGGREEN:16423679,MEDIUMTURQUOISE:1221709055,MEDIUMVIOLETRED:3340076543,MIDNIGHTBLUE:421097727,MINTCREAM:4127193855,MISTYROSE:4293190143,MOCCASIN:4293178879,NAVAJOWHITE:4292783615,NAVY:33023,OLDLACE:4260751103,OLIVE:2155872511,OLIVEDRAB:1804477439,ORANGE:4289003775,ORANGERED:4282712319,ORCHID:3664828159,PALEGOLDENROD:4008225535,PALEGREEN:2566625535,PALETURQUOISE:2951671551,PALEVIOLETRED:3681588223,PAPAYAWHIP:4293907967,PEACHPUFF:4292524543,PERU:3448061951,PINK:4290825215,PLUM:3718307327,POWDERBLUE:2967529215,PURPLE:2147516671,REBECCAPURPLE:1714657791,RED:4278190335,ROSYBROWN:3163525119,ROYALBLUE:1097458175,SADDLEBROWN:2336560127,SALMON:4202722047,SANDYBROWN:4104413439,SEAGREEN:780883967,SEASHELL:4294307583,SIENNA:2689740287,SILVER:3233857791,SKYBLUE:2278484991,SLATEBLUE:1784335871,SLATEGRAY:1887473919,SLATEGREY:1887473919,SNOW:4294638335,SPRINGGREEN:16744447,STEELBLUE:1182971135,TAN:3535047935,TEAL:8421631,THISTLE:3636451583,TOMATO:4284696575,TRANSPARENT:0,TURQUOISE:1088475391,VIOLET:4001558271,WHEAT:4125012991,WHITE:4294967295,WHITESMOKE:4126537215,YELLOW:4294902015,YELLOWGREEN:2597139199};(ve=le=le||{})[ve.BORDER_BOX=0]="BORDER_BOX",ve[ve.PADDING_BOX=1]="PADDING_BOX";function de(A,e){return A=ae(A,e[0]),(e=e[1])&&qA(e)?{color:A,stop:e}:{color:A,stop:null}}function Ee(A,t){var e=A[0],B=A[A.length-1];null===e.stop&&(e.stop=ne),null===B.stop&&(B.stop=oe);for(var r=[],n=0,s=0;sA.optimumDistance)?{optimumCorner:e,optimumDistance:B}:A},{optimumDistance:s?1/0:-1/0,optimumCorner:null}).optimumCorner}var pe,ye={name:"background-clip",initialValue:"border-box",prefix:!(ve[ve.CONTENT_BOX=2]="CONTENT_BOX"),type:1,parse:function(A,e){return e.map(function(A){if(kA(A))switch(A.value){case"padding-box":return le.PADDING_BOX;case"content-box":return le.CONTENT_BOX}return le.BORDER_BOX})}},Ke={name:"background-color",initialValue:"transparent",prefix:!1,type:3,format:"color"},Fe=function(t,A){var B=ce(180),r=[];return WA(A).forEach(function(A,e){if(0===e){e=A[0];if(20===e.type&&-1!==["top","left","right","bottom"].indexOf(e.value))return void(B=Ae(A));if($A(e))return void(B=(Qe(t,e)+ce(270))%ce(360))}A=de(t,A);r.push(A)}),{angle:B,stops:r,type:pe.LINEAR_GRADIENT}},Le="closest-side",me="farthest-side",De="closest-corner",be="farthest-corner",Re="ellipse",Oe="contain",we=function(B,A){var r=Se.CIRCLE,n=Te.FARTHEST_CORNER,s=[],o=[];return WA(A).forEach(function(A,e){var t=!0;0===e?t=A.reduce(function(A,e){if(kA(e))switch(e.value){case"center":return o.push(se),!1;case"top":case"left":return o.push(ne),!1;case"right":case"bottom":return o.push(oe),!1}else if(qA(e)||ZA(e))return o.push(e),!1;return A},t):1===e&&(t=A.reduce(function(A,e){if(kA(e))switch(e.value){case"circle":return r=Se.CIRCLE,!1;case Re:return r=Se.ELLIPSE,!1;case Oe:case Le:return n=Te.CLOSEST_SIDE,!1;case me:return n=Te.FARTHEST_SIDE,!1;case De:return n=Te.CLOSEST_CORNER,!1;case"cover":case be:return n=Te.FARTHEST_CORNER,!1}else if(ZA(e)||qA(e))return(n=!Array.isArray(n)?[]:n).push(e),!1;return A},t)),t&&(A=de(B,A),s.push(A))}),{size:n,shape:r,stops:s,position:o,type:pe.RADIAL_GRADIENT}};(ve=pe=pe||{})[ve.URL=0]="URL",ve[ve.LINEAR_GRADIENT=1]="LINEAR_GRADIENT",ve[ve.RADIAL_GRADIENT=2]="RADIAL_GRADIENT";var Se,Te,ve;(ve=Se=Se||{})[ve.CIRCLE=0]="CIRCLE",ve[ve.ELLIPSE=1]="ELLIPSE",(ve=Te=Te||{})[ve.CLOSEST_SIDE=0]="CLOSEST_SIDE",ve[ve.FARTHEST_SIDE=1]="FARTHEST_SIDE",ve[ve.CLOSEST_CORNER=2]="CLOSEST_CORNER",ve[ve.FARTHEST_CORNER=3]="FARTHEST_CORNER";var Me=function(A,e){if(22===e.type){var t={url:e.value,type:pe.URL};return A.cache.addImage(e.value),t}if(18!==e.type)throw new Error("Unsupported image type "+e.type);t=Ge[e.name];if(void 0===t)throw new Error('Attempting to parse an unsupported image function "'+e.name+'"');return t(A,e.values)};var Ne,Ge={"linear-gradient":function(t,A){var B=ce(180),r=[];return WA(A).forEach(function(A,e){if(0===e){e=A[0];if(20===e.type&&"to"===e.value)return void(B=Ae(A));if($A(e))return void(B=Qe(t,e))}A=de(t,A);r.push(A)}),{angle:B,stops:r,type:pe.LINEAR_GRADIENT}},"-moz-linear-gradient":Fe,"-ms-linear-gradient":Fe,"-o-linear-gradient":Fe,"-webkit-linear-gradient":Fe,"radial-gradient":function(r,A){var n=Se.CIRCLE,s=Te.FARTHEST_CORNER,o=[],i=[];return WA(A).forEach(function(A,e){var t,B=!0;0===e&&(t=!1,B=A.reduce(function(A,e){if(t)if(kA(e))switch(e.value){case"center":return i.push(se),A;case"top":case"left":return i.push(ne),A;case"right":case"bottom":return i.push(oe),A}else(qA(e)||ZA(e))&&i.push(e);else if(kA(e))switch(e.value){case"circle":return n=Se.CIRCLE,!1;case Re:return n=Se.ELLIPSE,!1;case"at":return!(t=!0);case Le:return s=Te.CLOSEST_SIDE,!1;case"cover":case me:return s=Te.FARTHEST_SIDE,!1;case Oe:case De:return s=Te.CLOSEST_CORNER,!1;case be:return s=Te.FARTHEST_CORNER,!1}else if(ZA(e)||qA(e))return(s=!Array.isArray(s)?[]:s).push(e),!1;return A},B)),B&&(A=de(r,A),o.push(A))}),{size:s,shape:n,stops:o,position:i,type:pe.RADIAL_GRADIENT}},"-moz-radial-gradient":we,"-ms-radial-gradient":we,"-o-radial-gradient":we,"-webkit-radial-gradient":we,"-webkit-gradient":function(B,A){var e=ce(180),r=[],n=pe.LINEAR_GRADIENT,t=Se.CIRCLE,s=Te.FARTHEST_CORNER;return WA(A).forEach(function(A,e){var t,A=A[0];if(0===e){if(kA(A)&&"linear"===A.value)return void(n=pe.LINEAR_GRADIENT);if(kA(A)&&"radial"===A.value)return void(n=pe.RADIAL_GRADIENT)}18===A.type&&("from"===A.name?(t=ae(B,A.values[0]),r.push({stop:ne,color:t})):"to"===A.name?(t=ae(B,A.values[0]),r.push({stop:oe,color:t})):"color-stop"!==A.name||2===(A=A.values.filter(YA)).length&&(t=ae(B,A[1]),A=A[0],PA(A)&&r.push({stop:{type:16,number:100*A.number,flags:A.flags},color:t})))}),n===pe.LINEAR_GRADIENT?{angle:(e+ce(180))%ce(360),stops:r,type:n}:{size:s,shape:t,stops:r,position:[],type:n}}},xe={name:"background-image",initialValue:"none",type:1,prefix:!1,parse:function(e,A){if(0===A.length)return[];var t=A[0];return 20===t.type&&"none"===t.value?[]:A.filter(function(A){return YA(A)&&!(20===(A=A).type&&"none"===A.value||18===A.type&&!Ge[A.name])}).map(function(A){return Me(e,A)})}},Ve={name:"background-origin",initialValue:"border-box",prefix:!1,type:1,parse:function(A,e){return e.map(function(A){if(kA(A))switch(A.value){case"padding-box":return 1;case"content-box":return 2}return 0})}},Pe={name:"background-position",initialValue:"0% 0%",type:1,prefix:!1,parse:function(A,e){return WA(e).map(function(A){return A.filter(qA)}).map(jA)}};(we=Ne=Ne||{})[we.REPEAT=0]="REPEAT",we[we.NO_REPEAT=1]="NO_REPEAT",we[we.REPEAT_X=2]="REPEAT_X";var ke,Je={name:"background-repeat",initialValue:"repeat",prefix:!(we[we.REPEAT_Y=3]="REPEAT_Y"),type:1,parse:function(A,e){return WA(e).map(function(A){return A.filter(kA).map(function(A){return A.value}).join(" ")}).map(Xe)}},Xe=function(A){switch(A){case"no-repeat":return Ne.NO_REPEAT;case"repeat-x":case"repeat no-repeat":return Ne.REPEAT_X;case"repeat-y":case"no-repeat repeat":return Ne.REPEAT_Y;default:return Ne.REPEAT}};(we=ke=ke||{}).AUTO="auto",we.CONTAIN="contain";var _e,Ye={name:"background-size",initialValue:"0",prefix:!(we.COVER="cover"),type:1,parse:function(A,e){return WA(e).map(function(A){return A.filter(We)})}},We=function(A){return kA(A)||qA(A)},we=function(A){return{name:"border-"+A+"-color",initialValue:"transparent",prefix:!1,type:3,format:"color"}},Ze=we("top"),qe=we("right"),je=we("bottom"),ze=we("left"),we=function(A){return{name:"border-radius-"+A,initialValue:"0 0",prefix:!1,type:1,parse:function(A,e){return jA(e.filter(qA))}}},$e=we("top-left"),At=we("top-right"),et=we("bottom-right"),tt=we("bottom-left");(we=_e=_e||{})[we.NONE=0]="NONE",we[we.SOLID=1]="SOLID",we[we.DASHED=2]="DASHED",we[we.DOTTED=3]="DOTTED",we[we.DOUBLE=4]="DOUBLE";var Bt,we=function(A){return{name:"border-"+A+"-style",initialValue:"solid",prefix:!1,type:2,parse:function(A,e){switch(e){case"none":return _e.NONE;case"dashed":return _e.DASHED;case"dotted":return _e.DOTTED;case"double":return _e.DOUBLE}return _e.SOLID}}},rt=we("top"),nt=we("right"),st=we("bottom"),ot=we("left"),we=function(A){return{name:"border-"+A+"-width",initialValue:"0",type:0,prefix:!1,parse:function(A,e){return VA(e)?e.number:0}}},it=we("top"),Qt=we("right"),ct=we("bottom"),at=we("left"),gt={name:"color",initialValue:"transparent",prefix:!1,type:3,format:"color"},wt={name:"direction",initialValue:"ltr",prefix:!1,type:2,parse:function(A,e){return"rtl"!==e?0:1}},Ct={name:"display",initialValue:"inline-block",prefix:!1,type:1,parse:function(A,e){return e.filter(kA).reduce(function(A,e){return A|Ut(e.value)},0)}},Ut=function(A){switch(A){case"block":case"-webkit-box":return 2;case"inline":return 4;case"run-in":return 8;case"flow":return 16;case"flow-root":return 32;case"table":return 64;case"flex":case"-webkit-flex":return 128;case"grid":case"-ms-grid":return 256;case"ruby":return 512;case"subgrid":return 1024;case"list-item":return 2048;case"table-row-group":return 4096;case"table-header-group":return 8192;case"table-footer-group":return 16384;case"table-row":return 32768;case"table-cell":return 65536;case"table-column-group":return 131072;case"table-column":return 262144;case"table-caption":return 524288;case"ruby-base":return 1048576;case"ruby-text":return 2097152;case"ruby-base-container":return 4194304;case"ruby-text-container":return 8388608;case"contents":return 16777216;case"inline-block":return 33554432;case"inline-list-item":return 67108864;case"inline-table":return 134217728;case"inline-flex":return 268435456;case"inline-grid":return 536870912}return 0};(we=Bt=Bt||{})[we.NONE=0]="NONE",we[we.LEFT=1]="LEFT",we[we.RIGHT=2]="RIGHT",we[we.INLINE_START=3]="INLINE_START";function lt(A,e){return kA(A)&&"normal"===A.value?1.2*e:17===A.type?e*A.number:qA(A)?ie(A,e):e}var Ft,ut,ht={name:"float",initialValue:"none",prefix:!(we[we.INLINE_END=4]="INLINE_END"),type:2,parse:function(A,e){switch(e){case"left":return Bt.LEFT;case"right":return Bt.RIGHT;case"inline-start":return Bt.INLINE_START;case"inline-end":return Bt.INLINE_END}return Bt.NONE}},dt={name:"letter-spacing",initialValue:"0",prefix:!1,type:0,parse:function(A,e){return!(20===e.type&&"normal"===e.value||17!==e.type&&15!==e.type)?e.number:0}},Et={name:"line-break",initialValue:(we=Ft=Ft||{}).NORMAL="normal",prefix:!(we.STRICT="strict"),type:2,parse:function(A,e){return"strict"!==e?Ft.NORMAL:Ft.STRICT}},Ht={name:"line-height",initialValue:"normal",prefix:!1,type:4},ft={name:"list-style-image",initialValue:"none",type:0,prefix:!1,parse:function(A,e){return 20===e.type&&"none"===e.value?null:Me(A,e)}};(we=ut=ut||{})[we.INSIDE=0]="INSIDE";var It,pt={name:"list-style-position",initialValue:"outside",prefix:!(we[we.OUTSIDE=1]="OUTSIDE"),type:2,parse:function(A,e){return"inside"!==e?ut.OUTSIDE:ut.INSIDE}};(we=It=It||{})[we.NONE=-1]="NONE",we[we.DISC=0]="DISC",we[we.CIRCLE=1]="CIRCLE",we[we.SQUARE=2]="SQUARE",we[we.DECIMAL=3]="DECIMAL",we[we.CJK_DECIMAL=4]="CJK_DECIMAL",we[we.DECIMAL_LEADING_ZERO=5]="DECIMAL_LEADING_ZERO",we[we.LOWER_ROMAN=6]="LOWER_ROMAN",we[we.UPPER_ROMAN=7]="UPPER_ROMAN",we[we.LOWER_GREEK=8]="LOWER_GREEK",we[we.LOWER_ALPHA=9]="LOWER_ALPHA",we[we.UPPER_ALPHA=10]="UPPER_ALPHA",we[we.ARABIC_INDIC=11]="ARABIC_INDIC",we[we.ARMENIAN=12]="ARMENIAN",we[we.BENGALI=13]="BENGALI",we[we.CAMBODIAN=14]="CAMBODIAN",we[we.CJK_EARTHLY_BRANCH=15]="CJK_EARTHLY_BRANCH",we[we.CJK_HEAVENLY_STEM=16]="CJK_HEAVENLY_STEM",we[we.CJK_IDEOGRAPHIC=17]="CJK_IDEOGRAPHIC",we[we.DEVANAGARI=18]="DEVANAGARI",we[we.ETHIOPIC_NUMERIC=19]="ETHIOPIC_NUMERIC",we[we.GEORGIAN=20]="GEORGIAN",we[we.GUJARATI=21]="GUJARATI",we[we.GURMUKHI=22]="GURMUKHI",we[we.HEBREW=22]="HEBREW",we[we.HIRAGANA=23]="HIRAGANA",we[we.HIRAGANA_IROHA=24]="HIRAGANA_IROHA",we[we.JAPANESE_FORMAL=25]="JAPANESE_FORMAL",we[we.JAPANESE_INFORMAL=26]="JAPANESE_INFORMAL",we[we.KANNADA=27]="KANNADA",we[we.KATAKANA=28]="KATAKANA",we[we.KATAKANA_IROHA=29]="KATAKANA_IROHA",we[we.KHMER=30]="KHMER",we[we.KOREAN_HANGUL_FORMAL=31]="KOREAN_HANGUL_FORMAL",we[we.KOREAN_HANJA_FORMAL=32]="KOREAN_HANJA_FORMAL",we[we.KOREAN_HANJA_INFORMAL=33]="KOREAN_HANJA_INFORMAL",we[we.LAO=34]="LAO",we[we.LOWER_ARMENIAN=35]="LOWER_ARMENIAN",we[we.MALAYALAM=36]="MALAYALAM",we[we.MONGOLIAN=37]="MONGOLIAN",we[we.MYANMAR=38]="MYANMAR",we[we.ORIYA=39]="ORIYA",we[we.PERSIAN=40]="PERSIAN",we[we.SIMP_CHINESE_FORMAL=41]="SIMP_CHINESE_FORMAL",we[we.SIMP_CHINESE_INFORMAL=42]="SIMP_CHINESE_INFORMAL",we[we.TAMIL=43]="TAMIL",we[we.TELUGU=44]="TELUGU",we[we.THAI=45]="THAI",we[we.TIBETAN=46]="TIBETAN",we[we.TRAD_CHINESE_FORMAL=47]="TRAD_CHINESE_FORMAL",we[we.TRAD_CHINESE_INFORMAL=48]="TRAD_CHINESE_INFORMAL",we[we.UPPER_ARMENIAN=49]="UPPER_ARMENIAN",we[we.DISCLOSURE_OPEN=50]="DISCLOSURE_OPEN";var yt,Kt={name:"list-style-type",initialValue:"none",prefix:!(we[we.DISCLOSURE_CLOSED=51]="DISCLOSURE_CLOSED"),type:2,parse:function(A,e){switch(e){case"disc":return It.DISC;case"circle":return It.CIRCLE;case"square":return It.SQUARE;case"decimal":return It.DECIMAL;case"cjk-decimal":return It.CJK_DECIMAL;case"decimal-leading-zero":return It.DECIMAL_LEADING_ZERO;case"lower-roman":return It.LOWER_ROMAN;case"upper-roman":return It.UPPER_ROMAN;case"lower-greek":return It.LOWER_GREEK;case"lower-alpha":return It.LOWER_ALPHA;case"upper-alpha":return It.UPPER_ALPHA;case"arabic-indic":return It.ARABIC_INDIC;case"armenian":return It.ARMENIAN;case"bengali":return It.BENGALI;case"cambodian":return It.CAMBODIAN;case"cjk-earthly-branch":return It.CJK_EARTHLY_BRANCH;case"cjk-heavenly-stem":return It.CJK_HEAVENLY_STEM;case"cjk-ideographic":return It.CJK_IDEOGRAPHIC;case"devanagari":return It.DEVANAGARI;case"ethiopic-numeric":return It.ETHIOPIC_NUMERIC;case"georgian":return It.GEORGIAN;case"gujarati":return It.GUJARATI;case"gurmukhi":return It.GURMUKHI;case"hebrew":return It.HEBREW;case"hiragana":return It.HIRAGANA;case"hiragana-iroha":return It.HIRAGANA_IROHA;case"japanese-formal":return It.JAPANESE_FORMAL;case"japanese-informal":return It.JAPANESE_INFORMAL;case"kannada":return It.KANNADA;case"katakana":return It.KATAKANA;case"katakana-iroha":return It.KATAKANA_IROHA;case"khmer":return It.KHMER;case"korean-hangul-formal":return It.KOREAN_HANGUL_FORMAL;case"korean-hanja-formal":return It.KOREAN_HANJA_FORMAL;case"korean-hanja-informal":return It.KOREAN_HANJA_INFORMAL;case"lao":return It.LAO;case"lower-armenian":return It.LOWER_ARMENIAN;case"malayalam":return It.MALAYALAM;case"mongolian":return It.MONGOLIAN;case"myanmar":return It.MYANMAR;case"oriya":return It.ORIYA;case"persian":return It.PERSIAN;case"simp-chinese-formal":return It.SIMP_CHINESE_FORMAL;case"simp-chinese-informal":return It.SIMP_CHINESE_INFORMAL;case"tamil":return It.TAMIL;case"telugu":return It.TELUGU;case"thai":return It.THAI;case"tibetan":return It.TIBETAN;case"trad-chinese-formal":return It.TRAD_CHINESE_FORMAL;case"trad-chinese-informal":return It.TRAD_CHINESE_INFORMAL;case"upper-armenian":return It.UPPER_ARMENIAN;case"disclosure-open":return It.DISCLOSURE_OPEN;case"disclosure-closed":return It.DISCLOSURE_CLOSED;default:return It.NONE}}},we=function(A){return{name:"margin-"+A,initialValue:"0",prefix:!1,type:4}},Lt=we("top"),mt=we("right"),Dt=we("bottom"),bt=we("left");(we=yt=yt||{})[we.VISIBLE=0]="VISIBLE",we[we.HIDDEN=1]="HIDDEN",we[we.SCROLL=2]="SCROLL",we[we.CLIP=3]="CLIP";var Rt,Ot={name:"overflow",initialValue:"visible",prefix:!(we[we.AUTO=4]="AUTO"),type:1,parse:function(A,e){return e.filter(kA).map(function(A){switch(A.value){case"hidden":return yt.HIDDEN;case"scroll":return yt.SCROLL;case"clip":return yt.CLIP;case"auto":return yt.AUTO;default:return yt.VISIBLE}})}},St={name:"overflow-wrap",initialValue:"normal",prefix:!1,type:2,parse:function(A,e){return"break-word"!==e?"normal":"break-word"}},we=function(A){return{name:"padding-"+A,initialValue:"0",prefix:!1,type:3,format:"length-percentage"}},Tt=we("top"),vt=we("right"),Mt=we("bottom"),Nt=we("left");(we=Rt=Rt||{})[we.LEFT=0]="LEFT",we[we.CENTER=1]="CENTER";var Gt,xt={name:"text-align",initialValue:"left",prefix:!(we[we.RIGHT=2]="RIGHT"),type:2,parse:function(A,e){switch(e){case"right":return Rt.RIGHT;case"center":case"justify":return Rt.CENTER;default:return Rt.LEFT}}};(we=Gt=Gt||{})[we.STATIC=0]="STATIC",we[we.RELATIVE=1]="RELATIVE",we[we.ABSOLUTE=2]="ABSOLUTE",we[we.FIXED=3]="FIXED";var Vt,Pt={name:"position",initialValue:"static",prefix:!(we[we.STICKY=4]="STICKY"),type:2,parse:function(A,e){switch(e){case"relative":return Gt.RELATIVE;case"absolute":return Gt.ABSOLUTE;case"fixed":return Gt.FIXED;case"sticky":return Gt.STICKY}return Gt.STATIC}},kt={name:"text-shadow",initialValue:"none",type:1,prefix:!1,parse:function(n,A){return 1===A.length&&XA(A[0],"none")?[]:WA(A).map(function(A){for(var e={color:he.TRANSPARENT,offsetX:ne,offsetY:ne,blur:ne},t=0,B=0;B>5],this.data[e=(e<<2)+(31&A)];if(A<=65535)return e=this.index[2048+(A-55296>>5)],this.data[e=(e<<2)+(31&A)];if(A>11)],e=this.index[e+=A>>5&63],this.data[e=(e<<2)+(31&A)];if(A<=1114111)return this.data[this.highValueIndex]}return this.errorValue},SB);function SB(A,e,t,B,r,n){this.initialValue=A,this.errorValue=e,this.highStart=t,this.highValueIndex=B,this.index=r,this.data=n}for(var TB="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",vB="undefined"==typeof Uint8Array?[]:new Uint8Array(256),MB=0;MB>10),s%1024+56320)),(r+1===t||16384>4,i[o++]=(15&t)<<4|B>>2,i[o++]=(3&B)<<6|63&r;return n}(PB="AAAAAAAAAAAAEA4AGBkAAFAaAAACAAAAAAAIABAAGAAwADgACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAAQABIAEQATAAIABAACAAQAAgAEAAIABAAVABcAAgAEAAIABAACAAQAGAAaABwAHgAgACIAI4AlgAIABAAmwCjAKgAsAC2AL4AvQDFAMoA0gBPAVYBWgEIAAgACACMANoAYgFkAWwBdAF8AX0BhQGNAZUBlgGeAaMBlQGWAasBswF8AbsBwwF0AcsBYwHTAQgA2wG/AOMBdAF8AekB8QF0AfkB+wHiAHQBfAEIAAMC5gQIAAsCEgIIAAgAFgIeAggAIgIpAggAMQI5AkACygEIAAgASAJQAlgCYAIIAAgACAAKBQoFCgUTBRMFGQUrBSsFCAAIAAgACAAIAAgACAAIAAgACABdAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABoAmgCrwGvAQgAbgJ2AggAHgEIAAgACADnAXsCCAAIAAgAgwIIAAgACAAIAAgACACKAggAkQKZAggAPADJAAgAoQKkAqwCsgK6AsICCADJAggA0AIIAAgACAAIANYC3gIIAAgACAAIAAgACABAAOYCCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAkASoB+QIEAAgACAA8AEMCCABCBQgACABJBVAFCAAIAAgACAAIAAgACAAIAAgACABTBVoFCAAIAFoFCABfBWUFCAAIAAgACAAIAAgAbQUIAAgACAAIAAgACABzBXsFfQWFBYoFigWKBZEFigWKBYoFmAWfBaYFrgWxBbkFCAAIAAgACAAIAAgACAAIAAgACAAIAMEFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAMgFCADQBQgACAAIAAgACAAIAAgACAAIAAgACAAIAO4CCAAIAAgAiQAIAAgACABAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAD0AggACAD8AggACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIANYFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAMDvwAIAAgAJAIIAAgACAAIAAgACAAIAAgACwMTAwgACAB9BOsEGwMjAwgAKwMyAwsFYgE3A/MEPwMIAEUDTQNRAwgAWQOsAGEDCAAIAAgACAAIAAgACABpAzQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFIQUoBSwFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABtAwgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABMAEwACAAIAAgACAAIABgACAAIAAgACAC/AAgACAAyAQgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACAAIAAwAAgACAAIAAgACAAIAAgACAAIAAAARABIAAgACAAIABQASAAIAAgAIABwAEAAjgCIABsAqAC2AL0AigDQAtwC+IJIQqVAZUBWQqVAZUBlQGVAZUBlQGrC5UBlQGVAZUBlQGVAZUBlQGVAXsKlQGVAbAK6wsrDGUMpQzlDJUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAfAKAAuZA64AtwCJALoC6ADwAAgAuACgA/oEpgO6AqsD+AAIAAgAswMIAAgACAAIAIkAuwP5AfsBwwPLAwgACAAIAAgACADRA9kDCAAIAOED6QMIAAgACAAIAAgACADuA/YDCAAIAP4DyQAIAAgABgQIAAgAXQAOBAgACAAIAAgACAAIABMECAAIAAgACAAIAAgACAD8AAQBCAAIAAgAGgQiBCoECAExBAgAEAEIAAgACAAIAAgACAAIAAgACAAIAAgACAA4BAgACABABEYECAAIAAgATAQYAQgAVAQIAAgACAAIAAgACAAIAAgACAAIAFoECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAOQEIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAB+BAcACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAEABhgSMBAgACAAIAAgAlAQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAwAEAAQABAADAAMAAwADAAQABAAEAAQABAAEAAQABHATAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAdQMIAAgACAAIAAgACAAIAMkACAAIAAgAfQMIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACFA4kDCAAIAAgACAAIAOcBCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAIcDCAAIAAgACAAIAAgACAAIAAgACAAIAJEDCAAIAAgACADFAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABgBAgAZgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAbAQCBXIECAAIAHkECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABAAJwEQACjBKoEsgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAC6BMIECAAIAAgACAAIAAgACABmBAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAxwQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAGYECAAIAAgAzgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBd0FXwUIAOIF6gXxBYoF3gT5BQAGCAaKBYoFigWKBYoFigWKBYoFigWKBYoFigXWBIoFigWKBYoFigWKBYoFigWKBYsFEAaKBYoFigWKBYoFigWKBRQGCACKBYoFigWKBQgACAAIANEECAAIABgGigUgBggAJgYIAC4GMwaKBYoF0wQ3Bj4GigWKBYoFigWKBYoFigWKBYoFigWKBYoFigUIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWLBf///////wQABAAEAAQABAAEAAQABAAEAAQAAwAEAAQAAgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAQADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUAAAAFAAUAAAAFAAUAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAQAAAAUABQAFAAUABQAFAAAAAAAFAAUAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAUAAQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAAABwAHAAcAAAAHAAcABwAFAAEAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAcABwAFAAUAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQABAAAAAAAAAAAAAAAFAAUABQAFAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAHAAcAAAAHAAcAAAAAAAUABQAHAAUAAQAHAAEABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwABAAUABQAFAAUAAAAAAAAAAAAAAAEAAQABAAEAAQABAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABQANAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAABQAHAAUABQAFAAAAAAAAAAcABQAFAAUABQAFAAQABAAEAAQABAAEAAQABAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUAAAAFAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAUAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAcABwAFAAcABwAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUABwAHAAUABQAFAAUAAAAAAAcABwAAAAAABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAAAAAAAAAAABQAFAAAAAAAFAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAFAAUABQAFAAUAAAAFAAUABwAAAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABwAFAAUABQAFAAAAAAAHAAcAAAAAAAcABwAFAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAAAAAAAAAHAAcABwAAAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAUABQAFAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAHAAcABQAHAAcAAAAFAAcABwAAAAcABwAFAAUAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAFAAcABwAFAAUABQAAAAUAAAAHAAcABwAHAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAHAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUAAAAFAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAUAAAAFAAUAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABwAFAAUABQAFAAUABQAAAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABQAFAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAFAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAHAAUABQAFAAUABQAFAAUABwAHAAcABwAHAAcABwAHAAUABwAHAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABwAHAAcABwAFAAUABwAHAAcAAAAAAAAAAAAHAAcABQAHAAcABwAHAAcABwAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAUABQAFAAUABQAFAAUAAAAFAAAABQAAAAAABQAFAAUABQAFAAUABQAFAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAUABQAFAAUABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABwAFAAcABwAHAAcABwAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAUABQAFAAUABwAHAAUABQAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABQAFAAcABwAHAAUABwAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAcABQAFAAUABQAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAAAAAABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAUABQAHAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAFAAUABQAFAAcABwAFAAUABwAHAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAcABwAFAAUABwAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABQAAAAAABQAFAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAcABwAAAAAAAAAAAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAcABwAFAAcABwAAAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAFAAUABQAAAAUABQAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABwAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAHAAcABQAHAAUABQAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAAABwAHAAAAAAAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAFAAUABwAFAAcABwAFAAcABQAFAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAAAAAABwAHAAcABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAFAAcABwAFAAUABQAFAAUABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAUABQAFAAcABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABQAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAAAAAAFAAUABwAHAAcABwAFAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAHAAUABQAFAAUABQAFAAUABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAABQAAAAUABQAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAHAAcAAAAFAAUAAAAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABQAFAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAABQAFAAUABQAFAAUABQAAAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAFAAUABQAFAAUADgAOAA4ADgAOAA4ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAMAAwADAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAAAAAAAAAAAAsADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwACwAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAADgAOAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAAAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4AAAAOAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAAAAAAAAAAAA4AAAAOAAAAAAAAAAAADgAOAA4AAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAA="),XB=Array.isArray(JB)?function(A){for(var e=A.length,t=[],B=0;Bs.x||t.y>s.y;return s=t,0===e||A});return A.body.removeChild(e),t}(document);return Object.defineProperty(tr,"SUPPORT_WORD_BREAKING",{value:A}),A},get SUPPORT_SVG_DRAWING(){var A=function(A){var e=new Image,t=A.createElement("canvas"),A=t.getContext("2d");if(!A)return!1;e.src="data:image/svg+xml,";try{A.drawImage(e,0,0),t.toDataURL()}catch(A){return!1}return!0}(document);return Object.defineProperty(tr,"SUPPORT_SVG_DRAWING",{value:A}),A},get SUPPORT_FOREIGNOBJECT_DRAWING(){var A="function"==typeof Array.from&&"function"==typeof window.fetch?function(t){var A=t.createElement("canvas"),B=100;A.width=B,A.height=B;var r=A.getContext("2d");if(!r)return Promise.reject(!1);r.fillStyle="rgb(0, 255, 0)",r.fillRect(0,0,B,B);var e=new Image,n=A.toDataURL();e.src=n;e=Ar(B,B,0,0,e);return r.fillStyle="red",r.fillRect(0,0,B,B),er(e).then(function(A){r.drawImage(A,0,0);var e=r.getImageData(0,0,B,B).data;r.fillStyle="red",r.fillRect(0,0,B,B);A=t.createElement("div");return A.style.backgroundImage="url("+n+")",A.style.height="100px",VB(e)?er(Ar(B,B,0,0,A)):Promise.reject(!1)}).then(function(A){return r.drawImage(A,0,0),VB(r.getImageData(0,0,B,B).data)}).catch(function(){return!1})}(document):Promise.resolve(!1);return Object.defineProperty(tr,"SUPPORT_FOREIGNOBJECT_DRAWING",{value:A}),A},get SUPPORT_CORS_IMAGES(){var A=void 0!==(new Image).crossOrigin;return Object.defineProperty(tr,"SUPPORT_CORS_IMAGES",{value:A}),A},get SUPPORT_RESPONSE_TYPE(){var A="string"==typeof(new XMLHttpRequest).responseType;return Object.defineProperty(tr,"SUPPORT_RESPONSE_TYPE",{value:A}),A},get SUPPORT_CORS_XHR(){var A="withCredentials"in new XMLHttpRequest;return Object.defineProperty(tr,"SUPPORT_CORS_XHR",{value:A}),A}},Br=function(A,e){this.text=A,this.bounds=e},rr=function(A,e){var t=e.ownerDocument;if(t){var B=t.createElement("html2canvaswrapper");B.appendChild(e.cloneNode(!0));t=e.parentNode;if(t){t.replaceChild(B,e);A=E(A,B);return B.firstChild&&t.replaceChild(B.firstChild,B),A}}return d.EMPTY},nr=function(A,e,t){var B=A.ownerDocument;if(!B)throw new Error("Node has no owner document");B=B.createRange();return B.setStart(A,e),B.setEnd(A,e+t),B},sr=function(A,e,t,B){return d.fromClientRect(A,nr(e,t,B).getBoundingClientRect())},or=function(A,e){return 0!==e.letterSpacing?xB(A):Qr(A,e)},ir=[32,160,4961,65792,65793,4153,4241],Qr=function(A,e){for(var t,B=oA(A,{lineBreak:e.lineBreak,wordBreak:"break-word"===e.overflowWrap?"break-word":e.wordBreak}),r=[];!(t=B.next()).done;)!function(){var A,e;t.value&&(A=t.value.slice(),A=Q(A),e="",A.forEach(function(A){-1===ir.indexOf(A)?e+=g(A):(e.length&&r.push(e),r.push(g(A)),e="")}),e.length&&r.push(e))}();return r},cr=function(A,e,t){var B,r,n,s,o;this.text=ar(e.data,t.textTransform),this.textBounds=(B=A,A=this.text,n=e,A=or(A,r=t),s=[],o=0,A.forEach(function(A){var e;r.textDecorationLine.length||0e.height?new d(e.left+(e.width-e.height)/2,e.top,e.height,e.height):e.width"),Gn(this.referenceElement.ownerDocument,t,n),o.replaceChild(o.adoptNode(this.documentElement),o.documentElement),o.close(),A},bn.prototype.createElementClone=function(A){if(KB(A,2),Qn(A))return this.createCanvasClone(A);if(_r(A))return this.createStyleClone(A);var e=A.cloneNode(!1);return cn(e)&&(cn(A)&&A.currentSrc&&A.currentSrc!==A.src&&(e.src=A.currentSrc,e.srcset=""),"lazy"===e.loading&&(e.loading="eager")),e},bn.prototype.createStyleClone=function(A){try{var e=A.sheet;if(e&&e.cssRules){var t=[].slice.call(e.cssRules,0).reduce(function(A,e){return e&&"string"==typeof e.cssText?A+e.cssText:A},""),B=A.cloneNode(!1);return B.textContent=t,B}}catch(A){if(this.context.logger.error("Unable to access cssRules property",A),"SecurityError"!==A.name)throw A}return A.cloneNode(!1)},bn.prototype.createCanvasClone=function(e){var A;if(this.options.inlineImages&&e.ownerDocument){var t=e.ownerDocument.createElement("img");try{return t.src=e.toDataURL(),t}catch(A){this.context.logger.info("Unable to inline canvas contents, canvas is tainted",e)}}t=e.cloneNode(!1);try{t.width=e.width,t.height=e.height;var B,r,n=e.getContext("2d"),s=t.getContext("2d");return s&&(!this.options.allowTaint&&n?s.putImageData(n.getImageData(0,0,e.width,e.height),0,0):(!(B=null!==(A=e.getContext("webgl2"))&&void 0!==A?A:e.getContext("webgl"))||!1===(null==(r=B.getContextAttributes())?void 0:r.preserveDrawingBuffer)&&this.context.logger.warn("Unable to clone WebGL context as it has preserveDrawingBuffer=false",e),s.drawImage(e,0,0))),t}catch(A){this.context.logger.info("Unable to clone canvas as it is tainted",e)}return t},bn.prototype.cloneNode=function(A){if($r(A))return document.createTextNode(A.data);if(!A.ownerDocument)return A.cloneNode(!1);var e=A.ownerDocument.defaultView;if(e&&An(A)&&(en(A)||tn(A))){var t=this.createElementClone(A);t.style.transitionProperty="none";var B=e.getComputedStyle(A),r=e.getComputedStyle(A,":before"),n=e.getComputedStyle(A,":after");this.referenceElement===A&&en(t)&&(this.clonedReferenceElement=t),on(t)&&Jn(t);for(var e=this.counters.parse(new pB(this.context,B)),r=this.resolvePseudoContent(A,t,r,Hn.BEFORE),s=A.firstChild;s;s=s.nextSibling)An(s)&&("SCRIPT"===s.tagName||s.hasAttribute(mn)||"function"==typeof this.options.ignoreElements&&this.options.ignoreElements(s))||this.options.copyStyles&&An(s)&&_r(s)||t.appendChild(this.cloneNode(s));r&&t.insertBefore(r,t.firstChild);n=this.resolvePseudoContent(A,t,n,Hn.AFTER);return n&&t.appendChild(n),this.counters.pop(e),B&&(this.options.copyStyles||tn(A))&&!an(A)&&Mn(B,t),0===A.scrollTop&&0===A.scrollLeft||this.scrolledElements.push([t,A.scrollLeft,A.scrollTop]),(gn(A)||wn(A))&&(gn(t)||wn(t))&&(t.value=A.value),t}return A.cloneNode(!1)},bn.prototype.resolvePseudoContent=function(o,A,e,t){var i=this;if(e){var B=e.content,Q=A.ownerDocument;if(Q&&B&&"none"!==B&&"-moz-alt-content"!==B&&"none"!==e.display){this.counters.parse(new pB(this.context,e));var c=new IB(this.context,e),a=Q.createElement("html2canvaspseudoelement");Mn(e,a),c.content.forEach(function(A){if(0===A.type)a.appendChild(Q.createTextNode(A.value));else if(22===A.type){var e=Q.createElement("img");e.src=A.value,e.style.opacity="1",a.appendChild(e)}else if(18===A.type){var t,B,r,n,s;"attr"===A.name?(e=A.values.filter(kA)).length&&a.appendChild(Q.createTextNode(o.getAttribute(e[0].value)||"")):"counter"===A.name?(r=(B=A.values.filter(YA))[0],B=B[1],r&&kA(r)&&(t=i.counters.getCounterValue(r.value),s=B&&kA(B)?Kt.parse(i.context,B.value):It.DECIMAL,a.appendChild(Q.createTextNode(Ln(t,s,!1))))):"counters"===A.name&&(r=(t=A.values.filter(YA))[0],s=t[1],B=t[2],r&&kA(r)&&(r=i.counters.getCounterValues(r.value),n=B&&kA(B)?Kt.parse(i.context,B.value):It.DECIMAL,s=s&&0===s.type?s.value:"",s=r.map(function(A){return Ln(A,n,!1)}).join(s),a.appendChild(Q.createTextNode(s))))}else if(20===A.type)switch(A.value){case"open-quote":a.appendChild(Q.createTextNode(cB(c.quotes,i.quoteDepth++,!0)));break;case"close-quote":a.appendChild(Q.createTextNode(cB(c.quotes,--i.quoteDepth,!1)));break;default:a.appendChild(Q.createTextNode(A.value))}}),a.className=Vn+" "+Pn;t=t===Hn.BEFORE?" "+Vn:" "+Pn;return tn(A)?A.className.baseValue+=t:A.className+=t,a}}},bn.destroy=function(A){return!!A.parentNode&&(A.parentNode.removeChild(A),!0)},bn);function bn(A,e,t){if(this.context=A,this.options=t,this.scrolledElements=[],this.referenceElement=e,this.counters=new Un,this.quoteDepth=0,!e.ownerDocument)throw new Error("Cloned element does not have an owner document");this.documentElement=this.cloneNode(e.ownerDocument.documentElement)}(we=Hn=Hn||{})[we.BEFORE=0]="BEFORE",we[we.AFTER=1]="AFTER";function Rn(e){return new Promise(function(A){!e.complete&&e.src?(e.onload=A,e.onerror=A):A()})}var On=function(A,e){var t=A.createElement("iframe");return t.className="html2canvas-container",t.style.visibility="hidden",t.style.position="fixed",t.style.left="-10000px",t.style.top="0px",t.style.border="0",t.width=e.width.toString(),t.height=e.height.toString(),t.scrolling="no",t.setAttribute(mn,"true"),A.body.appendChild(t),t},Sn=function(A){return Promise.all([].slice.call(A.images,0).map(Rn))},Tn=function(r){return new Promise(function(e,A){var t=r.contentWindow;if(!t)return A("No window assigned for iframe");var B=t.document;t.onload=r.onload=function(){t.onload=r.onload=null;var A=setInterval(function(){0"),e},Gn=function(A,e,t){A&&A.defaultView&&(e!==A.defaultView.pageXOffset||t!==A.defaultView.pageYOffset)&&A.defaultView.scrollTo(e,t)},xn=function(A){var e=A[0],t=A[1],A=A[2];e.scrollLeft=t,e.scrollTop=A},Vn="___html2canvas___pseudoelement_before",Pn="___html2canvas___pseudoelement_after",kn='{\n content: "" !important;\n display: none !important;\n}',Jn=function(A){Xn(A,"."+Vn+":before"+kn+"\n ."+Pn+":after"+kn)},Xn=function(A,e){var t=A.ownerDocument;t&&((t=t.createElement("style")).textContent=e,A.appendChild(t))},_n=(Yn.getOrigin=function(A){var e=Yn._link;return e?(e.href=A,e.href=e.href,e.protocol+e.hostname+e.port):"about:blank"},Yn.isSameOrigin=function(A){return Yn.getOrigin(A)===Yn._origin},Yn.setContext=function(A){Yn._link=A.document.createElement("a"),Yn._origin=Yn.getOrigin(A.location.href)},Yn._origin="about:blank",Yn);function Yn(){}var Wn=(Zn.prototype.addImage=function(A){var e=Promise.resolve();return this.has(A)||(Bs(A)||As(A))&&(this._cache[A]=this.loadImage(A)).catch(function(){}),e},Zn.prototype.match=function(A){return this._cache[A]},Zn.prototype.loadImage=function(s){return a(this,void 0,void 0,function(){var e,B,t,r,n=this;return H(this,function(A){switch(A.label){case 0:return(e=_n.isSameOrigin(s),B=!es(s)&&!0===this._options.useCORS&&tr.SUPPORT_CORS_IMAGES&&!e,t=!es(s)&&!e&&!Bs(s)&&"string"==typeof this._options.proxy&&tr.SUPPORT_CORS_XHR&&!B,e||!1!==this._options.allowTaint||es(s)||Bs(s)||t||B)?(r=s,t?[4,this.proxy(r)]:[3,2]):[2];case 1:r=A.sent(),A.label=2;case 2:return this.context.logger.debug("Added image "+s.substring(0,256)),[4,new Promise(function(A,e){var t=new Image;t.onload=function(){return A(t)},t.onerror=e,(ts(r)||B)&&(t.crossOrigin="anonymous"),t.src=r,!0===t.complete&&setTimeout(function(){return A(t)},500),0t.width+l?0:Math.max(0,n-l),Math.max(0,s-U),gs.TOP_RIGHT):new ss(t.left+t.width-l,t.top+U),this.bottomRightPaddingBox=0t.width+u+A?0:n-u+A,s-(U+h),gs.TOP_RIGHT):new ss(t.left+t.width-(l+d),t.top+U+h),this.bottomRightContentBox=0A.element.container.styles.zIndex.order?(s=e,!1):0=A.element.container.styles.zIndex.order?(o=e+1,!1):0Peloton Workout in progress! - + @@ -79,7 +79,7 @@

Peloton Workout in progress!

- + @@ -88,7 +88,21 @@

Peloton Workout in progress!

- + + + + + + + + + + + + + + + @@ -97,7 +111,7 @@

Peloton Workout in progress!

- + @@ -106,7 +120,7 @@

Peloton Workout in progress!

- + @@ -115,7 +129,7 @@

Peloton Workout in progress!

- + @@ -124,7 +138,7 @@

Peloton Workout in progress!

- + @@ -133,7 +147,7 @@

Peloton Workout in progress!

- + @@ -142,32 +156,32 @@

Peloton Workout in progress!

- + - + - + - + - + - + - + - +
🏃 SPEED AVGMAX 0.0
📐 INCLINE AVGMAX 0.0
🏃PACEAVG00:0000:00MAX00:00
🚵ELEV.0.0
🚴 CADENCE AVGMAX 0
💓 PULSE AVGMAX 0
POWER AVGMAX 0
🚥 P.ZONE AVGMAX 0.0
🆁 RESISTANCE AVGMAX 1
🅿 P.RESISTANCE AVGMAX 1
🔥 CALORIES 0
🔥 TOT.OUTPUT 0
📏 DISTANCE 0.00
⏲️ ELAPSED 0:00:00
⏲️ REM.TIME 0:00:00
P.OFFSET
GEARS
- \ No newline at end of file + diff --git a/src/inspirebike.cpp b/src/inspirebike.cpp index 84de75364..52c1fec51 100644 --- a/src/inspirebike.cpp +++ b/src/inspirebike.cpp @@ -1,6 +1,4 @@ #include "inspirebike.h" -#include "ios/lockscreen.h" -#include "keepawakehelper.h" #include "virtualbike.h" #include #include @@ -10,6 +8,10 @@ #include #include +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif + using namespace std::chrono_literals; //#include //#include @@ -191,16 +193,7 @@ void inspirebike::characteristicChanged(const QLowEnergyCharacteristic &characte #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -256,7 +249,7 @@ void inspirebike::stateChanged(QLowEnergyService::ServiceState state) { &inspirebike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -278,9 +271,10 @@ void inspirebike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&inspirebike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &inspirebike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -377,10 +371,6 @@ bool inspirebike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *inspirebike::VirtualBike() { return virtualBike; } - -void *inspirebike::VirtualDevice() { return VirtualBike(); } - uint16_t inspirebike::watts() { QSettings settings; if (currentCadence().value() == 0) { diff --git a/src/inspirebike.h b/src/inspirebike.h index fd60dada3..b04c7d0e5 100644 --- a/src/inspirebike.h +++ b/src/inspirebike.h @@ -37,11 +37,8 @@ class inspirebike : public bike { Q_OBJECT public: inspirebike(bool noWriteResistance, bool noHeartService); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; const resistance_t max_resistance = 40; @@ -49,11 +46,10 @@ class inspirebike : public bike { void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; QTimer *t_timeout; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattNotify1Characteristic; diff --git a/src/ios/AppDelegate.swift b/src/ios/AppDelegate.swift index 992d69f10..7e7420364 100644 --- a/src/ios/AppDelegate.swift +++ b/src/ios/AppDelegate.swift @@ -62,8 +62,8 @@ var pedometer = CMPedometer() } else { sender = "PHONE" } - Server.server?.send("SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#ODO=\(distance)#") WatchKitConnection.distance = distance; + Server.server?.send(createString(sender: sender)) } @objc public func setKcal(kcal: Double) -> Void @@ -74,8 +74,48 @@ var pedometer = CMPedometer() } else { sender = "PHONE" } - Server.server?.send("SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(kcal)#") WatchKitConnection.kcal = kcal; + Server.server?.send(createString(sender: sender)) + } + + @objc public func setCadence(cadence: Double) -> Void + { + var sender: String + if UIDevice.current.userInterfaceIdiom == .pad { + sender = "PAD" + } else { + sender = "PHONE" + } + WatchKitConnection.cadence = cadence; + Server.server?.send(createString(sender: sender)) + } + + @objc public func setSpeed(speed: Double) -> Void + { + var sender: String + if UIDevice.current.userInterfaceIdiom == .pad { + sender = "PAD" + } else { + sender = "PHONE" + } + WatchKitConnection.speed = speed; + Server.server?.send(createString(sender: sender)) + } + + @objc public func setPower(power: Double) -> Void + { + var sender: String + if UIDevice.current.userInterfaceIdiom == .pad { + sender = "PAD" + } else { + sender = "PHONE" + } + WatchKitConnection.power = power; + Server.server?.send(createString(sender: sender)) + } + + func createString(sender: String) -> String { + return "SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#KCAL=\(WatchKitConnection.kcal)#BCAD=\(WatchKitConnection.cadence)#SPD=\(WatchKitConnection.speed)#PWR=\(WatchKitConnection.power)#CAD=\(WatchKitConnection.stepCadence)#ODO=\(WatchKitConnection.distance)#"; } @objc func updateHeartRate() { @@ -85,8 +125,7 @@ var pedometer = CMPedometer() } else { sender = "PHONE" } - Server.server?.send("SENDER=\(sender)#HR=\(WatchKitConnection.currentHeartRate)#CAD=\(WatchKitConnection.stepCadence)#") - + Server.server?.send(createString(sender: sender)) } } /* diff --git a/src/ios/AppleWatchToIpad/Connection.swift b/src/ios/AppleWatchToIpad/Connection.swift index e0b4bdd33..921d21bd6 100644 --- a/src/ios/AppleWatchToIpad/Connection.swift +++ b/src/ios/AppleWatchToIpad/Connection.swift @@ -97,6 +97,18 @@ class Connection { if sender?.contains("PAD") ?? false && message.contains("ODO=") { let odo : String = message.slice(from: "ODO=", to: "#") ?? "" WatchKitConnection.distance = (Double(odo) ?? 0) + } + if sender?.contains("PAD") ?? false && message.contains("BCAD=") { + let cad : String = message.slice(from: "BCAD=", to: "#") ?? "" + WatchKitConnection.cadence = (Double(cad) ?? 0) + } + if sender?.contains("PAD") ?? false && message.contains("SPD=") { + let spd : String = message.slice(from: "SPD=", to: "#") ?? "" + WatchKitConnection.speed = (Double(spd) ?? 0) + } + if sender?.contains("PAD") ?? false && message.contains("PWR=") { + let pwr : String = message.slice(from: "PWR=", to: "#") ?? "" + WatchKitConnection.power = (Double(pwr) ?? 0) } } } diff --git a/src/ios/GarminConnect.swift b/src/ios/GarminConnect.swift new file mode 100644 index 000000000..76b0272aa --- /dev/null +++ b/src/ios/GarminConnect.swift @@ -0,0 +1,129 @@ +// +// GarminConnect.swift +// qdomyoszwift +// +// Created by Roberto Viola on 17/03/23. +// + +import Foundation +import ConnectIQ + +extension ConnectIQ { + static var shared: ConnectIQ? { + return sharedInstance() + } +} + +@available(iOS 13.0, *) +@objc public class GarminConnect : NSObject { + let v = GarminConnectSwift() + + @objc public func getHR() -> Int { + return v.HR; + } + + @objc public func getFootCad() -> Int { + return v.FootCad; + } + + @objc public func urlParser(_ url: URL) { + v.urlParser(url) + } +} + +@available(iOS 13.0, *) +class GarminConnectSwift: NSObject, IQDeviceEventDelegate, IQAppMessageDelegate { + // This must match the value in `Info.plist`. + private static let urlScheme = "org.cagnulein.connectiqcomms-ciq" + // UUID from `manifest.xml` of the ConnectIQ app. + private static let watchAppUuid = UUID(uuidString: "feec8674-2795-4e03-a283-0b69a0a291e3") + + // Device UUID mapped to the CommsApp on that device. + private var apps: [UUID: IQApp] = [:] + + @Published private var message = "" + + public var HR: Int = 0 + public var FootCad: Int = 0 + + private let formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 7 + return formatter + }() + + private func broadcastMessage() { + for app in apps.values { + // You may send any ObjC type (e.g. NSNumber, NSString, NSArray, NSDictionary). + // Unless you're experiencing difficulties, there's no need to use the `NS*` types directly, + // you can use their Swift equivalents. + ConnectIQ.shared?.sendMessage("General Kenobi.", to: app, progress: nil, completion: { print($0) }) + } + } + + func urlParser(_ url: URL) -> Bool { + guard url.scheme == GarminConnectSwift.urlScheme, + let devices = ConnectIQ.shared?.parseDeviceSelectionResponse(from: url) as? [IQDevice] else { return false } + registerForDeviceEvents(devices: devices) + return true + } + + private func registerForDeviceEvents(devices: [IQDevice]) { + for device in devices { + ConnectIQ.shared?.register(forDeviceEvents: device, delegate: self) + } + } + + public func deviceStatusChanged(_ device: IQDevice!, status: IQDeviceStatus) { + switch status { + case .connected: + // The `store` is not necessary for sending messages, I suppose it's for when you want the user to download the app. + // `IQApp` class needs to be instantiated for every IQDevice, you can't share them, it's the app on the specific device. + let app = IQApp(uuid: GarminConnectSwift.watchAppUuid, store: nil, device: device) + apps[device.uuid] = app + print(device.uuid) + + ConnectIQ.shared?.register(forAppMessages: app, delegate: self) + + // IMPORTANT: Apparently sending a message right after connecting sends the messages to the void. + // I have no idea why it doesn't work, but feel free to shrink the delay. I've found that 100ms works consistently. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + ConnectIQ.shared?.sendMessage("Hello there.", to: app, progress: nil, completion: { print($0) }) + } + + case .bluetoothNotReady, .invalidDevice, .notConnected, .notFound: + print(device.uuid) + apps.removeValue(forKey: device.uuid) + + @unknown default: + print("New case, still unhandled. \(status.rawValue)") + } + } + + func receivedMessage(_ message: Any!, from app: IQApp!) { + print("Received message from ConnectIQ: \(message.debugDescription)") + + guard let array = message as? [Any] else { + print("Failed to parse message sent from ConnectIQ.") + return + } + + for (index, contents) in array.enumerated() { + guard let dictionary = contents as? [Int: Any] else { + print("Failed to parse ConnectIQ message contents at index \(index).") + return + } + print(dictionary) + HR = dictionary[0] as? Int ?? 0 + FootCad = dictionary[1] as? Int ?? 0 + print("Garmin HR: \(HR)") + print("Garmin Foot Cadence: \(FootCad)") + } + } + + deinit { + ConnectIQ.shared?.unregister(forAllDeviceEvents: self) + ConnectIQ.shared?.unregister(forAllAppMessages: self) + } +} + diff --git a/src/ios/Info.plist b/src/ios/Info.plist index 0ca5a677c..bc6437e91 100644 --- a/src/ios/Info.plist +++ b/src/ios/Info.plist @@ -20,8 +20,25 @@ $(MARKETING_VERSION) CFBundleSignature ${QMAKE_PKGINFO_TYPEINFO} + CFBundleURLTypes + + + CFBundleTypeRole + None + CFBundleURLName + ConnectIQ URL Scheme + CFBundleURLSchemes + + org.cagnulein.connectiqcomms-ciq + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) + LSApplicationQueriesSchemes + + gcm-ciq + LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace diff --git a/src/ios/WatchKitConnection.swift b/src/ios/WatchKitConnection.swift index 5372b155f..4dbb649af 100644 --- a/src/ios/WatchKitConnection.swift +++ b/src/ios/WatchKitConnection.swift @@ -25,6 +25,9 @@ class WatchKitConnection: NSObject { static var distance = 0.0 static var stepCadence = 0 static var kcal = 0.0 + static var speed = 0.0 + static var power = 0.0 + static var cadence = 0.0 private override init() { super.init() @@ -130,6 +133,9 @@ extension WatchKitConnection: WCSessionDelegate { replyValues["distance"] = WatchKitConnection.distance replyValues["kcal"] = WatchKitConnection.kcal + replyValues["cadence"] = WatchKitConnection.cadence + replyValues["power"] = WatchKitConnection.power + replyValues["speed"] = WatchKitConnection.speed replyHandler(replyValues) diff --git a/src/ios/ios_app_delegate.mm b/src/ios/ios_app_delegate.mm new file mode 100644 index 000000000..470ab1597 --- /dev/null +++ b/src/ios/ios_app_delegate.mm @@ -0,0 +1,33 @@ +#ifndef IO_UNDER_QT +#import +#import "UIKit/UIKit.h" +#import "UserNotifications/UserNotifications.h" + +@interface QIOSApplicationDelegate +@end + +@interface QIOSApplicationDelegate (QZApplicationDelegate) +@end + +@implementation QIOSApplicationDelegate (QZApplicationDelegate) + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + NSLog(@"launch!"); + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + [center requestAuthorizationWithOptions:UNAuthorizationOptionBadge + completionHandler:^(BOOL granted, NSError *error){ + if(granted == YES) { + [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; + }; + }]; + + return YES; +} + +- (void)application:(UIApplication *)application +performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler +{ +} +@end +#endif diff --git a/src/ios/lockscreen.h b/src/ios/lockscreen.h index 9ce71b4a4..b31ecb6d9 100644 --- a/src/ios/lockscreen.h +++ b/src/ios/lockscreen.h @@ -9,13 +9,16 @@ class lockscreen { long stepCadence(); void setKcal(double kcal); void setDistance(double distance); + void setSpeed(double speed); + void setPower(double power); + void setCadence(double cadence); // virtualbike void virtualbike_ios(); void virtualbike_setHeartRate(unsigned char heartRate); void virtualbike_setCadence(unsigned short crankRevolutions, unsigned short lastCrankEventTime); - void virtualbike_zwift_ios(); + void virtualbike_zwift_ios(bool disable_hr); double virtualbike_getCurrentSlope(); double virtualbike_getCurrentCRR(); double virtualbike_getCurrentCW(); @@ -47,6 +50,15 @@ class lockscreen { // volume double getVolume(); + + // garmin + bool urlParser(const char *url); + void garminconnect_init(); + int getHR(); + int getFootCad(); + + // debug + static void debug(const char* debugstring); }; #endif // LOCKSCREEN_H diff --git a/src/ios/lockscreen.mm b/src/ios/lockscreen.mm index 6f137ed25..dd2257e13 100644 --- a/src/ios/lockscreen.mm +++ b/src/ios/lockscreen.mm @@ -4,8 +4,10 @@ #import #import #import +#import #import "qdomyoszwift-Swift2.h" #include "ios/lockscreen.h" +#include @class virtualbike_ios_swift; @class virtualbike_zwift; @@ -19,6 +21,8 @@ static virtualrower* _virtualrower = nil; static virtualtreadmill_zwift* _virtualtreadmill_zwift = nil; +static GarminConnect* Garmin = 0; + void lockscreen::setTimerDisabled() { [[UIApplication sharedApplication] setIdleTimerDisabled: YES]; } @@ -27,6 +31,9 @@ { h = [[healthkit alloc] init]; [h request]; + if (@available(iOS 13, *)) { + Garmin = [[GarminConnect alloc] init]; + } } long lockscreen::heartRate() @@ -49,6 +56,20 @@ [h setDistanceWithDistance:distance * 0.621371]; } +void lockscreen::setPower(double power) +{ + [h setPowerWithPower:power]; +} +void lockscreen::setCadence(double cadence) +{ + [h setCadenceWithCadence:cadence]; +} +void lockscreen::setSpeed(double speed) +{ + [h setSpeedWithSpeed:speed]; +} + + void lockscreen::virtualbike_ios() { _virtualbike = [[virtualbike_ios_swift alloc] init]; @@ -68,9 +89,9 @@ [_virtualbike updateCadenceWithCrankRevolutions:crankRevolutions LastCrankEventTime:lastCrankEventTime]; } -void lockscreen::virtualbike_zwift_ios() +void lockscreen::virtualbike_zwift_ios(bool disable_hr) { - _virtualbike_zwift = [[virtualbike_zwift alloc] init]; + _virtualbike_zwift = [[virtualbike_zwift alloc] initWithDisable_hr: disable_hr]; } void lockscreen::virtualrower_ios() @@ -205,6 +226,27 @@ return 0; } +void lockscreen::garminconnect_init() { + [[ConnectIQ sharedInstance] initializeWithUrlScheme:@"org.cagnulein.connectiqcomms-ciq" + uiOverrideDelegate:nil]; + + [[ConnectIQ sharedInstance] showConnectIQDeviceSelection]; +} + +bool lockscreen::urlParser(const char *url) { + NSString *sURL = [NSString stringWithCString:url encoding:NSASCIIStringEncoding]; + NSURL *URL = [NSURL URLWithString:[sURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + [Garmin urlParser: URL]; +} + +int lockscreen::getHR() { + return [Garmin getHR]; +} + +int lockscreen::getFootCad() { + return [Garmin getFootCad]; +} + // getVolume double lockscreen::getVolume() @@ -212,4 +254,8 @@ [[AVAudioSession sharedInstance] setActive:true error:0]; return [[AVAudioSession sharedInstance] outputVolume]; } + +void lockscreen::debug(const char* debugstring) { + qDebug() << debugstring; +} #endif diff --git a/src/ios/virtualbike_zwift.swift b/src/ios/virtualbike_zwift.swift index 7ed1e927a..9ca88f1b3 100644 --- a/src/ios/virtualbike_zwift.swift +++ b/src/ios/virtualbike_zwift.swift @@ -11,9 +11,9 @@ let TrainingStatusUuid = CBUUID(string: "0x2AD3"); @objc public class virtualbike_zwift: NSObject { private var peripheralManager: BLEPeripheralManagerZwift! - @objc public override init() { + @objc public init(disable_hr: Bool) { super.init() - peripheralManager = BLEPeripheralManagerZwift() + peripheralManager = BLEPeripheralManagerZwift(disable_hr: disable_hr) } @objc public func updateHeartRate(HeartRate: UInt8) @@ -61,6 +61,7 @@ let TrainingStatusUuid = CBUUID(string: "0x2AD3"); } class BLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate { + private var disable_hr: Bool = false private var peripheralManager: CBPeripheralManager! private var heartRateService: CBMutableService! @@ -107,8 +108,9 @@ class BLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate { private var notificationTimer: Timer! = nil //var delegate: BLEPeripheralManagerDelegate? - override init() { + init(disable_hr: Bool) { super.init() + self.disable_hr = disable_hr peripheralManager = CBPeripheralManager(delegate: self, queue: nil) } @@ -224,14 +226,14 @@ class BLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate { let PowerFeaturePermissions: CBAttributePermissions = [.readable] self.PowerFeatureCharacteristic = CBMutableCharacteristic(type: PowerFeatureCharacteristicUUID, properties: PowerFeatureProperties, - value: Data (bytes: [0x08, 0x00, 0x00, 0x00]), + value: Data (bytes: [0x00, 0x00, 0x00, 0x08]), permissions: PowerFeaturePermissions) let PowerSensorLocationProperties: CBCharacteristicProperties = [.read] let PowerSensorLocationPermissions: CBAttributePermissions = [.readable] self.PowerSensorLocationCharacteristic = CBMutableCharacteristic(type: PowerSensorLocationCharacteristicUUID, properties: PowerSensorLocationProperties, - value: Data (bytes: [0x13]), + value: Data (bytes: [0x0D]), permissions: PowerSensorLocationPermissions) let PowerMeasurementProperties: CBCharacteristicProperties = [.notify, .read] @@ -258,11 +260,19 @@ class BLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate { print("Failed to add service with error: \(uwError.localizedDescription)") return } + + if(disable_hr) { + // useful in order to hide HR from Garmin devices + let advertisementData = [CBAdvertisementDataLocalNameKey: "QZ", + CBAdvertisementDataServiceUUIDsKey: [FitnessMachineServiceUuid, CSCServiceUUID, PowerServiceUUID]] as [String : Any] + peripheralManager.startAdvertising(advertisementData) + } else { + let advertisementData = [CBAdvertisementDataLocalNameKey: "QZ", + CBAdvertisementDataServiceUUIDsKey: [heartRateServiceUUID, FitnessMachineServiceUuid, CSCServiceUUID, PowerServiceUUID]] as [String : Any] + peripheralManager.startAdvertising(advertisementData) + } - let advertisementData = [CBAdvertisementDataLocalNameKey: "QZ", - CBAdvertisementDataServiceUUIDsKey: [heartRateServiceUUID, FitnessMachineServiceUuid, CSCServiceUUID, PowerServiceUUID]] as [String : Any] - peripheralManager.startAdvertising(advertisementData) print("Successfully added service") } @@ -321,6 +331,10 @@ class BLEPeripheralManagerZwift: NSObject, CBPeripheralManagerDelegate { request.value = self.calculateIndoorBike() self.peripheralManager.respond(to: request, withResult: .success) print("Responded successfully to a read request") + } else if request.characteristic == self.PowerMeasurementCharacteristic { + request.value = self.calculatePower() + self.peripheralManager.respond(to: request, withResult: .success) + print("Responded successfully to a read request") } } diff --git a/src/keepbike.cpp b/src/keepbike.cpp index 06ef7d425..2138b660d 100644 --- a/src/keepbike.cpp +++ b/src/keepbike.cpp @@ -1,6 +1,4 @@ #include "keepbike.h" -#include "ios/lockscreen.h" -#include "keepawakehelper.h" #include "virtualbike.h" #include #include @@ -10,6 +8,10 @@ #include #include +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif + using namespace std::chrono_literals; #ifdef Q_OS_IOS @@ -59,11 +61,15 @@ void keepbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -212,14 +218,17 @@ void keepbike::characteristicChanged(const QLowEnergyCharacteristic &characteris Speed = ((uint8_t)newValue.at(18)); } else*/ { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } m_watt = GetWattFromPacket(newValue); if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -241,23 +250,15 @@ void keepbike::characteristicChanged(const QLowEnergyCharacteristic &characteris #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -368,7 +369,7 @@ void keepbike::stateChanged(QLowEnergyService::ServiceState state) { &keepbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -376,11 +377,14 @@ void keepbike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -390,10 +394,11 @@ void keepbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&keepbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &keepbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -487,10 +492,6 @@ bool keepbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *keepbike::VirtualBike() { return virtualBike; } - -void *keepbike::VirtualDevice() { return VirtualBike(); } - uint16_t keepbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/keepbike.h b/src/keepbike.h index 543220e9e..6267b69f7 100644 --- a/src/keepbike.h +++ b/src/keepbike.h @@ -37,12 +37,9 @@ class keepbike : public bike { Q_OBJECT public: keepbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: const resistance_t max_resistance = 36; @@ -57,10 +54,9 @@ class keepbike : public bike { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/kingsmithr1protreadmill.cpp b/src/kingsmithr1protreadmill.cpp index 88686f00b..04609e236 100644 --- a/src/kingsmithr1protreadmill.cpp +++ b/src/kingsmithr1protreadmill.cpp @@ -1,6 +1,9 @@ #include "kingsmithr1protreadmill.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -52,15 +55,19 @@ void kingsmithr1protreadmill::writeCharacteristic(uint8_t *data, uint8_t data_le return; } + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::Write) - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); else - gattCommunicationChannelService->writeCharacteristic( - gattWriteCharacteristic, QByteArray((const char *)data, data_len), QLowEnergyService::WriteWithoutResponse); + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info + " " + gattWriteCharacteristic.properties()); } @@ -104,7 +111,7 @@ void kingsmithr1protreadmill::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill && !virtualBike) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -113,13 +120,15 @@ void kingsmithr1protreadmill::update() { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &kingsmithr1protreadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &kingsmithr1protreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstInit = 1; } @@ -307,20 +316,10 @@ void kingsmithr1protreadmill::characteristicChanged(const QLowEnergyCharacterist uint8_t heart = 0; if (heart == 0) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif - } else - + update_hr_from_external(); + } else { Heart = heart; + } } } @@ -542,10 +541,6 @@ bool kingsmithr1protreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *kingsmithr1protreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *kingsmithr1protreadmill::VirtualDevice() { return VirtualTreadMill(); } - void kingsmithr1protreadmill::searchingStop() { searchStopped = true; } bool kingsmithr1protreadmill::autoPauseWhenSpeedIsZero() { diff --git a/src/kingsmithr1protreadmill.h b/src/kingsmithr1protreadmill.h index d044574e6..f0a51a6c0 100644 --- a/src/kingsmithr1protreadmill.h +++ b/src/kingsmithr1protreadmill.h @@ -28,8 +28,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -41,13 +39,11 @@ class kingsmithr1protreadmill : public treadmill { public: kingsmithr1protreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); + bool connected() override; - void *VirtualTreadMill(); - void *VirtualDevice(); - - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; + double minStepSpeed() override { return 0.1; } private: typedef enum K1_VERSION { CLASSIC = 0, RE = 1 } K1_VERSION; @@ -75,8 +71,6 @@ class kingsmithr1protreadmill : public treadmill { bool firstCharacteristicChanged = true; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/kingsmithr2treadmill.cpp b/src/kingsmithr2treadmill.cpp index 8d132bbc5..22d18a34c 100644 --- a/src/kingsmithr2treadmill.cpp +++ b/src/kingsmithr2treadmill.cpp @@ -1,6 +1,8 @@ #include "kingsmithr2treadmill.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -63,6 +65,8 @@ void kingsmithr2treadmill::writeCharacteristic(const QString &data, const QStrin encrypted.append(ENCRYPT_TABLE_v3[idx]); else if (settings.value(QZSettings::kingsmith_encrypt_v4, QZSettings::default_kingsmith_encrypt_v4).toBool()) encrypted.append(ENCRYPT_TABLE_v4[idx]); + else if (settings.value(QZSettings::kingsmith_encrypt_v5, QZSettings::default_kingsmith_encrypt_v5).toBool()) + encrypted.append(ENCRYPT_TABLE_v5[idx]); else encrypted.append(ENCRYPT_TABLE[idx]); } @@ -73,6 +77,7 @@ void kingsmithr2treadmill::writeCharacteristic(const QString &data, const QStrin } encrypted.append('\x0d'); for (int i = 0; i < encrypted.length(); i += 16) { + // it's missing the writeBuffer here, it could lead to crash on iOS gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, encrypted.mid(i, 16), QLowEnergyService::WriteWithoutResponse); } @@ -119,19 +124,25 @@ void kingsmithr2treadmill::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstInit && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); + if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &kingsmithr2treadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &kingsmithr2treadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstInit = 1; } @@ -170,7 +181,7 @@ void kingsmithr2treadmill::update() { requestSpeed = -1; } if (requestInclination != -100) { - if(requestInclination < 0) + if (requestInclination < 0) requestInclination = 0; // only 0.5 steps ara available requestInclination = qRound(requestInclination * 2.0) / 2.0; @@ -256,6 +267,8 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic idx = ENCRYPT_TABLE_v3.indexOf(ch); else if (settings.value(QZSettings::kingsmith_encrypt_v4, QZSettings::default_kingsmith_encrypt_v4).toBool()) idx = ENCRYPT_TABLE_v4.indexOf(ch); + else if (settings.value(QZSettings::kingsmith_encrypt_v5, QZSettings::default_kingsmith_encrypt_v5).toBool()) + idx = ENCRYPT_TABLE_v5.indexOf(ch); else idx = ENCRYPT_TABLE.indexOf(ch); decrypted.append(PLAINTEXT_TABLE[idx]); @@ -280,7 +293,7 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic if (!key.compare(QStringLiteral("mcu_version")) || !key.compare(QStringLiteral("goal"))) { continue; } - if(i+1 >= _props.count()) { + if (i + 1 >= _props.count()) { qDebug() << "error decoding" << i; return; } @@ -309,27 +322,18 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic uint8_t heart = 0; if (heart == 0) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif - } else - + update_hr_from_external(); + } else { Heart = heart; + } } } if (!firstCharacteristicChanged) { if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) KCal += - ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) * + ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo( @@ -399,6 +403,12 @@ void kingsmithr2treadmill::stateChanged(QLowEnergyService::ServiceState state) { QBluetoothUuid _gattWriteCharacteristicId((quint16)0xFED7); QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xFED8); + + if (KS_NACH_X21C) { + _gattWriteCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED7-0000-1000-8000-00805f9b34fb")); + _gattNotifyCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED8-0000-1000-8000-00805f9b34fb")); + } + QMetaEnum metaEnum = QMetaEnum::fromType(); emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); if (state == QLowEnergyService::DiscoveringServices) { @@ -448,6 +458,9 @@ void kingsmithr2treadmill::serviceScanDone(void) { QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0x1234); emit debug(QStringLiteral("serviceScanDone")); + if (KS_NACH_X21C) + _gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00021234-0000-1000-8000-00805f9b34fb")); + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &kingsmithr2treadmill::stateChanged); @@ -472,6 +485,10 @@ void kingsmithr2treadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { bluetoothDevice = device; + if (device.name().toUpper().startsWith(QStringLiteral("KS-NACH-X21C"))) { + qDebug() << "KS-NACH-X21C workaround!"; + KS_NACH_X21C = true; + } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &kingsmithr2treadmill::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &kingsmithr2treadmill::serviceScanDone); @@ -525,8 +542,4 @@ bool kingsmithr2treadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *kingsmithr2treadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *kingsmithr2treadmill::VirtualDevice() { return VirtualTreadMill(); } - void kingsmithr2treadmill::searchingStop() { searchStopped = true; } diff --git a/src/kingsmithr2treadmill.h b/src/kingsmithr2treadmill.h index 1412666b1..0c63b8d63 100644 --- a/src/kingsmithr2treadmill.h +++ b/src/kingsmithr2treadmill.h @@ -29,8 +29,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -42,11 +40,8 @@ class kingsmithr2treadmill : public treadmill { public: kingsmithr2treadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - - void *VirtualTreadMill(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + bool connected() override; + virtual bool canStartStop() override { return false; } private: const QByteArray PLAINTEXT_TABLE = @@ -59,6 +54,8 @@ class kingsmithr2treadmill : public treadmill { QStringLiteral("0aCw4FGHIJqLhN+P9RVTU/WcY6ObDdefgEijklmnopQrsBuvMxXz1yA2t5Z78KS3=").toUtf8(); const QByteArray ENCRYPT_TABLE_v4 = QStringLiteral("ZaCw4FGHIJqLhN9P+RVTU/WcY6ObDdefgEijklmnopQrsBuvMxXz1yA2t5078KS3=").toUtf8(); + const QByteArray ENCRYPT_TABLE_v5 = + QStringLiteral("iaCw4FGHIJqLhN+P9RVTU/WcY6ObDdefgEZjklmnopQrsBuvMxXz1yA2t5078KS3=").toUtf8(); double GetInclinationFromPacket(const QByteArray &packet); double GetKcalFromPacket(const QByteArray &packet); @@ -82,8 +79,6 @@ class kingsmithr2treadmill : public treadmill { bool firstCharacteristicChanged = true; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; @@ -92,6 +87,8 @@ class kingsmithr2treadmill : public treadmill { bool initDone = false; bool initRequest = false; + bool KS_NACH_X21C = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/lifefitnesstreadmill.cpp b/src/lifefitnesstreadmill.cpp index ed674fd8b..b576da836 100644 --- a/src/lifefitnesstreadmill.cpp +++ b/src/lifefitnesstreadmill.cpp @@ -57,10 +57,18 @@ void lifefitnesstreadmill::writeCharacteristic(QLowEnergyService *service, QLowE timeout.singleShot(3000, &loop, SLOT(quit())); } - service->writeCharacteristic(characteristic, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (characteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) + service->writeCharacteristic(characteristic, *writeBuffer, QLowEnergyService::WriteWithoutResponse); + else + service->writeCharacteristic(characteristic, *writeBuffer); if (!disable_log) - qDebug() << " >> " << QByteArray((const char *)data, data_len).toHex(' ') << " // " << info; + qDebug() << " >> " << writeBuffer->toHex(' ') << " // " << info; loop.exec(); } @@ -81,32 +89,43 @@ void lifefitnesstreadmill::btinit() { uint8_t initData3[1] = {0x00}; uint8_t initData4[7] = {0x00, 0x00, 0x00, 0x01, 0xb8, 0x5b, 0x5d}; uint8_t initData5[1] = {0x02}; - writeCharacteristic(gattCustomService1, gattWriteChar1CustomService1, initData1, sizeof(initData1), - QStringLiteral("init"), false, false); - writeCharacteristic(gattCustomService2, gattWriteChar3CustomService2, initData2a, sizeof(initData2a), - QStringLiteral("init"), false, false); - writeCharacteristic(gattCustomService2, gattWriteChar3CustomService2, initData2b, sizeof(initData2b), - QStringLiteral("init"), false, false); - writeCharacteristic(gattCustomService2, gattWriteChar4CustomService2, initData3, sizeof(initData3), - QStringLiteral("init"), false, false); - writeCharacteristic(gattCustomService1, gattWriteChar2CustomService1, initData4, sizeof(initData4), - QStringLiteral("init"), false, false); - writeCharacteristic(gattCustomService1, gattWriteChar1CustomService1, initData5, sizeof(initData5), - QStringLiteral("init"), false, false); + + if (gattWriteChar4CustomService2.isValid()) { + + writeCharacteristic(gattCustomService1, gattWriteChar1CustomService1, initData1, sizeof(initData1), + QStringLiteral("init"), false, false); + writeCharacteristic(gattCustomService2, gattWriteChar3CustomService2, initData2a, sizeof(initData2a), + QStringLiteral("init"), false, false); + writeCharacteristic(gattCustomService2, gattWriteChar3CustomService2, initData2b, sizeof(initData2b), + QStringLiteral("init"), false, false); + writeCharacteristic(gattCustomService2, gattWriteChar4CustomService2, initData3, sizeof(initData3), + QStringLiteral("init"), false, false); + writeCharacteristic(gattCustomService1, gattWriteChar2CustomService1, initData4, sizeof(initData4), + QStringLiteral("init"), false, false); + writeCharacteristic(gattCustomService1, gattWriteChar1CustomService1, initData5, sizeof(initData5), + QStringLiteral("init"), false, false); + } QByteArray descriptor; QBluetoothUuid _gattTreadmillDataId((quint16)0x2ACD); QBluetoothUuid _gattTrainingStatusId((quint16)0x2AD3); + QBluetoothUuid _gattCrossTrainerDataId((quint16)0x2ACE); QLowEnergyCharacteristic gattTreadmillData = gattFTMSService->characteristic(_gattTreadmillDataId); QLowEnergyCharacteristic gattTrainingStatus = gattFTMSService->characteristic(_gattTrainingStatusId); + QLowEnergyCharacteristic gattCrossTrainerData = gattFTMSService->characteristic(_gattCrossTrainerDataId); descriptor.append((char)0x01); descriptor.append((char)0x00); gattFTMSService->writeDescriptor(gattTrainingStatus.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); - gattFTMSService->writeDescriptor(gattTreadmillData.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), - descriptor); - gattFTMSService->writeDescriptor(gattTreadmillData.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), - descriptor); + if (gattTreadmillData.isValid()) { + gattFTMSService->writeDescriptor( + gattTreadmillData.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + gattFTMSService->writeDescriptor( + gattTreadmillData.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else if (gattCrossTrainerData.isValid()) { + gattFTMSService->writeDescriptor( + gattCrossTrainerData.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } initDone = true; } @@ -155,7 +174,7 @@ void lifefitnesstreadmill::update() { requestInclination = 0; else { // the treadmill accepts only .5 steps - requestInclination = floor(requestInclination) + 0.5; + requestInclination = std::llround(requestInclination * 2) / 2.0; } if (requestInclination != currentInclination().value() && requestInclination >= 0 && requestInclination <= 15) { @@ -579,19 +598,8 @@ void lifefitnesstreadmill::characteristicChanged(const QLowEnergyCharacteristic if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { if (heart == 0.0 || settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { - Heart = heart; } } @@ -690,7 +698,7 @@ void lifefitnesstreadmill::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -707,15 +715,17 @@ void lifefitnesstreadmill::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &lifefitnesstreadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &lifefitnesstreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &lifefitnesstreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } } firstStateChanged = 1; @@ -831,10 +841,6 @@ bool lifefitnesstreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *lifefitnesstreadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *lifefitnesstreadmill::VirtualDevice() { return VirtualTreadmill(); } - void lifefitnesstreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/lifefitnesstreadmill.h b/src/lifefitnesstreadmill.h index 9303b26a9..d50e2c1c6 100644 --- a/src/lifefitnesstreadmill.h +++ b/src/lifefitnesstreadmill.h @@ -40,15 +40,12 @@ class lifefitnesstreadmill : public treadmill { Q_OBJECT public: lifefitnesstreadmill(bool noWriteResistance, bool noHeartService); - bool connected(); + bool connected() override; void forceSpeed(double requestSpeed); void forceIncline(double requestIncline); - double minStepInclination(); - double minStepSpeed(); - - void *VirtualTreadmill(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + double minStepInclination() override; + double minStepSpeed() override; + virtual bool canStartStop() override{ return false; } private: void writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic characteristic, uint8_t *data, @@ -58,8 +55,6 @@ class lifefitnesstreadmill : public treadmill { void btinit(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; diff --git a/src/localipaddress.cpp b/src/localipaddress.cpp new file mode 100644 index 000000000..478bf4c64 --- /dev/null +++ b/src/localipaddress.cpp @@ -0,0 +1,124 @@ +#include "localipaddress.h" +#include + +#ifdef Q_OS_ANDROID +#include +#include +#include +#endif + +#ifdef Q_OS_ANDROID + +/* + * Get WifiManager object + * Parameters: jCtxObj is Context object + */ +jobject getWifiManagerObj(JNIEnv *env, jobject jCtxObj) { + qDebug() << "gotWifiMangerObj "; + // Get the value of Context.WIFI_SERVICE + // jstring jstr_wifi_service = env->NewStringUTF("wifi"); + jclass jCtxClz = env->FindClass("android/content/Context"); + jfieldID fid_wifi_service = env->GetStaticFieldID(jCtxClz, "WIFI_SERVICE", "Ljava/lang/String;"); + jstring jstr_wifi_service = (jstring)env->GetStaticObjectField(jCtxClz, fid_wifi_service); + + jclass jclz = env->GetObjectClass(jCtxObj); + jmethodID mid_getSystemService = + env->GetMethodID(jclz, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + jobject wifiManager = env->CallObjectMethod(jCtxObj, mid_getSystemService, jstr_wifi_service); + + // Because jclass inherits from jobject, it needs to be released; + // jfieldID and jmethodID are memory addresses, this memory is not allocated in our code, and we don’t need to + // release it. + env->DeleteLocalRef(jCtxClz); + env->DeleteLocalRef(jclz); + env->DeleteLocalRef(jstr_wifi_service); + + return wifiManager; +} + +/* + * Get the WifiInfo object + * Parameters: wifiMgrObj is the WifiManager object + */ +jobject getWifiInfoObj(JNIEnv *env, jobject wifiMgrObj) { + qDebug() << "getWifiInfoObj "; + if (wifiMgrObj == NULL) { + return NULL; + } + jclass jclz = env->GetObjectClass(wifiMgrObj); + jmethodID mid = env->GetMethodID(jclz, "getConnectionInfo", "()Landroid/net/wifi/WifiInfo;"); + jobject wifiInfo = env->CallObjectMethod(wifiMgrObj, mid); + + env->DeleteLocalRef(jclz); + return wifiInfo; +} + +/* + * Get MAC address + * Parameters: wifiInfoObj, WifiInfo object + */ +char *getMacAddress(JNIEnv *env, jobject wifiInfoObj) { + qDebug() << "getMacAddress.... "; + if (wifiInfoObj == NULL) { + return NULL; + } + jclass jclz = env->GetObjectClass(wifiInfoObj); + jmethodID mid = env->GetMethodID(jclz, "getMacAddress", "()Ljava/lang/String;"); + jstring jstr_mac = (jstring)env->CallObjectMethod(wifiInfoObj, mid); + if (jstr_mac == NULL) { + env->DeleteLocalRef(jclz); + return NULL; + } + + const char *tmp = env->GetStringUTFChars(jstr_mac, NULL); + char *mac = (char *)malloc(strlen(tmp) + 1); + memcpy(mac, tmp, strlen(tmp) + 1); + env->ReleaseStringUTFChars(jstr_mac, tmp); + env->DeleteLocalRef(jclz); + return mac; +} + +/* + * Get MAC address + * Parameters: wifiInfoObj, WifiInfo object + */ +int getIpAddress(JNIEnv *env, jobject wifiInfoObj) { + qDebug() << "getIpAddress.... "; + if (wifiInfoObj == NULL) { + return NULL; + } + jclass jclz = env->GetObjectClass(wifiInfoObj); + jmethodID mid = env->GetMethodID(jclz, "getIpAddress", "()I"); + return env->CallIntMethod(wifiInfoObj, mid); +} +#endif + +QHostAddress localipaddress::getIP(const QHostAddress &srcAddress) { + // Attempt to find the interface that corresponds with the provided + // address and determine this device's address from the interface + + const auto interfaces = QNetworkInterface::allInterfaces(); + for (const QNetworkInterface &networkInterface : interfaces) { + const auto entries = networkInterface.addressEntries(); + for (const QNetworkAddressEntry &entry : entries) { + if (srcAddress.isInSubnet(entry.ip(), entry.prefixLength())) { + for (const QNetworkAddressEntry &newEntry : entries) { + QHostAddress address = newEntry.ip(); + if ((address.protocol() == QAbstractSocket::IPv4Protocol && !address.isLoopback())) { + return address; + } + } + } + } + } +#ifdef Q_OS_ANDROID + QAndroidJniEnvironment env; + jobject wifiManagerObj = getWifiManagerObj(env, QtAndroid::androidContext().object()); + jobject wifiInfoObj = getWifiInfoObj(env, wifiManagerObj); + int ip = getIpAddress(env, wifiInfoObj); + QHostAddress qip = QHostAddress(qFromBigEndian(ip)); + qDebug() << "getIP from JNI" << qip; + return qip; +#endif + return QHostAddress(); +} diff --git a/src/localipaddress.h b/src/localipaddress.h new file mode 100644 index 000000000..5823b53eb --- /dev/null +++ b/src/localipaddress.h @@ -0,0 +1,13 @@ +#ifndef LOCALIPADDRESS_H +#define LOCALIPADDRESS_H + +#include +#include + +class localipaddress +{ +public: + static QHostAddress getIP(const QHostAddress &srcAddress); +}; + +#endif // LOCALIPADDRESS_H diff --git a/src/m3ibike.cpp b/src/m3ibike.cpp index 8cb1f8052..0618ec244 100644 --- a/src/m3ibike.cpp +++ b/src/m3ibike.cpp @@ -1,6 +1,7 @@ #include "m3ibike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -299,9 +300,6 @@ m3ibike::~m3ibike() { elapsedTimer->stop(); delete elapsedTimer; } - if (virtualBike) { - delete virtualBike; - } m_instance = 0; disconnecting = true; #if defined(Q_OS_ANDROID) @@ -616,7 +614,7 @@ void m3ibike::processAdvertising(const QByteArray &data) { detectDisc->start(M3i_DISCONNECT_THRESHOLD); if (!initDone) { initDone = true; - if (!virtualBike + if (!this->hasVirtualDevice() #if defined(Q_OS_IOS) && !defined(IO_UNDER_QT) && !h #endif @@ -633,9 +631,10 @@ void m3ibike::processAdvertising(const QByteArray &data) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike, &virtualbike::debug, this, &m3ibike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &m3ibike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } int buffSize = settings.value(QZSettings::m3i_bike_speed_buffsize, QZSettings::default_m3i_bike_speed_buffsize).toInt(); k3s.inner_reset(buffSize, @@ -724,6 +723,9 @@ void m3ibike::processAdvertising(const QByteArray &data) { long appleWatchHeartRate = h->heartRate(); h->setKcal(KCal.value()); h->setDistance(Distance.value()); + h->setSpeed(Speed.value()); + h->setPower(m_watt.value()); + h->setCadence(Cadence.value()); if (appleWatchHeartRate == 0) Heart = k3.pulse; else @@ -765,10 +767,6 @@ void m3ibike::deviceDiscovered(const QBluetoothDeviceInfo &device) { bool m3ibike::connected() { return initDone; } -void *m3ibike::VirtualBike() { return virtualBike; } - -void *m3ibike::VirtualDevice() { return VirtualBike(); } - uint16_t m3ibike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/m3ibike.h b/src/m3ibike.h index a016ff296..f2557a1f2 100644 --- a/src/m3ibike.h +++ b/src/m3ibike.h @@ -28,7 +28,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/M3iIOS-Interface.h" @@ -141,10 +140,7 @@ class m3ibike : public bike { public: m3ibike(bool noWriteResistance, bool noHeartService); virtual ~m3ibike(); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; static bool parse_data(const QByteArray &data, keiser_m3i_out_t *f); static bool valid_id(int id); static bool isCorrectUnit(const QBluetoothDeviceInfo &device); @@ -163,7 +159,7 @@ class m3ibike : public bike { void initScan(); Q_INVOKABLE void processAdvertising(const QByteArray &data); Q_INVOKABLE void restartScan(); - uint16_t watts(); + uint16_t watts() override; QTimer *detectDisc = nullptr, *elapsedTimer = nullptr; KeiserM3iDeviceSimulator k3s; keiser_m3i_out_t k3; @@ -171,8 +167,6 @@ class m3ibike : public bike { int lastTimerRestartOffset = 0; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); - virtualbike *virtualBike = nullptr; - bool firstUpdate = true; bool initDone = false; diff --git a/src/main.cpp b/src/main.cpp index 790118ae0..ad68224b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,6 +38,8 @@ #include "ios/lockscreen.h" #endif +#include "handleurl.h" + bool logs = true; bool noWriteResistance = false; bool noHeartService = true; @@ -51,6 +53,7 @@ QString peloton_username = ""; QString peloton_password = ""; QString pzp_username = ""; QString pzp_password = ""; +bool fit_file_saved_on_quit = false; bool testResistance = false; bool forceQml = true; bool miles = false; @@ -173,6 +176,9 @@ QCoreApplication *createApplication(int &argc, char *argv[]) { bikeResistanceOffset = atoi(argv[++i]); } + if (!qstrcmp(argv[i], "-fit-file-saved-on-quit")) { + fit_file_saved_on_quit = true; + } if (!qstrcmp(argv[i], "-profile")) { QString profileName = argv[++i]; if (QFile::exists(homeform::getProfileDir() + "/" + profileName + ".qzs")) { @@ -277,9 +283,6 @@ void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QS int main(int argc, char *argv[]) { -#ifdef Q_OS_ANDROID - qputenv("QT_ANDROID_VOLUME_KEYS", "1"); // "1" is dummy -#endif #ifdef Q_OS_WIN32 qputenv("QT_MULTIMEDIA_PREFERRED_PLUGINS", "windowsmediafoundation"); #endif @@ -287,6 +290,11 @@ int main(int argc, char *argv[]) { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) QScopedPointer app(createApplication(argc, argv)); #else +#ifdef Q_OS_IOS + HandleURL *URLHandler = new HandleURL(); + QDesktopServices::setUrlHandler("org.cagnulein.ConnectIQComms-ciq", URLHandler, "handleURL"); +#endif + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QScopedPointer app(new QApplication(argc, argv)); #endif @@ -315,6 +323,12 @@ int main(int argc, char *argv[]) { homeform::loadSettings(profileToLoad); } + if (fit_file_saved_on_quit) { + settings.setValue(QZSettings::fit_file_saved_on_quit, true); + qDebug() << "fit_file_saved_on_quit" + << settings.value(QZSettings::fit_file_saved_on_quit, QZSettings::default_fit_file_saved_on_quit); + } + if (forceQml) #endif { @@ -335,6 +349,7 @@ int main(int argc, char *argv[]) { bikeResistanceOffset = settings.value(QZSettings::bike_resistance_offset, bikeResistanceOffset).toInt(); bikeResistanceGain = settings.value(QZSettings::bike_resistance_gain_f, bikeResistanceGain).toDouble(); deviceName = settings.value(QZSettings::filter_device, QZSettings::default_filter_device).toString(); + pollDeviceTime = settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt(); } #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) else { @@ -352,6 +367,13 @@ int main(int argc, char *argv[]) { } #endif +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::volume_change_gears, QZSettings::default_volume_change_gears).toBool()) { + qDebug() << "handling volume keys"; + qputenv("QT_ANDROID_VOLUME_KEYS", "1"); // "1" is dummy + } +#endif + qInstallMessageHandler(myMessageOutput); qDebug() << QStringLiteral("version ") << app->applicationVersion(); foreach (QString s, settings.allKeys()) { diff --git a/src/main.qml b/src/main.qml index 84df837c5..81c912dce 100644 --- a/src/main.qml +++ b/src/main.qml @@ -6,6 +6,8 @@ import QtGraphicalEffects 1.12 import Qt.labs.settings 1.0 import QtMultimedia 5.15 import org.cagnulein.qdomyoszwift 1.0 +import QtQuick.Window 2.12 +import Qt.labs.platform 1.1 ApplicationWindow { id: window @@ -35,12 +37,16 @@ ApplicationWindow { signal keyMediaPrevious() signal keyMediaNext() signal floatingOpen() + signal openFloatingWindowBrowser(); property bool lockTiles: false + property bool settings_restart_to_apply: false Settings { id: settings - property string profile_name: "default" + property string profile_name: "default" + property string theme_status_bar_background_color: "#800080" + property bool volume_change_gears: false } Store { @@ -84,6 +90,67 @@ ApplicationWindow { } } + ToastManager { + id: toast + } + + Timer { + interval: 1 + repeat: false + running: (rootItem.toastRequested !== "") + onTriggered: { + toast.show(rootItem.toastRequested); + rootItem.toastRequested = ""; + } + } + + /* + Timer { + interval: 1000 + repeat: true + running: true + property int i: 0 + onTriggered: { + toast.show("This timer has triggered " + (++i) + " times!"); + } + } + + Timer { + interval: 3000 + repeat: true + running: true + property int i: 0 + onTriggered: { + toast.show("This important message has been shown " + (++i) + " times.", 5000); + } + }*/ + + Keys.onBackPressed: { + if(OS_VERSION === "Android") { + toast.show("Pressed it quickly to close the app!") + timer.pressBack(); + } + } + Timer{ + id: timer + + property bool backPressed: false + repeat: false + interval: 200//ms + onTriggered: backPressed = false + function pressBack(){ + if(backPressed){ + timer.stop() + backPressed = false + Qt.callLater(Qt.quit) + } + else{ + backPressed = true + timer.start() + } + } + } + Popup { id: popup parent: Overlay.overlay @@ -313,18 +380,33 @@ ApplicationWindow { } } + MessageDialog { + id: popupRestartApp + text: "Settings changed" + informativeText: "In order to apply the changes you need to restart the app.\nDo you want to do it now?" + buttons: (MessageDialog.Yes | MessageDialog.No) + onYesClicked: Qt.callLater(Qt.quit) + onNoClicked: this.visible = false; + visible: false + } + header: ToolBar { contentHeight: toolButton.implicitHeight - Material.primary: Material.Purple + Material.primary: settings.theme_status_bar_background_color id: headerToolbar ToolButton { id: toolButton icon.source: "icons/icons/icon.png" - text: stackView.depth > 1 ? "⏴" : "⏴" + text: stackView.depth > 1 ? "◄" : "◄" font.pixelSize: Qt.application.font.pixelSize * 1.6 onClicked: { if (stackView.depth > 1) { + if(window.settings_restart_to_apply === true) { + window.settings_restart_to_apply = false; + popupRestartApp.visible = true; + } + stackView.pop() toolButtonLoadSettings.visible = false; toolButtonSaveSettings.visible = false; @@ -466,13 +548,13 @@ ApplicationWindow { id: toolButtonMaps icon.source: ( "icons/icons/maps-icon-16.png" ) onClicked: { loadMaps(); } - anchors.right: toolButtonLockTiles.left + anchors.right: toolButtonChart.left visible: rootItem.mapsVisible } ToolButton { function loadVideo() { - if(rootItem.currentCoordinateValid) { + if(rootItem.currentCoordinateValid || rootItem.trainProgramLoadedWithVideo) { console.log("coordinate is valid for map"); //stackView.push("videoPlayback.qml"); rootItem.videoVisible = !rootItem.videoVisible @@ -487,6 +569,14 @@ ApplicationWindow { visible: rootItem.videoIconVisible } + ToolButton { + id: toolButtonChart + icon.source: ( "icons/icons/chart.png" ) + onClicked: { rootItem.chartFooterVisible = !rootItem.chartFooterVisible } + anchors.right: toolButtonLockTiles.left + visible: rootItem.chartIconVisible + } + ToolButton { id: toolButtonLockTiles icon.source: ( window.lockTiles ? "icons/icons/unlock.png" : "icons/icons/lock.png") @@ -618,16 +708,6 @@ ApplicationWindow { popupSaveFile.open() } } - ItemDelegate { - id: strava_connect - text: qsTr("Connect to Strava") - width: parent.width - onClicked: { - stackView.push("WebStravaAuth.qml") - strava_connect_clicked() - drawer.close() - } - } ItemDelegate { id: help text: qsTr("Help") @@ -665,9 +745,28 @@ ApplicationWindow { } ItemDelegate { - text: "version 2.13.4" + text: "version 2.16.22" width: parent.width } + + ItemDelegate { + id: strava_connect + Image { + anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter + source: "icons/icons/btn_strava_connectwith_orange.png" + fillMode: Image.PreserveAspectFit + visible: true + width: parent.width + } + width: parent.width + onClicked: { + stackView.push("WebStravaAuth.qml") + strava_connect_clicked() + drawer.close() + } + } + FileDialog { id: fileDialogGPX title: "Please choose a file" @@ -691,13 +790,15 @@ ApplicationWindow { initialItem: "Home.qml" anchors.fill: parent focus: true - Keys.onVolumeUpPressed: { console.log("onVolumeUpPressed"); volumeUp(); } - Keys.onVolumeDownPressed: { console.log("onVolumeDownPressed"); volumeDown(); } - Keys.onPressed: { + Keys.onVolumeUpPressed: (event)=> { console.log("onVolumeUpPressed"); volumeUp(); event.accepted = settings.volume_change_gears; } + Keys.onVolumeDownPressed: (event)=> { console.log("onVolumeDownPressed"); volumeDown(); event.accepted = settings.volume_change_gears; } + Keys.onPressed: (event)=> { if (event.key === Qt.Key_MediaPrevious) keyMediaPrevious(); else if (event.key === Qt.Key_MediaNext) keyMediaNext(); + + event.accepted = settings.volume_change_gears; } } } diff --git a/src/mcfbike.cpp b/src/mcfbike.cpp index 470af0d13..3fe120d43 100644 --- a/src/mcfbike.cpp +++ b/src/mcfbike.cpp @@ -1,6 +1,7 @@ #include "mcfbike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -58,11 +59,15 @@ void mcfbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -203,7 +208,9 @@ void mcfbike::characteristicChanged(const QLowEnergyCharacteristic &characterist if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = (((uint16_t)newValue.at(11) << 8) | (uint16_t)((uint8_t)newValue.at(12))) / 10.0; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } Distance += ((Speed.value() / 3600000.0) * @@ -212,8 +219,8 @@ void mcfbike::characteristicChanged(const QLowEnergyCharacteristic &characterist m_watt = (((uint16_t)newValue.at(9) << 8) | (uint16_t)((uint8_t)newValue.at(10))); if (watts()) - KCal += ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * - 3.5) / + KCal += ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); @@ -241,23 +248,15 @@ void mcfbike::characteristicChanged(const QLowEnergyCharacteristic &characterist #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -335,7 +334,7 @@ void mcfbike::stateChanged(QLowEnergyService::ServiceState state) { &mcfbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -343,11 +342,14 @@ void mcfbike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -357,10 +359,11 @@ void mcfbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&mcfbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &mcfbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -453,10 +456,6 @@ bool mcfbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *mcfbike::VirtualBike() { return virtualBike; } - -void *mcfbike::VirtualDevice() { return VirtualBike(); } - uint16_t mcfbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/mcfbike.h b/src/mcfbike.h index 5671afeb8..ff010d55f 100644 --- a/src/mcfbike.h +++ b/src/mcfbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,13 +36,10 @@ class mcfbike : public bike { Q_OBJECT public: mcfbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: const resistance_t max_resistance = 14; @@ -56,10 +52,9 @@ class mcfbike : public bike { bool wait_for_response = false); void startDiscover(); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/mepanelbike.cpp b/src/mepanelbike.cpp index f1d794096..e0d51ec8a 100644 --- a/src/mepanelbike.cpp +++ b/src/mepanelbike.cpp @@ -59,11 +59,15 @@ void mepanelbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -248,16 +252,7 @@ void mepanelbike::characteristicChanged(const QLowEnergyCharacteristic &characte #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled")) && disable_hr_frommachinery) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } @@ -334,7 +329,7 @@ void mepanelbike::stateChanged(QLowEnergyService::ServiceState state) { &mepanelbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -359,10 +354,11 @@ void mepanelbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&mepanelbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &mepanelbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -456,10 +452,6 @@ bool mepanelbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *mepanelbike::VirtualBike() { return virtualBike; } - -void *mepanelbike::VirtualDevice() { return VirtualBike(); } - uint16_t mepanelbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/mepanelbike.h b/src/mepanelbike.h index d83481dfc..2be77c488 100644 --- a/src/mepanelbike.h +++ b/src/mepanelbike.h @@ -37,10 +37,7 @@ class mepanelbike : public bike { Q_OBJECT public: mepanelbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: const resistance_t max_resistance = 32; @@ -49,11 +46,10 @@ class mepanelbike : public bike { bool wait_for_response = false); void startDiscover(); void forceResistance(resistance_t requestResistance); - uint16_t watts(); + uint16_t watts() override; uint8_t getCheckNum(uint8_t i, uint8_t i2); QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/metric.cpp b/src/metric.cpp index dff65ed66..31c174567 100644 --- a/src/metric.cpp +++ b/src/metric.cpp @@ -25,12 +25,10 @@ void metric::setValue(double v, bool applyGainAndOffset) { } v *= settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble(); } - if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() < 0) { - if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() != 0.0) { - qDebug() - << QStringLiteral("watt value was ") << v << QStringLiteral("but it will be transformed to") - << v + settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); - } + if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() != 0.0) { + qDebug() + << QStringLiteral("watt value was ") << v << QStringLiteral("but it will be transformed to") + << v + settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); v += settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); } } @@ -43,7 +41,8 @@ void metric::setValue(double v, bool applyGainAndOffset) { } QDateTime now = QDateTime::currentDateTime(); - if (v != m_value) { + if (v != m_value && v != INFINITY) { + m_valueChanged = now; if (m_last5.count() > 1) { double diff = v - m_value; double diffFromLastValue = qAbs(now.msecsTo(m_lastChanged)); @@ -63,7 +62,7 @@ void metric::setValue(double v, bool applyGainAndOffset) { return; } - if (value() != 0) { + if (value() != 0 && value() != INFINITY) { m_countValue++; m_lapCountValue++; m_totValue += value(); @@ -80,14 +79,14 @@ void metric::setValue(double v, bool applyGainAndOffset) { if (value() < m_lapMin) { m_lapMin = value(); } - } - if (value() > m_max) { - m_max = value(); - } + if (value() > m_max) { + m_max = value(); + } - if (value() > m_lapMax) { - m_lapMax = value(); + if (value() > m_lapMax) { + m_lapMax = value(); + } } } @@ -283,20 +282,18 @@ struct CompareBests { } }; -// VO2 (L/min) = 0.0108 x power (W) + 0.007 x body mass (kg) -// power = 5 min peak power for a specific ride -double metric::calculateVO2Max(QList *session) { +double metric::powerPeak(QList *session, int seconds) { QList bests; QList _results; - uint windowSize = 5 * 60; // 5 mins + uint windowSize = seconds; double total = 0.0; QList window; if (session->count() == 0) return -1; - // ride is shorter than the window size! + // ride is shorter than the window size! if (windowSize > session->last().elapsedTime) return -1; @@ -326,7 +323,13 @@ double metric::calculateVO2Max(QList *session) { std::sort(bests.begin(), bests.end(), CompareBests()); - double peak = bests.first().avg; + return bests.first().avg; +} + +// VO2 (L/min) = 0.0108 x power (W) + 0.007 x body mass (kg) +// power = 5 min peak power for a specific ride +double metric::calculateVO2Max(QList *session) { + double peak = powerPeak(session, 5*60); QSettings settings; return ((0.0108 * peak + 0.007 * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()) / settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()) * diff --git a/src/metric.h b/src/metric.h index 10f3fc901..c13be8668 100644 --- a/src/metric.h +++ b/src/metric.h @@ -21,7 +21,8 @@ class metric { void setType(_metric_type t); void setValue(double value, bool applyGainAndOffset = true); double value(); - QDateTime lastChanged() {return m_lastChanged;} + QDateTime lastChanged() { return m_lastChanged; } + QDateTime valueChanged() { return m_valueChanged; } double average(); double average5s(); @@ -41,16 +42,19 @@ class metric { void operator+=(double); void setPaused(bool p); void setLap(bool accumulator); - void setColor(QString color) {m_color = color;} - QString color() {return m_color;} + void setColor(QString color) { m_color = color; } + QString color() { return m_color; } static double calculateMaxSpeedFromPower(double power, double inclination); static double calculatePowerFromSpeed(double speed, double inclination); - static double calculateSpeedFromPower(double power, double inclination, double speed, double deltaTimeSeconds, double speedLimit); + static double calculateSpeedFromPower(double power, double inclination, double speed, double deltaTimeSeconds, + double speedLimit); static double calculateWeightLoss(double kcal); static double calculateVO2Max(QList *session); static double calculateKCalfromHR(double HR_AVG, double elapsed); + static double powerPeak(QList *session, int seconds); + private: double m_value = 0; double m_totValue = 0; @@ -67,6 +71,7 @@ class metric { double m_lapMax = 0; QDateTime m_lastChanged = QDateTime::currentDateTime(); + QDateTime m_valueChanged = QDateTime::currentDateTime(); double m_rateAtSec = 0; _metric_type m_type = METRIC_OTHER; diff --git a/src/nautilusbike.cpp b/src/nautilusbike.cpp index f73a44724..e6c146ef4 100644 --- a/src/nautilusbike.cpp +++ b/src/nautilusbike.cpp @@ -1,7 +1,9 @@ #include "nautilusbike.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" -#include "virtualtreadmill.h" +#endif +#include "virtualbike.h" #include #include #include @@ -29,13 +31,7 @@ nautilusbike::nautilusbike(bool noWriteResistance, bool noHeartService, bool tes refresh->start(300ms); } -nautilusbike::~nautilusbike() { - qDebug() << QStringLiteral("~nautilusbike()") << virtualBike; - if (virtualBike) { - - delete virtualBike; - } -} +nautilusbike::~nautilusbike() { qDebug() << QStringLiteral("~nautilusbike()"); } void nautilusbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -50,11 +46,15 @@ void nautilusbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QS timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -91,12 +91,14 @@ void nautilusbike::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstVirtual && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + if (!firstVirtual && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &nautilusbike::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); firstVirtual = 1; } } @@ -174,13 +176,14 @@ void nautilusbike::characteristicChanged(const QLowEnergyCharacteristic &charact // kg * 3.5) / 200 ) / 60 // double kcal = GetKcalFromPacket(newValue); // double distance = GetDistanceFromPacket(newValue) * - // settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); - // uint16_t watt = (newValue.at(13) << 8) | newValue.at(14); + // settings.value(QZSettings::domyos_elliptical_speed_ratio, + // QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); uint16_t watt = (newValue.at(13) << 8) | + // newValue.at(14); if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() .startsWith(QStringLiteral("Disabled"))) { - Cadence = speed * 2.6; // this device doesn't send cadence so I'm calculating it from the speed + Cadence = newValue.at(1); } Speed = speed; @@ -214,7 +217,8 @@ double nautilusbike::GetSpeedFromPacket(const QByteArray &packet) { double data = 0; convertedData = (packet.at(4) << 8) | packet.at(3); data = (double)convertedData / 100.0f; - data = data * miles; + if (!B616) + data = data * miles; return data; } @@ -316,11 +320,23 @@ void nautilusbike::serviceScanDone(void) { gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); - if (gattCommunicationChannelService == nullptr) { - qDebug() << QStringLiteral("invalid service") << _gattCommunicationChannelServiceId.toString(); - return; + if (!gattCommunicationChannelService) { + _gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("f755c9cf-e1fc-4ecd-8d90-f2d7ebf56b81")); + B616 = true; + + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); + if (!gattCommunicationChannelService) { + _gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("44f8d44f-7e03-4baf-9cc1-bd5a9c7a076b")); + B616 = false; + + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); + if (!gattCommunicationChannelService) { + qDebug() << QStringLiteral("invalid service") << _gattCommunicationChannelServiceId.toString(); + return; + } + } } - + connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &nautilusbike::stateChanged); gattCommunicationChannelService->discoverDetails(); } @@ -388,8 +404,6 @@ bool nautilusbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *nautilusbike::VirtualDevice() { return virtualBike; } - void nautilusbike::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { @@ -399,3 +413,5 @@ void nautilusbike::controllerStateChanged(QLowEnergyController::ControllerState m_control->connectToDevice(); } } + +uint16_t nautilusbike::watts() { return m_watt.value(); } diff --git a/src/nautilusbike.h b/src/nautilusbike.h index 9505c2614..9abd63c34 100644 --- a/src/nautilusbike.h +++ b/src/nautilusbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" class nautilusbike : public bike { Q_OBJECT @@ -35,22 +34,20 @@ class nautilusbike : public bike { nautilusbike(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); ~nautilusbike(); - bool connected(); - - void *VirtualDevice(); + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); double GetInclinationFromPacket(QByteArray packet); double GetWattFromPacket(const QByteArray &packet); double GetDistanceFromPacket(const QByteArray &packet); + uint16_t watts() override; void btinit(bool startTape); void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); QTimer *refresh; - virtualbike *virtualBike = 0; uint8_t firstVirtual = 0; uint8_t counterPoll = 0; @@ -71,6 +68,8 @@ class nautilusbike : public bike { QByteArray lastPacket; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + bool B616 = false; + signals: void disconnected(); void debug(QString string); diff --git a/src/nautiluselliptical.cpp b/src/nautiluselliptical.cpp index 79752f5c2..1d6b56373 100644 --- a/src/nautiluselliptical.cpp +++ b/src/nautiluselliptical.cpp @@ -1,6 +1,9 @@ #include "nautiluselliptical.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -29,13 +32,7 @@ nautiluselliptical::nautiluselliptical(bool noWriteResistance, bool noHeartServi refresh->start(300ms); } -nautiluselliptical::~nautiluselliptical() { - qDebug() << QStringLiteral("~nautiluselliptical()") << virtualTreadmill; - if (virtualTreadmill) { - - delete virtualTreadmill; - } -} +nautiluselliptical::~nautiluselliptical() { qDebug() << QStringLiteral("~nautiluselliptical()"); } void nautiluselliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -50,11 +47,15 @@ void nautiluselliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, co timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -122,21 +123,26 @@ void nautiluselliptical::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstVirtual && searchStopped && !virtualTreadmill && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstVirtual && searchStopped && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &nautiluselliptical::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &nautiluselliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &nautiluselliptical::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstVirtual = 1; } @@ -250,7 +256,9 @@ void nautiluselliptical::characteristicChanged(const QLowEnergyCharacteristic &c } double speed = - GetSpeedFromPacket(newValue) * settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); + GetSpeedFromPacket(newValue) * + settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio) + .toDouble(); if (watts()) KCal += ((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( @@ -258,8 +266,9 @@ void nautiluselliptical::characteristicChanged(const QLowEnergyCharacteristic &c // kg * 3.5) / 200 ) / 60 // double kcal = GetKcalFromPacket(newValue); // double distance = GetDistanceFromPacket(newValue) * - // settings.value(QZSettings::domyos_elliptical_speed_ratio, QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); - // uint16_t watt = (newValue.at(13) << 8) | newValue.at(14); + // settings.value(QZSettings::domyos_elliptical_speed_ratio, + // QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); uint16_t watt = (newValue.at(13) << 8) | + // newValue.at(14); if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() @@ -483,10 +492,6 @@ bool nautiluselliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *nautiluselliptical::VirtualTreadmill() { return virtualTreadmill; } - -void *nautiluselliptical::VirtualDevice() { return VirtualTreadmill(); } - void nautiluselliptical::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/nautiluselliptical.h b/src/nautiluselliptical.h index d43e09533..4e630211b 100644 --- a/src/nautiluselliptical.h +++ b/src/nautiluselliptical.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" class nautiluselliptical : public elliptical { Q_OBJECT @@ -36,10 +34,7 @@ class nautiluselliptical : public elliptical { nautiluselliptical(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); ~nautiluselliptical(); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -52,8 +47,6 @@ class nautiluselliptical : public elliptical { void startDiscover(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = 0; uint8_t firstVirtual = 0; uint8_t counterPoll = 0; diff --git a/src/nautilustreadmill.cpp b/src/nautilustreadmill.cpp index f235ca69c..244b08467 100644 --- a/src/nautilustreadmill.cpp +++ b/src/nautilustreadmill.cpp @@ -1,5 +1,7 @@ #include "nautilustreadmill.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include @@ -49,11 +51,15 @@ void nautilustreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, con // &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -90,13 +96,14 @@ void nautilustreadmill::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &nautilustreadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -401,10 +408,6 @@ bool nautilustreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *nautilustreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *nautilustreadmill::VirtualDevice() { return VirtualTreadMill(); } - bool nautilustreadmill::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/nautilustreadmill.h b/src/nautilustreadmill.h index 6e1b9e940..faa708809 100644 --- a/src/nautilustreadmill.h +++ b/src/nautilustreadmill.h @@ -26,21 +26,17 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class nautilustreadmill : public treadmill { Q_OBJECT public: nautilustreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadMill(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + bool connected() override; + double minStepInclination() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; + virtual bool canStartStop() override { return false; } private: double GetSpeedFromPacket(const QByteArray &packet); @@ -65,7 +61,6 @@ class nautilustreadmill : public treadmill { int64_t lastStop = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/nordictrackelliptical.cpp b/src/nordictrackelliptical.cpp index 68d71af95..5e2837218 100644 --- a/src/nordictrackelliptical.cpp +++ b/src/nordictrackelliptical.cpp @@ -1,6 +1,9 @@ #include "nordictrackelliptical.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -10,6 +13,7 @@ #include #include #include +#include using namespace std::chrono_literals; @@ -39,11 +43,15 @@ void nordictrackelliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -936,7 +944,7 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); double cadence_gain = settings.value(QZSettings::cadence_gain, QZSettings::default_cadence_gain).toDouble(); double cadence_offset = settings.value(QZSettings::cadence_offset, QZSettings::default_cadence_offset).toDouble(); - //const double miles = 1.60934; //not used + // const double miles = 1.60934; //not used bool proform_hybrid_trainer_xt = settings.value(QZSettings::proform_hybrid_trainer_xt, QZSettings::default_proform_hybrid_trainer_xt).toBool(); bool disable_hr_frommachinery = @@ -950,8 +958,8 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic lastPacket = newValue; - if (!proform_hybrid_trainer_xt && !nordictrack_elliptical_c7_5 && newValue.length() == 20 && newValue.at(0) == 0x01 && - newValue.at(1) == 0x12 && newValue.at(19) == 0x2C) { + if (!proform_hybrid_trainer_xt && !nordictrack_elliptical_c7_5 && newValue.length() == 20 && + newValue.at(0) == 0x01 && newValue.at(1) == 0x12 && newValue.at(19) == 0x2C) { uint8_t c = newValue.at(2); if (c > 0) Cadence = (c * cadence_gain) + cadence_offset; @@ -984,10 +992,9 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic if (nordictrack_elliptical_c7_5 && newValue.length() == 20 && newValue.at(0) == 0x01 && newValue.at(1) == 0x12 && newValue.at(4) == 0x46 && initDone == true && - !(((uint8_t)newValue.at(4)) == 0xFF && ((uint8_t)newValue.at(5)) == 0xFF && - ((uint8_t)newValue.at(6)) == 0xFF && ((uint8_t)newValue.at(7)) == 0xFF && - ((uint8_t)newValue.at(8)) == 0xFF && ((uint8_t)newValue.at(9)) == 0xFF && - ((uint8_t)newValue.at(12)) == 0xFF && ((uint8_t)newValue.at(11)) == 0xFF)) { + !(((uint8_t)newValue.at(4)) == 0xFF && ((uint8_t)newValue.at(5)) == 0xFF && ((uint8_t)newValue.at(6)) == 0xFF && + ((uint8_t)newValue.at(7)) == 0xFF && ((uint8_t)newValue.at(8)) == 0xFF && ((uint8_t)newValue.at(9)) == 0xFF && + ((uint8_t)newValue.at(12)) == 0xFF && ((uint8_t)newValue.at(11)) == 0xFF)) { Speed = (double)(((uint16_t)((uint8_t)newValue.at(13)) << 8) + (uint16_t)((uint8_t)newValue.at(12))) / 100.0; emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); lastSpeedChanged = QDateTime::currentDateTime(); @@ -1039,11 +1046,13 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic } } - if (nordictrack_elliptical_c7_5 && newValue.length() == 20 && newValue.at(0) == 0x00 && newValue.at(1) == 0x12 && - newValue.at(2) == 0x01 && newValue.at(3) == 0x04 && newValue.at(4) == 0x02 && - (newValue.at(5) == 0x30 || newValue.at(5) == 0x31)){ + if (!nordictrack_elliptical_c7_5) { + Resistance = GetResistanceFromPacket(newValue); + } else if (nordictrack_elliptical_c7_5 && newValue.length() == 20 && newValue.at(0) == 0x00 && + newValue.at(1) == 0x12 && newValue.at(2) == 0x01 && newValue.at(3) == 0x04 && newValue.at(4) == 0x02 && + (newValue.at(5) == 0x30 || newValue.at(5) == 0x31)) { Inclination = GetInclinationFromPacket(newValue); - Resistance = GetResistanceFromPacket(newValue); + Resistance = GetResistanceFromPacket(newValue); } uint16_t p = qCeil((Resistance.value() * 3.33) + 10.0); @@ -1070,16 +1079,7 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } } @@ -1282,7 +1282,7 @@ void nordictrackelliptical::btinit() { writeCharacteristic(noOpData9b, sizeof(noOpData9b), QStringLiteral("init"), false, false); QThread::msleep(400); writeCharacteristic(noOpData10b, sizeof(noOpData10b), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(400); } else { writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); QThread::msleep(400); @@ -1351,7 +1351,7 @@ void nordictrackelliptical::stateChanged(QLowEnergyService::ServiceState state) // ******************************************* virtual treadmill init ************************************* QSettings settings; - if (!firstStateChanged && !virtualTreadmill && !virtualBike) { + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -1360,16 +1360,18 @@ void nordictrackelliptical::stateChanged(QLowEnergyService::ServiceState state) if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &nordictrackelliptical::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &nordictrackelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, - bikeResistanceGain); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); connect(virtualBike, &virtualbike::changeInclination, this, &nordictrackelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -1466,10 +1468,6 @@ bool nordictrackelliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *nordictrackelliptical::VirtualTreadmill() { return virtualTreadmill; } - -void *nordictrackelliptical::VirtualDevice() { return VirtualTreadmill(); } - void nordictrackelliptical::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/nordictrackelliptical.h b/src/nordictrackelliptical.h index 02586ac39..8ae6a8131 100644 --- a/src/nordictrackelliptical.h +++ b/src/nordictrackelliptical.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,12 +37,9 @@ class nordictrackelliptical : public elliptical { public: nordictrackelliptical(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); - int pelotonToEllipticalResistance(int pelotonResistance); - bool inclinationAvailableByHardware() { return false; } + bool connected() override; + int pelotonToEllipticalResistance(int pelotonResistance) override; + bool inclinationAvailableByHardware() override{ return false; } private: double GetDistanceFromPacket(QByteArray packet); @@ -61,8 +56,6 @@ class nordictrackelliptical : public elliptical { void forceSpeed(double speed); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; uint8_t bikeResistanceOffset = 4; double bikeResistanceGain = 1.0; diff --git a/src/nordictrackifitadbbike.cpp b/src/nordictrackifitadbbike.cpp index cebd0fefe..63fdc2696 100644 --- a/src/nordictrackifitadbbike.cpp +++ b/src/nordictrackifitadbbike.cpp @@ -1,11 +1,12 @@ #include "nordictrackifitadbbike.h" -#include "homeform.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" -#include "virtualtreadmill.h" +#endif #include #include #include +#include #include #include #include @@ -13,9 +14,100 @@ using namespace std::chrono_literals; -nordictrackifitadbbike::nordictrackifitadbbike(bool noWriteResistance, bool noHeartService) { +nordictrackifitadbbikeLogcatAdbThread::nordictrackifitadbbikeLogcatAdbThread(QString s) { Q_UNUSED(s) } + +void nordictrackifitadbbikeLogcatAdbThread::run() { QSettings settings; - bool nordictrack_ifit_adb_remote = settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote).toBool(); + QString ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); + runAdbCommand("connect " + ip); + + while (1) { + runAdbTailCommand("logcat"); + if(adbCommandPending.length() != 0) { + runAdbCommand(adbCommandPending); + adbCommandPending = ""; + } + msleep(100); + } +} + +QString nordictrackifitadbbikeLogcatAdbThread::runAdbCommand(QString command) { +#ifdef Q_OS_WINDOWS + QProcess process; + emit debug("adb >> " + command); + process.start("adb/adb.exe", QStringList(command.split(' '))); + process.waitForFinished(-1); // will wait forever until finished + + QString out = process.readAllStandardOutput(); + QString err = process.readAllStandardError(); + + emit debug("adb << OUT " + out); + emit debug("adb << ERR" + err); +#else + QString out; +#endif + return out; +} + +bool nordictrackifitadbbikeLogcatAdbThread::runCommand(QString command) { + if(adbCommandPending.length() == 0) { + adbCommandPending = command; + return true; + } + return false; +} + +void nordictrackifitadbbikeLogcatAdbThread::runAdbTailCommand(QString command) { +#ifdef Q_OS_WINDOWS + auto process = new QProcess; + QObject::connect(process, &QProcess::readyReadStandardOutput, [process, this]() { + QString output = process->readAllStandardOutput(); + // qDebug() << "adbLogCat STDOUT << " << output; + QStringList lines = output.split('\n', Qt::SplitBehaviorFlags::SkipEmptyParts); + bool wattFound = false; + bool hrmFound = false; + foreach (QString line, lines) { + if (line.contains("Changed KPH")) { + emit debug(line); + speed = line.split(' ').last().toDouble(); + } else if (line.contains("Changed Grade")) { + emit debug(line); + inclination = line.split(' ').last().toDouble(); + } else if (line.contains("Changed Watts")) { + emit debug(line); + watt = line.split(' ').last().toDouble(); + wattFound = true; + } else if (line.contains("HeartRateDataUpdate")) { + emit debug(line); + QStringList splitted = line.split(' ', Qt::SkipEmptyParts); + if (splitted.length() > 14) { + hrm = splitted[14].toInt(); + hrmFound = true; + } + } + } + emit onSpeedInclination(speed, inclination); + if (wattFound) + emit onWatt(watt); + if (hrmFound) + emit onHRM(hrm); + }); + QObject::connect(process, &QProcess::readyReadStandardError, [process, this]() { + auto output = process->readAllStandardError(); + emit debug("adbLogCat ERROR << " + output); + }); + emit debug("adbLogCat >> " + command); + process->start("adb/adb.exe", QStringList(command.split(' '))); + process->waitForFinished(-1); +#endif +} + +nordictrackifitadbbike::nordictrackifitadbbike(bool noWriteResistance, bool noHeartService, + uint8_t bikeResistanceOffset, double bikeResistanceGain) { + QSettings settings; + bool nordictrack_ifit_adb_remote = + settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote) + .toBool(); m_watt.setType(metric::METRIC_WATT); Speed.setType(metric::METRIC_SPEED); refresh = new QTimer(this); @@ -33,33 +125,58 @@ nordictrackifitadbbike::nordictrackifitadbbike(bool noWriteResistance, bool noHe connect(socket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams())); // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualBike) { + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - if (virtual_device_enabled) { - debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); - connect(virtualBike, &virtualbike::changeInclination, this, - &nordictrackifitadbbike::changeInclinationRequested); - firstStateChanged = 1; +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + if (virtual_device_enabled) { + qDebug() << QStringLiteral("creating virtual bike interface..."); + auto virtualBike = + new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + // connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug); + connect(virtualBike, &virtualbike::changeInclination, this, &nordictrackifitadbbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } + firstStateChanged = 1; // ******************************************************************************************************** + if (nordictrack_ifit_adb_remote) { #ifdef Q_OS_ANDROID - if(nordictrack_ifit_adb_remote) { QAndroidJniObject IP = QAndroidJniObject::fromString(ip).object(); QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/QZAdbRemote", "createConnection", - "(Ljava/lang/String;Landroid/content/Context;)V", IP.object(), QtAndroid::androidContext().object()); - } + "(Ljava/lang/String;Landroid/content/Context;)V", + IP.object(), QtAndroid::androidContext().object()); +#elif defined Q_OS_WIN + logcatAdbThread = new nordictrackifitadbbikeLogcatAdbThread("logcatAdbThread"); + /*connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::onSpeedInclination, this, + &nordictrackifitadbbike::onSpeedInclination); + connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::onWatt, this, + &nordictrackifitadbbike::onWatt);*/ + connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::onHRM, this, &nordictrackifitadbbike::onHRM); + connect(logcatAdbThread, &nordictrackifitadbbikeLogcatAdbThread::debug, this, &nordictrackifitadbbike::debug); + logcatAdbThread->start(); #endif + } } bool nordictrackifitadbbike::inclinationAvailableByHardware() { return true; } double nordictrackifitadbbike::getDouble(QString v) { QChar d = QLocale().decimalPoint(); - if(d == ',') { + if (d == ',') { v = v.replace('.', ','); } return QLocale().toDouble(v); @@ -93,7 +210,8 @@ void nordictrackifitadbbike::processPendingDatagrams() { QStringList lines = QString::fromLocal8Bit(datagram.data()).split("\n"); foreach (QString line, lines) { qDebug() << line; - if (line.contains(QStringLiteral("Changed KPH"))) { + + if (line.contains(QStringLiteral("Changed KPH")) && !settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { QStringList aValues = line.split(" "); if (aValues.length()) { speed = getDouble(aValues.last()); @@ -110,12 +228,17 @@ void nordictrackifitadbbike::processPendingDatagrams() { if (aValues.length()) { gear = getDouble(aValues.last()); Resistance = gear; + gearsAvailable = true; } } else if (line.contains(QStringLiteral("Changed Resistance"))) { QStringList aValues = line.split(" "); if (aValues.length()) { resistance = getDouble(aValues.last()); - //Resistance = resistance; + m_pelotonResistance = (100 / 32) * resistance; + qDebug() << QStringLiteral("Current Peloton Resistance: ") << m_pelotonResistance.value() + << resistance; + if(!gearsAvailable) + Resistance = resistance; } } else if (line.contains(QStringLiteral("Changed Watts"))) { QStringList aValues = line.split(" "); @@ -132,29 +255,50 @@ void nordictrackifitadbbike::processPendingDatagrams() { } } + if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + // since the motor of the bike is slow, let's filter the inclination changes to more than 4 seconds - if(lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > 4) { + if (lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > 4) { lastInclinationChanged = QDateTime::currentDateTime(); - #ifdef Q_OS_ANDROID - bool nordictrack_ifit_adb_remote = settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote).toBool(); - if(nordictrack_ifit_adb_remote) { - if(requestInclination != -100) { + bool nordictrack_ifit_adb_remote = + settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote) + .toBool(); + if (nordictrack_ifit_adb_remote) { + if (requestInclination != -100) { double inc = qRound(requestInclination / 0.5) * 0.5; - if(inc != currentInclination().value()) { + if (inc != currentInclination().value()) { + bool proform_studio = settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool(); int x1 = 75; - int y2 = (int) (616.18 - (17.223 * (inc + gears()))); - int y1Resistance = (int) (616.18 - (17.223 * currentInclination().value())); + int y2 = (int)(616.18 - (17.223 * (inc + gears()))); + int y1Resistance = (int)(616.18 - (17.223 * currentInclination().value())); + + if(proform_studio) { + int x1 = 1827; + int y2 = (int)(806 - (21.375 * (inc + gears()))); + int y1Resistance = (int)(806 - (21.375 * currentInclination().value())); + } - lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " + QString::number(x1) + " " + QString::number(y2) + " 200"; + lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " + + QString::number(x1) + " " + QString::number(y2) + " 200"; qDebug() << " >> " + lastCommand; +#ifdef Q_OS_ANDROID QAndroidJniObject command = QAndroidJniObject::fromString(lastCommand).object(); - QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/QZAdbRemote", "sendCommand", - "(Ljava/lang/String;)V", command.object()); + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/QZAdbRemote", + "sendCommand", "(Ljava/lang/String;)V", + command.object()); +#elif defined(Q_OS_WIN) + if (logcatAdbThread) + logcatAdbThread->runCommand("shell " + lastCommand); +#endif } } + requestInclination = -100; } - #endif QByteArray message = (QString::number(requestInclination).toLocal8Bit()) + ";"; requestInclination = -100; @@ -172,6 +316,11 @@ void nordictrackifitadbbike::processPendingDatagrams() { Distance += ((Speed.value() / 3600000.0) * ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); #ifdef Q_OS_ANDROID @@ -181,18 +330,22 @@ void nordictrackifitadbbike::processPendingDatagrams() { #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); + } + } + #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); + bool cadencep = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadencep && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } #endif #endif - } - } emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); @@ -205,6 +358,76 @@ void nordictrackifitadbbike::processPendingDatagrams() { } } +void nordictrackifitadbbike::onHRM(int hrm) { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + + if ( +#ifdef Q_OS_ANDROID + (!settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) && +#endif + heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) { + + Heart = hrm; + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); + } +} + +resistance_t nordictrackifitadbbike::pelotonToBikeResistance(int pelotonResistance) { + if (pelotonResistance <= 10) { + return 1; + } + if (pelotonResistance <= 20) { + return 2; + } + if (pelotonResistance <= 25) { + return 3; + } + if (pelotonResistance <= 30) { + return 4; + } + if (pelotonResistance <= 35) { + return 5; + } + if (pelotonResistance <= 40) { + return 6; + } + if (pelotonResistance <= 45) { + return 7; + } + if (pelotonResistance <= 50) { + return 8; + } + if (pelotonResistance <= 55) { + return 9; + } + if (pelotonResistance <= 60) { + return 10; + } + if (pelotonResistance <= 65) { + return 11; + } + if (pelotonResistance <= 70) { + return 12; + } + if (pelotonResistance <= 75) { + return 13; + } + if (pelotonResistance <= 80) { + return 14; + } + if (pelotonResistance <= 85) { + return 15; + } + if (pelotonResistance <= 100) { + return 16; + } + return Resistance.value(); +} + void nordictrackifitadbbike::forceResistance(double resistance) {} void nordictrackifitadbbike::update() { @@ -241,5 +464,3 @@ void nordictrackifitadbbike::changeInclinationRequested(double grade, double per } bool nordictrackifitadbbike::connected() { return true; } - -void *nordictrackifitadbbike::VirtualDevice() { return virtualBike; } diff --git a/src/nordictrackifitadbbike.h b/src/nordictrackifitadbbike.h index b629b8b2e..de9fbaf03 100644 --- a/src/nordictrackifitadbbike.h +++ b/src/nordictrackifitadbbike.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include "bike.h" @@ -25,23 +26,52 @@ #include "ios/lockscreen.h" #endif -class nordictrackifitadbbike : public bike { +class nordictrackifitadbbikeLogcatAdbThread : public QThread { Q_OBJECT + public: - nordictrackifitadbbike(bool noWriteResistance, bool noHeartService); - bool connected(); - bool inclinationAvailableByHardware(); + explicit nordictrackifitadbbikeLogcatAdbThread(QString s); + bool runCommand(QString command); + + void run() override; + + signals: + void onSpeedInclination(double speed, double inclination); + void debug(QString message); + void onWatt(double watt); + void onHRM(int hrm); + + private: + QString adbCommandPending = ""; + QString runAdbCommand(QString command); + double speed = 0; + double inclination = 0; + double watt = 0; + int hrm = 0; + QString name; + struct adbfile { + QDateTime date; + QString name; + }; + + void runAdbTailCommand(QString command); +}; - void *VirtualTreadmill(); - void *VirtualDevice(); +class nordictrackifitadbbike : public bike { + Q_OBJECT + public: + nordictrackifitadbbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain); + bool connected() override; + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + bool inclinationAvailableByHardware() override; private: void forceResistance(double resistance); - uint16_t watts(); + uint16_t watts() override; double getDouble(QString v); QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t sec1Update = 0; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); @@ -55,12 +85,14 @@ class nordictrackifitadbbike : public bike { bool noWriteResistance = false; bool noHeartService = false; + bool gearsAvailable = false; + QUdpSocket *socket = nullptr; QHostAddress lastSender; -#ifdef Q_OS_ANDROID + nordictrackifitadbbikeLogcatAdbThread *logcatAdbThread = nullptr; + QString lastCommand; -#endif QString ip; @@ -76,6 +108,7 @@ class nordictrackifitadbbike : public bike { void processPendingDatagrams(); void changeInclinationRequested(double grade, double percentage); + void onHRM(int hrm); void update(); }; diff --git a/src/nordictrackifitadbtreadmill.cpp b/src/nordictrackifitadbtreadmill.cpp index 6364f7cb5..01ba37e57 100644 --- a/src/nordictrackifitadbtreadmill.cpp +++ b/src/nordictrackifitadbtreadmill.cpp @@ -1,11 +1,13 @@ #include "nordictrackifitadbtreadmill.h" -#include "homeform.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include #include +#include #include #include #include @@ -28,15 +30,15 @@ void nordictrackifitadbtreadmillLogcatAdbThread::run() { QString nordictrackifitadbtreadmillLogcatAdbThread::runAdbCommand(QString command) { #ifdef Q_OS_WINDOWS QProcess process; - qDebug() << "adb >> " << command; + emit debug("adb >> " + command); process.start("adb/adb.exe", QStringList(command.split(' '))); process.waitForFinished(-1); // will wait forever until finished QString out = process.readAllStandardOutput(); QString err = process.readAllStandardError(); - qDebug() << "adb << OUT" << out; - qDebug() << "adb << ERR" << err; + emit debug("adb << OUT " + out); + emit debug("adb << ERR" + err); #else QString out; #endif @@ -50,22 +52,29 @@ void nordictrackifitadbtreadmillLogcatAdbThread::runAdbTailCommand(QString comma QString output = process->readAllStandardOutput(); qDebug() << "adbLogCat STDOUT << " << output; QStringList lines = output.split('\n', Qt::SplitBehaviorFlags::SkipEmptyParts); + bool wattFound = false; foreach (QString line, lines) { if (line.contains("Changed KPH")) { - qDebug() << line; + emit debug(line); speed = line.split(' ').last().toDouble(); } else if (line.contains("Changed Grade")) { - qDebug() << line; + emit debug(line); inclination = line.split(' ').last().toDouble(); + } else if (line.contains("Changed Watts")) { + emit debug(line); + watt = line.split(' ').last().toDouble(); + wattFound = true; } } emit onSpeedInclination(speed, inclination); + if (wattFound) + emit onWatt(watt); }); QObject::connect(process, &QProcess::readyReadStandardError, [process, this]() { auto output = process->readAllStandardError(); - qDebug() << "adbLogCat ERROR << " << output; + emit debug("adbLogCat ERROR << " + output); }); - qDebug() << "adbLogCat >> " << command; + emit debug("adbLogCat >> " + command); process->start("adb/adb.exe", QStringList(command.split(' '))); process->waitForFinished(-1); #endif @@ -109,6 +118,10 @@ nordictrackifitadbtreadmill::nordictrackifitadbtreadmill(bool noWriteResistance, logcatAdbThread = new nordictrackifitadbtreadmillLogcatAdbThread("logcatAdbThread"); connect(logcatAdbThread, &nordictrackifitadbtreadmillLogcatAdbThread::onSpeedInclination, this, &nordictrackifitadbtreadmill::onSpeedInclination); + connect(logcatAdbThread, &nordictrackifitadbtreadmillLogcatAdbThread::onWatt, this, + &nordictrackifitadbtreadmill::onWatt); + connect(logcatAdbThread, &nordictrackifitadbtreadmillLogcatAdbThread::debug, this, + &nordictrackifitadbtreadmill::debug); logcatAdbThread->start(); } #endif @@ -125,7 +138,7 @@ nordictrackifitadbtreadmill::nordictrackifitadbtreadmill(bool noWriteResistance, initRequest = true; // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill && !virtualBike) { + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -134,15 +147,17 @@ nordictrackifitadbtreadmill::nordictrackifitadbtreadmill(bool noWriteResistance, if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &nordictrackifitadbtreadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &nordictrackifitadbtreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &nordictrackifitadbtreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -169,9 +184,12 @@ void nordictrackifitadbtreadmill::processPendingDatagrams() { QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); double speed = 0; double incline = 0; + bool hrmFound = false; QStringList lines = QString::fromLocal8Bit(datagram.data()).split("\n"); foreach (QString line, lines) { qDebug() << line; @@ -187,6 +205,17 @@ void nordictrackifitadbtreadmill::processPendingDatagrams() { incline = getDouble(aValues.last()); Inclination = incline; } + } else if (line.contains("HeartRateDataUpdate") && +#ifdef Q_OS_ANDROID + (!settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) && +#endif + heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery + ) { + QStringList splitted = line.split(' ', Qt::SkipEmptyParts); + if (splitted.length() > 14) { + Heart = splitted[14].toInt(); + hrmFound = true; + } } } @@ -252,16 +281,7 @@ void nordictrackifitadbtreadmill::processPendingDatagrams() { #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -271,6 +291,7 @@ void nordictrackifitadbtreadmill::processPendingDatagrams() { emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value())); // debug("Current Distance: " + QString::number(distance)); + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); emit debug(QStringLiteral("Current Watt: ") + QString::number(watts(weight))); } } @@ -289,7 +310,7 @@ disable_log, bool wait_for_response) { QEventLoop loop; QTimer timeout; if (wait QByteArray((const char *)data, data_len)); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -300,6 +321,11 @@ void nordictrackifitadbtreadmill::forceIncline(double incline) {} void nordictrackifitadbtreadmill::forceSpeed(double speed) {} +void nordictrackifitadbtreadmill::onWatt(double watt) { + m_watt = watt; + wattReadFromTM = true; +} + void nordictrackifitadbtreadmill::onSpeedInclination(double speed, double inclination) { Speed = speed; @@ -328,16 +354,7 @@ void nordictrackifitadbtreadmill::onSpeedInclination(double speed, double inclin #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -355,7 +372,7 @@ void nordictrackifitadbtreadmill::update() { settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); - update_metrics(true, watts(weight)); + update_metrics(!wattReadFromTM, watts(weight)); if (initRequest) { initRequest = false; @@ -391,7 +408,3 @@ void nordictrackifitadbtreadmill::changeInclinationRequested(double grade, doubl } bool nordictrackifitadbtreadmill::connected() { return true; } - -void *nordictrackifitadbtreadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *nordictrackifitadbtreadmill::VirtualDevice() { return VirtualTreadmill(); } diff --git a/src/nordictrackifitadbtreadmill.h b/src/nordictrackifitadbtreadmill.h index 8fea8f4b3..99bc9b099 100644 --- a/src/nordictrackifitadbtreadmill.h +++ b/src/nordictrackifitadbtreadmill.h @@ -20,8 +20,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -33,14 +31,17 @@ class nordictrackifitadbtreadmillLogcatAdbThread : public QThread { public: explicit nordictrackifitadbtreadmillLogcatAdbThread(QString s); - void run(); + void run() override; signals: void onSpeedInclination(double speed, double inclination); + void debug(QString message); + void onWatt(double watt); private: double speed = 0; double inclination = 0; + double watt = 0; QString name; struct adbfile { QDateTime date; @@ -55,11 +56,8 @@ class nordictrackifitadbtreadmill : public treadmill { Q_OBJECT public: nordictrackifitadbtreadmill(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + bool connected() override; + bool canStartStop() override { return false; } private: void forceIncline(double incline); @@ -67,13 +65,12 @@ class nordictrackifitadbtreadmill : public treadmill { double getDouble(QString v); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t sec1Update = 0; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); uint8_t firstStateChanged = 0; uint16_t m_watts = 0; + bool wattReadFromTM = false; bool initDone = false; bool initRequest = false; @@ -101,6 +98,7 @@ class nordictrackifitadbtreadmill : public treadmill { private slots: void onSpeedInclination(double speed, double inclination); + void onWatt(double watt); void processPendingDatagrams(); void changeInclinationRequested(double grade, double percentage); diff --git a/src/npecablebike.cpp b/src/npecablebike.cpp index 182f56690..44ce83f1b 100644 --- a/src/npecablebike.cpp +++ b/src/npecablebike.cpp @@ -1,5 +1,5 @@ #include "npecablebike.h" -#include "ios/lockscreen.h" + #include "virtualbike.h" #include #include @@ -9,9 +9,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -402,16 +403,7 @@ void npecablebike::characteristicChanged(const QLowEnergyCharacteristic &charact } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && Heart.value() == 0) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } if (Cadence.value() > 0) { @@ -515,7 +507,7 @@ void npecablebike::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -537,9 +529,10 @@ void npecablebike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&npecablebike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &npecablebike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -645,10 +638,6 @@ bool npecablebike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *npecablebike::VirtualBike() { return virtualBike; } - -void *npecablebike::VirtualDevice() { return VirtualBike(); } - uint16_t npecablebike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/npecablebike.h b/src/npecablebike.h index 5899458b4..56f423714 100644 --- a/src/npecablebike.h +++ b/src/npecablebike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,19 +36,15 @@ class npecablebike : public bike { Q_OBJECT public: npecablebike(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; // QLowEnergyCharacteristic gattNotify1Characteristic; diff --git a/src/octaneelliptical.cpp b/src/octaneelliptical.cpp index 2c9c9fc8d..2257de163 100644 --- a/src/octaneelliptical.cpp +++ b/src/octaneelliptical.cpp @@ -170,10 +170,10 @@ octaneelliptical::octaneelliptical(uint32_t pollDeviceTime, bool noConsole, bool actualPace2Sign.clear(); // SPEED - actualPaceSign.append(0x02); actualPaceSign.append(0x07); - actualPace2Sign.append(0x01); + actualPaceSign.append(0x03); actualPace2Sign.append(0x07); + actualPace2Sign.append(0x03); m_watt.setType(metric::METRIC_WATT); Speed.setType(metric::METRIC_SPEED); @@ -199,11 +199,15 @@ void octaneelliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, cons timeout.singleShot(400ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -260,15 +264,16 @@ void octaneelliptical::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); - connect(virtualTreadMill, &virtualtreadmill::debug, this, &octaneelliptical::debug); - connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); + connect(virtualTreadmill, &virtualtreadmill::debug, this, &octaneelliptical::debug); + connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &octaneelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -335,8 +340,8 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha if ((newValue.length() != 20)) return; - if ((uint8_t)newValue[0] == 0xa5 && newValue[1] == 0x06) { - Resistance = (uint8_t)newValue[5]; + if ((uint8_t)newValue[0] == 0xa5 && newValue[1] == 0x09) { + Resistance = (uint8_t)newValue[4]; emit debug(QStringLiteral("Current resistance: ") + QString::number(Resistance.value())); return; } @@ -397,8 +402,8 @@ void octaneelliptical::characteristicChanged(const QLowEnergyCharacteristic &cha } double octaneelliptical::GetSpeedFromPacket(const QByteArray &packet, int index) { - uint16_t convertedData = (packet.at(index + 1) << 8) | ((uint8_t)packet.at(index)); - return ((double)convertedData) / 1000.0; + uint16_t convertedData = (packet.at(index + 4) << 8) | ((uint8_t)packet.at(index + 5)); + return ((double)convertedData) / 100.0; } void octaneelliptical::btinit(bool startTape) { @@ -534,10 +539,6 @@ bool octaneelliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *octaneelliptical::VirtualTreadMill() { return virtualTreadMill; } - -void *octaneelliptical::VirtualDevice() { return VirtualTreadMill(); } - bool octaneelliptical::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/octaneelliptical.h b/src/octaneelliptical.h index 25ee57b3f..6f590dede 100644 --- a/src/octaneelliptical.h +++ b/src/octaneelliptical.h @@ -33,15 +33,12 @@ class octaneelliptical : public elliptical { public: octaneelliptical(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); + bool connected() override; + double minStepInclination() override; double minStepSpeed(); bool autoPauseWhenSpeedIsZero(); bool autoStartWhenSpeedIsGreaterThenZero(); - void *VirtualTreadMill(); - void *VirtualDevice(); - private: double GetSpeedFromPacket(const QByteArray &packet, int index); void forceSpeed(double requestSpeed); @@ -67,7 +64,6 @@ class octaneelliptical : public elliptical { QByteArray actualPace2Sign; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/octanetreadmill.cpp b/src/octanetreadmill.cpp index e635955e9..c20234566 100644 --- a/src/octanetreadmill.cpp +++ b/src/octanetreadmill.cpp @@ -174,6 +174,9 @@ octanetreadmill::octanetreadmill(uint32_t pollDeviceTime, bool noConsole, bool n actualPaceSign.append(0x23); actualPace2Sign.append(0x01); actualPace2Sign.append(0x23); + cadenceSign.append(0x2c); + cadenceSign.append(0x01); + cadenceSign.append(0x3A); m_watt.setType(metric::METRIC_WATT); Speed.setType(metric::METRIC_SPEED); @@ -205,11 +208,15 @@ void octanetreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(400ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -266,15 +273,16 @@ void octanetreadmill::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &octanetreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &octanetreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -337,14 +345,29 @@ void octanetreadmill::characteristicChanged(const QLowEnergyCharacteristic &char emit packetReceived(); - if (lastTimeCharacteristicChanged.secsTo(QDateTime::currentDateTime()) > 5) { + if (ZR8 == false && lastTimeCharacteristicChanged.secsTo(QDateTime::currentDateTime()) > 5) { + emit debug(QStringLiteral("resetting speed")); + Speed = 0; + Cadence = 0; + } else if (ZR8 == true && Speed.lastChanged().secsTo(QDateTime::currentDateTime()) > 15 && + Cadence.lastChanged().secsTo(QDateTime::currentDateTime()) > 15) { emit debug(QStringLiteral("resetting speed")); Speed = 0; + Cadence = 0; } if ((newValue.length() != 20)) return; + if (ZR8 && newValue.contains(cadenceSign)) { + int16_t i = newValue.indexOf(cadenceSign) + 3; + + if (i >= newValue.length()) + return; + + Cadence = ((uint8_t)newValue.at(i)); + } + if ((uint8_t)newValue[0] == 0xa5 && newValue[1] == 0x17) return; @@ -368,8 +391,7 @@ void octanetreadmill::characteristicChanged(const QLowEnergyCharacteristic &char else #endif { - /*if(heartRateBeltName.startsWith("Disabled")) - Heart = value.at(18);*/ + update_hr_from_external(); } emit debug(QStringLiteral("Current speed: ") + QString::number(speed)); @@ -404,7 +426,9 @@ void octanetreadmill::characteristicChanged(const QLowEnergyCharacteristic &char (1000.0 / (lastTimeCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); } - cadenceFromAppleWatch(); + // ZR8 has builtin cadence sensor + if (!ZR8) + cadenceFromAppleWatch(); emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value())); emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); @@ -504,6 +528,12 @@ void octanetreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { device.address().toString() + ')'); { bluetoothDevice = device; + + if (device.name().toUpper().startsWith(QLatin1String("ZR8"))) { + ZR8 = true; + qDebug() << "ZR8 workaround activated"; + } + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &octanetreadmill::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &octanetreadmill::serviceScanDone); @@ -553,10 +583,6 @@ bool octanetreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *octanetreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *octanetreadmill::VirtualDevice() { return VirtualTreadMill(); } - bool octanetreadmill::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/octanetreadmill.h b/src/octanetreadmill.h index 7f1f731a4..88b2ef746 100644 --- a/src/octanetreadmill.h +++ b/src/octanetreadmill.h @@ -26,22 +26,18 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class octanetreadmill : public treadmill { Q_OBJECT public: octanetreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - double minStepSpeed(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadMill(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + bool connected() override; + double minStepInclination() override; + double minStepSpeed() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; + bool canStartStop() override { return false; } private: double GetSpeedFromPacket(const QByteArray &packet, int index); @@ -66,9 +62,9 @@ class octanetreadmill : public treadmill { QByteArray actualPaceSign; QByteArray actualPace2Sign; + QByteArray cadenceSign; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; @@ -77,6 +73,8 @@ class octanetreadmill : public treadmill { bool initDone = false; bool initRequest = false; + bool ZR8 = false; + Q_SIGNALS: void disconnected(); void debug(QString string); diff --git a/src/pafersbike.cpp b/src/pafersbike.cpp index 2a3cec7b6..7e698fbe4 100644 --- a/src/pafersbike.cpp +++ b/src/pafersbike.cpp @@ -1,6 +1,8 @@ #include "pafersbike.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -59,11 +61,15 @@ void pafersbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStr return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } @@ -208,7 +214,9 @@ void pafersbike::characteristicChanged(const QLowEnergyCharacteristic &character if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = ((uint8_t)newValue.at(3)); } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } Resistance = ((uint8_t)newValue.at(5)); @@ -218,7 +226,8 @@ void pafersbike::characteristicChanged(const QLowEnergyCharacteristic &character if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -240,23 +249,15 @@ void pafersbike::characteristicChanged(const QLowEnergyCharacteristic &character #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -324,7 +325,7 @@ void pafersbike::stateChanged(QLowEnergyService::ServiceState state) { &pafersbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -332,11 +333,14 @@ void pafersbike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -346,10 +350,11 @@ void pafersbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&pafersbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &pafersbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -443,10 +448,6 @@ bool pafersbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *pafersbike::VirtualBike() { return virtualBike; } - -void *pafersbike::VirtualDevice() { return VirtualBike(); } - uint16_t pafersbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/pafersbike.h b/src/pafersbike.h index db7394251..be79ada0e 100644 --- a/src/pafersbike.h +++ b/src/pafersbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,13 +36,10 @@ class pafersbike : public bike { Q_OBJECT public: pafersbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: const resistance_t max_resistance = 24; @@ -58,10 +54,9 @@ class pafersbike : public bike { void forceResistance(resistance_t requestResistance); double GetWattFromPacket(const QByteArray &packet); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/paferstreadmill.cpp b/src/paferstreadmill.cpp index a73d24bdf..8e8e4eda6 100644 --- a/src/paferstreadmill.cpp +++ b/src/paferstreadmill.cpp @@ -1,11 +1,14 @@ #include "paferstreadmill.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include #include #include #include +#include #include using namespace std::chrono_literals; @@ -51,11 +54,15 @@ void paferstreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(400ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -112,15 +119,16 @@ void paferstreadmill::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill) { + if (!firstInit && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &paferstreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &paferstreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); firstInit = 1; } } @@ -433,10 +441,6 @@ bool paferstreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *paferstreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *paferstreadmill::VirtualDevice() { return VirtualTreadMill(); } - bool paferstreadmill::autoPauseWhenSpeedIsZero() { if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) return true; diff --git a/src/paferstreadmill.h b/src/paferstreadmill.h index 0761478d3..cb563ec72 100644 --- a/src/paferstreadmill.h +++ b/src/paferstreadmill.h @@ -26,21 +26,17 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class paferstreadmill : public treadmill { Q_OBJECT public: paferstreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - double minStepInclination(); - double minStepSpeed(); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadMill(); - void *VirtualDevice(); + bool connected() override; + double minStepInclination() override; + double minStepSpeed() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -65,7 +61,6 @@ class paferstreadmill : public treadmill { int64_t lastStop = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/peloton.cpp b/src/peloton.cpp index a9b3dec7a..f91c03845 100644 --- a/src/peloton.cpp +++ b/src/peloton.cpp @@ -26,6 +26,223 @@ peloton::peloton(bluetooth *bl, QObject *parent) : QObject(parent) { return; } + rower_pace_offset = 1; + + rower_pace[0].value = -1; + rower_pace[0].display_name = QStringLiteral("Recovery"); + rower_pace[0].levels[0].fast_pace = 4.31; + rower_pace[0].levels[0].slow_pace = 15; + rower_pace[0].levels[0].display_name = QStringLiteral("Level 1"); + rower_pace[0].levels[0].slug = QStringLiteral("level_1"); + rower_pace[0].levels[1].fast_pace = 3.58; + rower_pace[0].levels[1].slow_pace = 15; + rower_pace[0].levels[1].display_name = QStringLiteral("Level 2"); + rower_pace[0].levels[1].slug = QStringLiteral("level_2"); + rower_pace[0].levels[2].fast_pace = 3.34; + rower_pace[0].levels[2].slow_pace = 15; + rower_pace[0].levels[2].display_name = QStringLiteral("Level 3"); + rower_pace[0].levels[2].slug = QStringLiteral("level_3"); + rower_pace[0].levels[3].fast_pace = 3.17; + rower_pace[0].levels[3].slow_pace = 15; + rower_pace[0].levels[3].display_name = QStringLiteral("Level 4"); + rower_pace[0].levels[3].slug = QStringLiteral("level_4"); + rower_pace[0].levels[4].fast_pace = 3.03; + rower_pace[0].levels[4].slow_pace = 15; + rower_pace[0].levels[4].display_name = QStringLiteral("Level 5"); + rower_pace[0].levels[4].slug = QStringLiteral("level_5"); + rower_pace[0].levels[5].fast_pace = 2.5; + rower_pace[0].levels[5].slow_pace = 15; + rower_pace[0].levels[5].display_name = QStringLiteral("Level 6"); + rower_pace[0].levels[5].slug = QStringLiteral("level_6"); + rower_pace[0].levels[6].fast_pace = 2.38; + rower_pace[0].levels[6].slow_pace = 15; + rower_pace[0].levels[6].display_name = QStringLiteral("Level 7"); + rower_pace[0].levels[6].slug = QStringLiteral("level_7"); + rower_pace[0].levels[7].fast_pace = 2.28; + rower_pace[0].levels[7].slow_pace = 15; + rower_pace[0].levels[7].display_name = QStringLiteral("Level 8"); + rower_pace[0].levels[7].slug = QStringLiteral("level_8"); + rower_pace[0].levels[8].fast_pace = 2.17; + rower_pace[0].levels[8].slow_pace = 15; + rower_pace[0].levels[8].display_name = QStringLiteral("Level 9"); + rower_pace[0].levels[8].slug = QStringLiteral("level_9"); + rower_pace[0].levels[9].fast_pace = 2.07; + rower_pace[0].levels[9].slow_pace = 15; + rower_pace[0].levels[9].display_name = QStringLiteral("Level 10"); + rower_pace[0].levels[9].slug = QStringLiteral("level_10"); + + rower_pace[1].value = 0; + rower_pace[1].display_name = QStringLiteral("Easy"); + rower_pace[1].levels[0].fast_pace = 3.51; + rower_pace[1].levels[0].slow_pace = 4.31; + rower_pace[1].levels[0].display_name = QStringLiteral("Level 1"); + rower_pace[1].levels[0].slug = QStringLiteral("level_1"); + rower_pace[1].levels[1].fast_pace = 3.22; + rower_pace[1].levels[1].slow_pace = 3.58; + rower_pace[1].levels[1].display_name = QStringLiteral("Level 2"); + rower_pace[1].levels[1].slug = QStringLiteral("level_2"); + rower_pace[1].levels[2].fast_pace = 3.02; + rower_pace[1].levels[2].slow_pace = 3.34; + rower_pace[1].levels[2].display_name = QStringLiteral("Level 3"); + rower_pace[1].levels[2].slug = QStringLiteral("level_3"); + rower_pace[1].levels[3].fast_pace = 2.47; + rower_pace[1].levels[3].slow_pace = 3.17; + rower_pace[1].levels[3].display_name = QStringLiteral("Level 4"); + rower_pace[1].levels[3].slug = QStringLiteral("level_4"); + rower_pace[1].levels[4].fast_pace = 2.36; + rower_pace[1].levels[4].slow_pace = 3.03; + rower_pace[1].levels[4].display_name = QStringLiteral("Level 5"); + rower_pace[1].levels[4].slug = QStringLiteral("level_5"); + rower_pace[1].levels[5].fast_pace = 2.24; + rower_pace[1].levels[5].slow_pace = 2.5; + rower_pace[1].levels[5].display_name = QStringLiteral("Level 6"); + rower_pace[1].levels[5].slug = QStringLiteral("level_6"); + rower_pace[1].levels[6].fast_pace = 2.14; + rower_pace[1].levels[6].slow_pace = 2.38; + rower_pace[1].levels[6].display_name = QStringLiteral("Level 7"); + rower_pace[1].levels[6].slug = QStringLiteral("level_7"); + rower_pace[1].levels[7].fast_pace = 2.06; + rower_pace[1].levels[7].slow_pace = 2.28; + rower_pace[1].levels[7].display_name = QStringLiteral("Level 8"); + rower_pace[1].levels[7].slug = QStringLiteral("level_8"); + rower_pace[1].levels[8].fast_pace = 1.56; + rower_pace[1].levels[8].slow_pace = 2.17; + rower_pace[1].levels[8].display_name = QStringLiteral("Level 9"); + rower_pace[1].levels[8].slug = QStringLiteral("level_9"); + rower_pace[1].levels[9].fast_pace = 1.48; + rower_pace[1].levels[9].slow_pace = 2.07; + rower_pace[1].levels[9].display_name = QStringLiteral("Level 10"); + rower_pace[1].levels[9].slug = QStringLiteral("level_10"); + + rower_pace[2].value = 1; + rower_pace[2].display_name = QStringLiteral("Moderate"); + rower_pace[2].levels[0].fast_pace = 3.35; + rower_pace[2].levels[0].slow_pace = 3.51; + rower_pace[2].levels[0].display_name = QStringLiteral("Level 1"); + rower_pace[2].levels[0].slug = QStringLiteral("level_1"); + rower_pace[2].levels[1].fast_pace = 3.09; + rower_pace[2].levels[1].slow_pace = 3.22; + rower_pace[2].levels[1].display_name = QStringLiteral("Level 2"); + rower_pace[2].levels[1].slug = QStringLiteral("level_2"); + rower_pace[2].levels[2].fast_pace = 2.5; + rower_pace[2].levels[2].slow_pace = 3.02; + rower_pace[2].levels[2].display_name = QStringLiteral("Level 3"); + rower_pace[2].levels[2].slug = QStringLiteral("level_3"); + rower_pace[2].levels[3].fast_pace = 2.36; + rower_pace[2].levels[3].slow_pace = 2.47; + rower_pace[2].levels[3].display_name = QStringLiteral("Level 4"); + rower_pace[2].levels[3].slug = QStringLiteral("level_4"); + rower_pace[2].levels[4].fast_pace = 2.25; + rower_pace[2].levels[4].slow_pace = 2.36; + rower_pace[2].levels[4].display_name = QStringLiteral("Level 5"); + rower_pace[2].levels[4].slug = QStringLiteral("level_5"); + rower_pace[2].levels[5].fast_pace = 2.15; + rower_pace[2].levels[5].slow_pace = 2.24; + rower_pace[2].levels[5].display_name = QStringLiteral("Level 6"); + rower_pace[2].levels[5].slug = QStringLiteral("level_6"); + rower_pace[2].levels[6].fast_pace = 2.05; + rower_pace[2].levels[6].slow_pace = 2.14; + rower_pace[2].levels[6].display_name = QStringLiteral("Level 7"); + rower_pace[2].levels[6].slug = QStringLiteral("level_7"); + rower_pace[2].levels[7].fast_pace = 1.57; + rower_pace[2].levels[7].slow_pace = 2.06; + rower_pace[2].levels[7].display_name = QStringLiteral("Level 8"); + rower_pace[2].levels[7].slug = QStringLiteral("level_8"); + rower_pace[2].levels[8].fast_pace = 1.49; + rower_pace[2].levels[8].slow_pace = 1.57; + rower_pace[2].levels[8].display_name = QStringLiteral("Level 9"); + rower_pace[2].levels[8].slug = QStringLiteral("level_9"); + rower_pace[2].levels[9].fast_pace = 1.41; + rower_pace[2].levels[9].slow_pace = 1.48; + rower_pace[2].levels[9].display_name = QStringLiteral("Level 10"); + rower_pace[2].levels[9].slug = QStringLiteral("level_10"); + + rower_pace[3].value = 2; + rower_pace[3].display_name = QStringLiteral("Challenging"); + rower_pace[3].levels[0].fast_pace = 3.17; + rower_pace[3].levels[0].slow_pace = 3.35; + rower_pace[3].levels[0].display_name = QStringLiteral("Level 1"); + rower_pace[3].levels[0].slug = QStringLiteral("level_1"); + rower_pace[3].levels[1].fast_pace = 2.52; + rower_pace[3].levels[1].slow_pace = 3.09; + rower_pace[3].levels[1].display_name = QStringLiteral("Level 2"); + rower_pace[3].levels[1].slug = QStringLiteral("level_2"); + rower_pace[3].levels[2].fast_pace = 2.35; + rower_pace[3].levels[2].slow_pace = 2.5; + rower_pace[3].levels[2].display_name = QStringLiteral("Level 3"); + rower_pace[3].levels[2].slug = QStringLiteral("level_3"); + rower_pace[3].levels[3].fast_pace = 2.23; + rower_pace[3].levels[3].slow_pace = 2.36; + rower_pace[3].levels[3].display_name = QStringLiteral("Level 4"); + rower_pace[3].levels[3].slug = QStringLiteral("level_4"); + rower_pace[3].levels[4].fast_pace = 2.13; + rower_pace[3].levels[4].slow_pace = 2.25; + rower_pace[3].levels[4].display_name = QStringLiteral("Level 5"); + rower_pace[3].levels[4].slug = QStringLiteral("level_5"); + rower_pace[3].levels[5].fast_pace = 2.03; + rower_pace[3].levels[5].slow_pace = 2.15; + rower_pace[3].levels[5].display_name = QStringLiteral("Level 6"); + rower_pace[3].levels[5].slug = QStringLiteral("level_6"); + rower_pace[3].levels[6].fast_pace = 1.54; + rower_pace[3].levels[6].slow_pace = 2.05; + rower_pace[3].levels[6].display_name = QStringLiteral("Level 7"); + rower_pace[3].levels[6].slug = QStringLiteral("level_7"); + rower_pace[3].levels[7].fast_pace = 1.47; + rower_pace[3].levels[7].slow_pace = 1.57; + rower_pace[3].levels[7].display_name = QStringLiteral("Level 8"); + rower_pace[3].levels[7].slug = QStringLiteral("level_8"); + rower_pace[3].levels[8].fast_pace = 1.4; + rower_pace[3].levels[8].slow_pace = 1.49; + rower_pace[3].levels[8].display_name = QStringLiteral("Level 9"); + rower_pace[3].levels[8].slug = QStringLiteral("level_9"); + rower_pace[3].levels[9].fast_pace = 1.32; + rower_pace[3].levels[9].slow_pace = 1.41; + rower_pace[3].levels[9].display_name = QStringLiteral("Level 10"); + rower_pace[3].levels[9].slug = QStringLiteral("level_10"); + + rower_pace[4].value = 3; + rower_pace[4].display_name = QStringLiteral("Max"); + rower_pace[4].levels[0].fast_pace = 3.06; + rower_pace[4].levels[0].slow_pace = 3.17; + rower_pace[4].levels[0].display_name = QStringLiteral("Level 1"); + rower_pace[4].levels[0].slug = QStringLiteral("level_1"); + rower_pace[4].levels[1].fast_pace = 2.42; + rower_pace[4].levels[1].slow_pace = 2.52; + rower_pace[4].levels[1].display_name = QStringLiteral("Level 2"); + rower_pace[4].levels[1].slug = QStringLiteral("level_2"); + rower_pace[4].levels[2].fast_pace = 2.26; + rower_pace[4].levels[2].slow_pace = 2.35; + rower_pace[4].levels[2].display_name = QStringLiteral("Level 3"); + rower_pace[4].levels[2].slug = QStringLiteral("level_3"); + rower_pace[4].levels[3].fast_pace = 2.15; + rower_pace[4].levels[3].slow_pace = 2.23; + rower_pace[4].levels[3].display_name = QStringLiteral("Level 4"); + rower_pace[4].levels[3].slug = QStringLiteral("level_4"); + rower_pace[4].levels[4].fast_pace = 2.05; + rower_pace[4].levels[4].slow_pace = 2.13; + rower_pace[4].levels[4].display_name = QStringLiteral("Level 5"); + rower_pace[4].levels[4].slug = QStringLiteral("level_5"); + rower_pace[4].levels[5].fast_pace = 1.56; + rower_pace[4].levels[5].slow_pace = 2.03; + rower_pace[4].levels[5].display_name = QStringLiteral("Level 6"); + rower_pace[4].levels[5].slug = QStringLiteral("level_6"); + rower_pace[4].levels[6].fast_pace = 1.48; + rower_pace[4].levels[6].slow_pace = 1.54; + rower_pace[4].levels[6].display_name = QStringLiteral("Level 7"); + rower_pace[4].levels[6].slug = QStringLiteral("level_7"); + rower_pace[4].levels[7].fast_pace = 1.41; + rower_pace[4].levels[7].slow_pace = 1.47; + rower_pace[4].levels[7].display_name = QStringLiteral("Level 8"); + rower_pace[4].levels[7].slug = QStringLiteral("level_8"); + rower_pace[4].levels[8].fast_pace = 1.34; + rower_pace[4].levels[8].slow_pace = 1.4; + rower_pace[4].levels[8].display_name = QStringLiteral("Level 9"); + rower_pace[4].levels[8].slug = QStringLiteral("level_9"); + rower_pace[4].levels[9].fast_pace = 1.27; + rower_pace[4].levels[9].slow_pace = 1.32; + rower_pace[4].levels[9].display_name = QStringLiteral("Level 10"); + rower_pace[4].levels[9].slug = QStringLiteral("level_10"); + connect(timer, &QTimer::timeout, this, &peloton::startEngine); PZP = new powerzonepack(bl, this); @@ -109,6 +326,7 @@ void peloton::login_onfinish(QNetworkReply *reply) { peloton_credentials_wrong = true; qDebug() << QStringLiteral("invalid peloton credentials during login ") << status; + emit loginState(false); return; } @@ -365,7 +583,9 @@ void peloton::ride_onfinish(QNetworkReply *reply) { } bool atLeastOnePower = false; - if (trainrows.empty() && !segments_segment_list.isEmpty()) { + if (trainrows.empty() && !segments_segment_list.isEmpty() && + bluetoothManager->device()->deviceType() != bluetoothdevice::ROWING && + bluetoothManager->device()->deviceType() != bluetoothdevice::TREADMILL) { foreach (QJsonValue o, segments_segment_list) { QJsonArray subsegments_v2 = o["subsegments_v2"].toArray(); if (!subsegments_v2.isEmpty()) { @@ -375,9 +595,13 @@ void peloton::ride_onfinish(QNetworkReply *reply) { int len = s["length"].toInt(); if (!zone.toUpper().compare(QStringLiteral("SPIN UPS")) || !zone.toUpper().compare(QStringLiteral("SPIN-UPS"))) { - bool peloton_spinups_autoresistance = settings.value(QZSettings::peloton_spinups_autoresistance, QZSettings::default_peloton_spinups_autoresistance).toBool(); + bool peloton_spinups_autoresistance = + settings + .value(QZSettings::peloton_spinups_autoresistance, + QZSettings::default_peloton_spinups_autoresistance) + .toBool(); uint32_t Duration = len; - if(peloton_spinups_autoresistance) { + if (peloton_spinups_autoresistance) { double PowerLow = 0.5; double PowerHigh = 0.83; for (uint32_t i = 0; i < Duration; i++) { @@ -426,6 +650,14 @@ void peloton::ride_onfinish(QNetworkReply *reply) { trainrows.append(row); atLeastOnePower = true; } + } else if (!zone.toUpper().compare(QStringLiteral("RECOVERY"))) { + r.duration = QTime(0, len / 60, len % 60, 0); + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.45; + if (r.power != -1) { + atLeastOnePower = true; + } + trainrows.append(r); + qDebug() << r.duration << "power" << r.power; } else if (!zone.toUpper().compare(QStringLiteral("FLAT ROAD"))) { r.duration = QTime(0, len / 60, len % 60, 0); r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.50; @@ -506,6 +738,16 @@ void peloton::ride_onfinish(QNetworkReply *reply) { } trainrows.append(r); qDebug() << r.duration << "power" << r.power; + } else { + if(len > 0 && atLeastOnePower) { + r.duration = QTime(0, len / 60, len % 60, 0); + r.power = -1; + if (r.power != -1) { + atLeastOnePower = true; + } + qDebug() << "ERROR not handled!" << zone; + trainrows.append(r); + } } } } @@ -514,6 +756,59 @@ void peloton::ride_onfinish(QNetworkReply *reply) { if (!atLeastOnePower) { trainrows.clear(); } + } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { + QJsonObject target_metrics_data_list = ride[QStringLiteral("target_metrics_data")].toObject(); + QJsonArray pace_intensities_list = target_metrics_data_list[QStringLiteral("pace_intensities")].toArray(); + + int pace_count = 0; + rower_pace_offset = 0; + + foreach (QJsonValue o, pace_intensities_list) { + if(o["value"].toInt() < 0) { + if(abs(o["value"].toInt()) > rower_pace_offset) + rower_pace_offset = abs(o["value"].toInt()); + } + } + + qDebug() << "rower_pace_offset" << rower_pace_offset; + + foreach (QJsonValue o, pace_intensities_list) { + qDebug() << o; + pace_count = o["value"].toInt() + rower_pace_offset; + if (pace_count < 5 && pace_count >= 0) { + rower_pace[pace_count].display_name = o["display_name"].toString(); + rower_pace[pace_count].value = o["value"].toInt(); + + QJsonArray levels = o["pace_levels"].toArray(); + if (levels.count() > 10) { + qDebug() << "peloton pace levels had been changed!"; + } + int count = 0; + foreach (QJsonValue level, levels) { + if(level["slug"].toString().split("_").count() > 1 ) { + count = level["slug"].toString().split("_")[1].toInt() - 1; + if (count >= 0 && count < 11) { + rower_pace[pace_count].levels[count].fast_pace = level["fast_pace"].toDouble(); + rower_pace[pace_count].levels[count].slow_pace = level["slow_pace"].toDouble(); + rower_pace[pace_count].levels[count].display_name = level["display_name"].toString(); + rower_pace[pace_count].levels[count].slug = level["slug"].toString(); + + qDebug() << count << level << rower_pace[pace_count].levels[count].display_name + << rower_pace[pace_count].levels[count].fast_pace + << rower_pace[pace_count].levels[count].slow_pace + << rower_pace[pace_count].levels[count].slug; + } else { + qDebug() << level["slug"].toString() << "slug error"; + } + } else { + qDebug() << level["slug"].toString() << "slug count error"; + } + } + qDebug() << pace_count << rower_pace[pace_count].display_name << rower_pace[pace_count].value; + } else { + qDebug() << "pace_count error!"; + } + } } if (log_request) { @@ -622,7 +917,7 @@ void peloton::performance_onfinish(QNetworkReply *reply) { r.upper_inclination = inc_upper; trainrows.append(r); qDebug() << i << r.duration << r.speed << r.inclination; - } else if (segment_type.contains("floor")) { + } else if (segment_type.contains("floor") || segment_type.contains("free_mode")) { int offset_start = offset[QStringLiteral("start")].toInt(); int offset_end = offset[QStringLiteral("end")].toInt(); trainrow r; @@ -632,6 +927,78 @@ void peloton::performance_onfinish(QNetworkReply *reply) { qDebug() << i << r.duration << r.speed << r.inclination; } } + } else if (!target_metrics_performance_data.isEmpty() && bluetoothManager->device() && + bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { + QJsonArray target_metrics = target_metrics_performance_data[QStringLiteral("target_metrics")].toArray(); + trainrows.reserve(target_metrics.count() + 2); + for (int i = 0; i < target_metrics.count(); i++) { + QJsonObject metrics = target_metrics.at(i).toObject(); + QJsonArray metrics_ar = metrics[QStringLiteral("metrics")].toArray(); + QJsonObject offset = metrics[QStringLiteral("offsets")].toObject(); + QString segment_type = metrics[QStringLiteral("segment_type")].toString(); + if (metrics_ar.count() > 1 && !offset.isEmpty()) { + QJsonObject strokes_rate = metrics_ar.at(0).toObject(); + QJsonObject pace_intensity = metrics_ar.at(1).toObject(); + int peloton_rower_level = + settings.value(QZSettings::peloton_rower_level, QZSettings::default_peloton_rower_level).toInt() - + 1; + double strokes_rate_lower = strokes_rate[QStringLiteral("lower")].toDouble(); + double strokes_rate_upper = strokes_rate[QStringLiteral("upper")].toDouble(); + int pace_intensity_lower = pace_intensity[QStringLiteral("lower")].toInt() + rower_pace_offset; + int pace_intensity_upper = pace_intensity[QStringLiteral("upper")].toInt() + rower_pace_offset; + int offset_start = offset[QStringLiteral("start")].toInt(); + int offset_end = offset[QStringLiteral("end")].toInt(); + double strokes_rate_average = ((strokes_rate_upper - strokes_rate_lower) / 2.0) + strokes_rate_lower; + trainrow r; + r.duration = QTime(0, 0, 0, 0); + r.duration = r.duration.addSecs((offset_end - offset_start) + 1); + if (!difficulty.toUpper().compare(QStringLiteral("LOWER"))) { + r.cadence = strokes_rate_lower; + } else if (!difficulty.toUpper().compare(QStringLiteral("UPPER"))) { + r.cadence = strokes_rate_upper; + } else { + r.cadence = ((strokes_rate_upper - strokes_rate_lower) / 2.0) + strokes_rate_lower; + } + + if (pace_intensity_lower >= 0 && pace_intensity_lower < 5) { + r.average_speed = + (rowerpaceToSpeed(rower_pace[pace_intensity_lower].levels[peloton_rower_level].fast_pace) + + rowerpaceToSpeed(rower_pace[pace_intensity_lower].levels[peloton_rower_level].slow_pace)) / + 2.0; + r.upper_speed = + rowerpaceToSpeed(rower_pace[pace_intensity_lower].levels[peloton_rower_level].fast_pace); + r.lower_speed = + rowerpaceToSpeed(rower_pace[pace_intensity_lower].levels[peloton_rower_level].slow_pace); + + if (!difficulty.toUpper().compare(QStringLiteral("LOWER"))) { + r.pace_intensity = pace_intensity_lower; + r.speed = r.lower_speed; + } else if (!difficulty.toUpper().compare(QStringLiteral("UPPER"))) { + r.pace_intensity = pace_intensity_upper; + r.speed = r.upper_speed; + } else { + r.pace_intensity = (pace_intensity_upper + pace_intensity_lower) / 2; + r.speed = r.average_speed; + } + r.forcespeed = 1; + } + + r.lower_cadence = strokes_rate_lower; + r.average_cadence = strokes_rate_average; + r.upper_cadence = strokes_rate_upper; + + trainrows.append(r); + qDebug() << i << r.duration << r.cadence << r.speed << r.upper_speed << r.lower_speed; + } else if (segment_type.contains("floor") || segment_type.contains("free_mode")) { + int offset_start = offset[QStringLiteral("start")].toInt(); + int offset_end = offset[QStringLiteral("end")].toInt(); + trainrow r; + r.duration = QTime(0, 0, 0, 0); + r.duration = r.duration.addSecs((offset_end - offset_start) + 1); + trainrows.append(r); + qDebug() << i << r.duration << r.cadence; + } + } } // Target METS it's quite useless so I removed, no one use this /* else if (!segment_list.isEmpty() && bluetoothManager->device()->deviceType() != bluetoothdevice::BIKE) { @@ -671,6 +1038,17 @@ void peloton::performance_onfinish(QNetworkReply *reply) { timer->start(30s); // check for a status changed } +double peloton::rowerpaceToSpeed(double pace) { + float whole, fractional; + + fractional = std::modf(pace, &whole); + double seconds = whole * 60.0; + seconds += (fractional * 100.0); + seconds *= 2.0; + + return 3600.0 / seconds; +} + void peloton::getInstructor(const QString &instructor_id) { connect(mgr, &QNetworkAccessManager::finished, this, &peloton::instructor_onfinish); diff --git a/src/peloton.h b/src/peloton.h index f330b6588..337890392 100644 --- a/src/peloton.h +++ b/src/peloton.h @@ -82,6 +82,24 @@ class peloton : public QObject { bool testMode = false; + // rowers + double rowerpaceToSpeed(double pace); + typedef struct _peloton_rower_pace_intensities_level { + QString display_name; + double fast_pace; + double slow_pace; + QString slug; + }_peloton_rower_pace_intensities_level; + + typedef struct _peloton_rower_pace_intensities { + QString display_name; + int value; + _peloton_rower_pace_intensities_level levels[10]; + } _peloton_rower_pace_intensities; + + _peloton_rower_pace_intensities rower_pace[5]; + int rower_pace_offset = 0; + private slots: void login_onfinish(QNetworkReply *reply); void workoutlist_onfinish(QNetworkReply *reply); diff --git a/src/pelotonbike.cpp b/src/pelotonbike.cpp index 0fc7f497b..2c17e9375 100644 --- a/src/pelotonbike.cpp +++ b/src/pelotonbike.cpp @@ -10,6 +10,7 @@ #include #include #include +#include "localipaddress.h" using namespace std::chrono_literals; @@ -24,14 +25,20 @@ pelotonbike::pelotonbike(bool noWriteResistance, bool noHeartService) { connect(refresh, &QTimer::timeout, this, &pelotonbike::update); refresh->start(200ms); - // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualBike) { + pelotonOCRsocket = new QUdpSocket(this); + bool result = pelotonOCRsocket->bind(QHostAddress::AnyIPv4, 8003); + qDebug() << result; + pelotonOCRprocessPendingDatagrams(); + connect(pelotonOCRsocket, SIGNAL(readyRead()), this, SLOT(pelotonOCRprocessPendingDatagrams())); + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &pelotonbike::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); firstStateChanged = 1; } } @@ -42,6 +49,41 @@ bool pelotonbike::inclinationAvailableByHardware() { return true; } void pelotonbike::forceResistance(double resistance) {} +void pelotonbike::pelotonOCRprocessPendingDatagrams() { + qDebug() << "in !"; + QHostAddress sender; + QSettings settings; + uint16_t port; + while (pelotonOCRsocket->hasPendingDatagrams()) { + QByteArray datagram; + datagram.resize(pelotonOCRsocket->pendingDatagramSize()); + pelotonOCRsocket->readDatagram(datagram.data(), datagram.size(), &sender, &port); + qDebug() << "PelotonOCR Message From :: " << sender.toString(); + qDebug() << "PelotonOCR Port From :: " << port; + qDebug() << "PelotonOCR Message :: " << datagram; + + QString s = datagram; + QStringList metrics = s.split(";"); + if(s.length() >= 5) { + m_watt = metrics.at(1).toDouble(); + qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts()); + Cadence = metrics.at(2).toDouble(); + qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value()); + Resistance = metrics.at(3).toDouble(); + qDebug() << QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()); + Speed = metrics.at(4).toDouble(); + qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value()); + } + + + QString url = "http://" + localipaddress::getIP(sender).toString() + ":" + + QString::number(settings.value("template_inner_QZWS_port", 6666).toInt()) + + "/floating/floating.htm"; + int r = pelotonOCRsocket->writeDatagram(QByteArray(url.toLatin1()), sender, 8003); + qDebug() << "url floating" << url << r; + } +} + void pelotonbike::update() { QSettings settings; @@ -52,13 +94,6 @@ void pelotonbike::update() { emit connectedAndDiscovered(); } -#ifdef Q_OS_ANDROID - QAndroidJniObject text = QAndroidJniObject::callStaticObjectMethod( - "org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastText"); - QString t = text.toString(); - qDebug() << "OCR" << t; -#endif - QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); @@ -81,23 +116,10 @@ void pelotonbike::update() { #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } - emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); - emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); - emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); - emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value())); emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value())); // debug("Current Distance: " + QString::number(distance)); @@ -132,4 +154,4 @@ void pelotonbike::changeInclinationRequested(double grade, double percentage) { bool pelotonbike::connected() { return true; } -void *pelotonbike::VirtualDevice() { return virtualBike; } + diff --git a/src/pelotonbike.h b/src/pelotonbike.h index 4c15cda40..13ebe70e8 100644 --- a/src/pelotonbike.h +++ b/src/pelotonbike.h @@ -29,19 +29,15 @@ class pelotonbike : public bike { Q_OBJECT public: pelotonbike(bool noWriteResistance, bool noHeartService); - bool connected(); - bool inclinationAvailableByHardware(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; + bool inclinationAvailableByHardware() override; private: void forceResistance(double resistance); - uint16_t watts(); + uint16_t watts() override; double getDouble(QString v); QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t sec1Update = 0; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); @@ -55,6 +51,8 @@ class pelotonbike : public bike { bool noWriteResistance = false; bool noHeartService = false; + QUdpSocket* pelotonOCRsocket = nullptr; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif @@ -63,7 +61,8 @@ class pelotonbike : public bike { void disconnected(); void debug(QString string); - private slots: +private slots: + void pelotonOCRprocessPendingDatagrams(); void changeInclinationRequested(double grade, double percentage); diff --git a/src/proformbike.cpp b/src/proformbike.cpp index 8150e5a11..78c44066b 100644 --- a/src/proformbike.cpp +++ b/src/proformbike.cpp @@ -1,6 +1,8 @@ #include "proformbike.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -39,12 +41,15 @@ void proformbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -156,6 +161,7 @@ void proformbike::forceResistance(resistance_t requestResistance) { settings .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) .toBool(); + bool proform_bike_sb = settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); if (proform_studio || proform_tdf_10) { const uint8_t res1[] = {0xfe, 0x02, 0x16, 0x03}; @@ -172,7 +178,7 @@ void proformbike::forceResistance(resistance_t requestResistance) { writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, false); writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, false); writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true); - } else if (proform_hybrid_trainer_PFEL03815) { + } else if (proform_hybrid_trainer_PFEL03815 || proform_bike_sb) { const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, 0x04, 0x32, 0x02, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00}; const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01, @@ -510,6 +516,11 @@ void proformbike::update() { .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) .toBool(); + bool proform_bike_sb = + settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); + bool proform_bike_PFEVEX71316_1 = + settings.value(QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1) + .toBool(); uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, @@ -523,6 +534,14 @@ void proformbike::update() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02}; + // proform_bike_sb + uint8_t noOpData2_proform_bike_sb[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, + 0x0d, 0x3c, 0x9e, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80}; + uint8_t noOpData3_proform_bike_sb[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x81, 0xb5, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData5_proform_bike_sb[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x00, + 0x03, 0x80, 0x00, 0x40, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x00}; + // proform_tour_de_france_clc uint8_t noOpData2_proform_tour_de_france_clc[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, 0x0f, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -570,9 +589,19 @@ void proformbike::update() { 0x0d, 0x02, 0x00, 0x07, 0xbc, 0x90, 0x70, 0x00, 0x00, 0x00, 0x40, 0x19, 0x00}; + // proform_bike_PFEVEX71316_1 + uint8_t noOpData2_proform_bike_PFEVEX71316_1[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x08, 0x15, 0x02, 0x00, + 0x0f, 0xb6, 0x10, 0x30, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3_proform_bike_PFEVEX71316_1[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData5_proform_bike_PFEVEX71316_1[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x08, 0x13, 0x02, 0x00, + 0x0d, 0x09, 0x8e, 0x41, 0x00, 0x00, 0x40, 0x50, 0x00, 0x80}; + uint8_t noOpData6_proform_bike_PFEVEX71316_1[] = {0xff, 0x05, 0x18, 0x00, 0x00, 0x81, 0xab, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + switch (counterPoll) { case 0: - if (nordictrack_gx_2_7 || proform_hybrid_trainer_PFEL03815) { + if (nordictrack_gx_2_7 || proform_hybrid_trainer_PFEL03815 || proform_bike_sb) { writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp")); } else { writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); @@ -593,6 +622,12 @@ void proformbike::update() { else if (proform_cycle_trainer_400) writeCharacteristic(noOpData2_proform_cycle_trainer_400, sizeof(noOpData2_proform_cycle_trainer_400), QStringLiteral("noOp")); + else if (proform_bike_sb) + writeCharacteristic(noOpData2_proform_bike_sb, sizeof(noOpData2_proform_bike_sb), + QStringLiteral("noOp")); + else if (proform_bike_PFEVEX71316_1) + writeCharacteristic(noOpData2_proform_bike_PFEVEX71316_1, sizeof(noOpData2_proform_bike_PFEVEX71316_1), + QStringLiteral("noOp")); else writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp")); break; @@ -611,6 +646,12 @@ void proformbike::update() { } else if (proform_cycle_trainer_400) writeCharacteristic(noOpData3_proform_cycle_trainer_400, sizeof(noOpData3_proform_cycle_trainer_400), QStringLiteral("noOp")); + else if (proform_bike_sb) + writeCharacteristic(noOpData3_proform_bike_sb, sizeof(noOpData3_proform_bike_sb), + QStringLiteral("noOp")); + else if (proform_bike_PFEVEX71316_1) + writeCharacteristic(noOpData3_proform_bike_PFEVEX71316_1, sizeof(noOpData3_proform_bike_PFEVEX71316_1), + QStringLiteral("noOp")); else writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp")); break; @@ -624,6 +665,9 @@ void proformbike::update() { innerWriteResistance(); writeCharacteristic(noOpData4_proform_hybrid_trainer_PFEL03815, sizeof(noOpData4_proform_hybrid_trainer_PFEL03815), QStringLiteral("noOp")); + } else if (proform_bike_sb) { + innerWriteResistance(); + writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp")); } else writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp")); break; @@ -636,7 +680,13 @@ void proformbike::update() { } else if (proform_hybrid_trainer_PFEL03815) { writeCharacteristic(noOpData5_proform_hybrid_trainer_PFEL03815, sizeof(noOpData5_proform_hybrid_trainer_PFEL03815), QStringLiteral("noOp")); - } else + } else if (proform_bike_sb) + writeCharacteristic(noOpData5_proform_bike_sb, sizeof(noOpData5_proform_bike_sb), + QStringLiteral("noOp")); + else if (proform_bike_PFEVEX71316_1) + writeCharacteristic(noOpData5_proform_bike_PFEVEX71316_1, sizeof(noOpData5_proform_bike_PFEVEX71316_1), + QStringLiteral("noOp")); + else writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp")); break; case 5: @@ -648,6 +698,9 @@ void proformbike::update() { else if (proform_cycle_trainer_400) writeCharacteristic(noOpData6_proform_cycle_trainer_400, sizeof(noOpData6_proform_cycle_trainer_400), QStringLiteral("noOp")); + else if (proform_bike_PFEVEX71316_1) + writeCharacteristic(noOpData6_proform_bike_PFEVEX71316_1, sizeof(noOpData6_proform_bike_PFEVEX71316_1), + QStringLiteral("noOp")); else writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp")); break; @@ -675,11 +728,12 @@ void proformbike::update() { counterPoll++; if (counterPoll > 6) { counterPoll = 0; - } else if (counterPoll == 6 && (proform_tour_de_france_clc || proform_cycle_trainer_400) && + } else if (counterPoll == 6 && + (proform_tour_de_france_clc || proform_cycle_trainer_400 || proform_bike_PFEVEX71316_1) && requestResistance == -1) { // this bike sends the frame noOpData7 only when it needs to change the resistance counterPoll = 0; - } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_hybrid_trainer_PFEL03815)) { + } else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_hybrid_trainer_PFEL03815 || proform_bike_sb)) { counterPoll = 0; } @@ -788,6 +842,9 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte settings .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) .toBool(); + bool proform_bike_sb = settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); + bool proform_bike_PFEVEX71316_1 = + settings.value(QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1).toBool(); emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); @@ -834,6 +891,43 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte Cadence = ((uint8_t)newValue.at(2)); } } + } else if (proform_bike_PFEVEX71316_1) { + if (newValue.length() == 20 && newValue.at(0) == 0x00 && newValue.at(1) == 0x12 && newValue.at(2) == 0x01 && + newValue.at(3) == 0x04 && newValue.at(4) == 0x02 && newValue.at(5) == 0x2f && newValue.at(6) == 0x08 && + newValue.at(7) == 0x2f && newValue.at(8) == 0x02 && newValue.at(9) == 0x02 && newValue.at(10) == 0x00 && + newValue.at(11) == 0x00 && newValue.at(14) == 0x5a) { + m_watt = ((uint16_t)(((uint8_t)newValue.at(13)) << 8) + (uint16_t)((uint8_t)newValue.at(12))); + } else if (newValue.length() == 20 && newValue.at(0) == 0x00 && newValue.at(1) == 0x12 && + newValue.at(2) == 0x01 && newValue.at(3) == 0x04 && newValue.at(4) == 0x02 && + newValue.at(5) == 0x2f && newValue.at(6) == 0x08 && newValue.at(7) == 0x2f && + newValue.at(8) == 0x02 && newValue.at(9) == 0x02 && newValue.at(10) != 0x00 && + newValue.at(11) != 0x00 && newValue.at(14) == 0x5a) { + + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = ((double)((uint16_t)(((uint8_t)newValue.at(13)) << 8) + (uint16_t)((uint8_t)newValue.at(12))) / + 100.0); + } else { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + + double incline = + ((double)((int16_t)(((int8_t)newValue.at(11)) << 8) + (int16_t)((uint8_t)newValue.at(10))) / 100.0); + + if ((uint16_t)(qAbs(incline) * 10) % 5 == 0) { + Inclination = incline; + emit debug(QStringLiteral("Current Inclination: ") + QString::number(incline)); + } else { + emit debug(QStringLiteral("Filtering bad inclination")); + } + + if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) { + Cadence = ((uint8_t)newValue.at(18)); + } + } } else { if (newValue.length() != 20 || newValue.at(0) != 0x00 || newValue.at(1) != 0x12 || newValue.at(2) != 0x01 || @@ -853,7 +947,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte if (m_watts > 3000) { m_watts = 0; } else { - if (proform_hybrid_trainer_PFEL03815) { + if (proform_hybrid_trainer_PFEL03815 || proform_bike_sb) { switch ((uint8_t)newValue.at(11)) { case 0: Resistance = 0; @@ -914,6 +1008,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte Resistance = 13; m_pelotonResistance = 80; break; + case 0x21: case 0x22: Resistance = 14; m_pelotonResistance = 90; @@ -922,6 +1017,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte Resistance = 15; m_pelotonResistance = 95; break; + case 0x26: case 0x27: Resistance = 16; m_pelotonResistance = 100; @@ -946,6 +1042,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte m_pelotonResistance = 30; break; case 0x0b: + case 0x0c: Resistance = 5; m_pelotonResistance = 35; break; @@ -954,6 +1051,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte m_pelotonResistance = 40; break; case 0x10: + case 0x11: Resistance = 7; m_pelotonResistance = 45; break; @@ -982,6 +1080,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte m_pelotonResistance = 75; break; case 0x21: + case 0x22: Resistance = 14; m_pelotonResistance = 80; break; @@ -990,6 +1089,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte m_pelotonResistance = 85; break; case 0x26: + case 0x27: Resistance = 16; m_pelotonResistance = 100; break; @@ -1133,16 +1233,7 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -1183,6 +1274,7 @@ void proformbike::btinit() { settings .value(QZSettings::proform_hybrid_trainer_PFEL03815, QZSettings::default_proform_hybrid_trainer_PFEL03815) .toBool(); + bool proform_bike_sb = settings.value(QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb).toBool(); if (settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool()) { @@ -1244,7 +1336,9 @@ void proformbike::btinit() { QThread::msleep(400); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); QThread::msleep(400); - } else if (settings.value(QZSettings::proform_tdf_10, QZSettings::default_proform_tdf_10).toBool()) { + } else if (settings.value(QZSettings::proform_tdf_10, QZSettings::default_proform_tdf_10).toBool() || + settings.value(QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1) + .toBool()) { max_resistance = 26; uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; @@ -1290,19 +1384,36 @@ void proformbike::btinit() { writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); QThread::msleep(400); - uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x08, 0x28, 0x90, 0x04, - 0x00, 0x40, 0x0c, 0xca, 0x9e, 0x64, 0x38, 0xf6, 0xda, 0xa8}; - uint8_t initData11[] = {0x01, 0x12, 0x74, 0x42, 0x26, 0x0c, 0xf0, 0xae, 0xb2, 0x90, - 0x7c, 0x5a, 0x2e, 0x34, 0x08, 0xe6, 0xea, 0xf8, 0xc4, 0xd2}; - uint8_t initData12[] = {0xff, 0x08, 0xd6, 0xdc, 0xe0, 0x88, 0x02, 0x00, 0x00, 0x0e, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + if (settings.value(QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1) + .toBool()) { + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x08, 0x28, 0x90, 0x04, + 0x00, 0xc1, 0x58, 0xfd, 0x90, 0x31, 0xd0, 0x75, 0x28, 0xc1}; + uint8_t initData11[] = {0x01, 0x12, 0x78, 0x2d, 0xc0, 0x71, 0x20, 0xf5, 0x88, 0x41, + 0x18, 0xdd, 0x90, 0x51, 0x10, 0xd5, 0x88, 0x41, 0x38, 0xed}; + uint8_t initData12[] = {0xff, 0x08, 0xa0, 0x91, 0x40, 0x88, 0x02, 0x00, 0x00, 0x21, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(400); + } else { + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x08, 0x28, 0x90, 0x04, + 0x00, 0x40, 0x0c, 0xca, 0x9e, 0x64, 0x38, 0xf6, 0xda, 0xa8}; + uint8_t initData11[] = {0x01, 0x12, 0x74, 0x42, 0x26, 0x0c, 0xf0, 0xae, 0xb2, 0x90, + 0x7c, 0x5a, 0x2e, 0x34, 0x08, 0xe6, 0xea, 0xf8, 0xc4, 0xd2}; + uint8_t initData12[] = {0xff, 0x08, 0xd6, 0xdc, 0xe0, 0x88, 0x02, 0x00, 0x00, 0x0e, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(400); + } } else { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; @@ -1358,6 +1469,22 @@ void proformbike::btinit() { uint8_t initData12[] = {0xff, 0x08, 0x70, 0xf9, 0x40, 0x98, 0x02, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(400); + } else if (proform_bike_sb) { + max_resistance = 16; + + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07, + 0x01, 0x86, 0x64, 0x38, 0x1a, 0xfa, 0xe8, 0xcc, 0xa6, 0x9e}; + uint8_t initData11[] = {0x01, 0x12, 0x8c, 0x60, 0x52, 0x42, 0x50, 0x24, 0x3e, 0x36, + 0x34, 0x08, 0x0a, 0x0a, 0x38, 0x3c, 0x36, 0x2e, 0x5c, 0x50}; + uint8_t initData12[] = {0xff, 0x08, 0x42, 0x72, 0xa0, 0x88, 0x02, 0x00, 0x00, 0x0f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); QThread::msleep(400); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); @@ -1459,7 +1586,7 @@ void proformbike::stateChanged(QLowEnergyService::ServiceState state) { &proformbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -1484,10 +1611,11 @@ void proformbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&proformbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &proformbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -1581,10 +1709,6 @@ bool proformbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *proformbike::VirtualBike() { return virtualBike; } - -void *proformbike::VirtualDevice() { return VirtualBike(); } - uint16_t proformbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/proformbike.h b/src/proformbike.h index 3881bd435..f39c7b2d0 100644 --- a/src/proformbike.h +++ b/src/proformbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,14 +36,11 @@ class proformbike : public bike { Q_OBJECT public: proformbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool inclinationAvailableByHardware(); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool inclinationAvailableByHardware() override; + bool connected() override; private: resistance_t max_resistance = 16; @@ -56,13 +52,12 @@ class proformbike : public bike { bool wait_for_response = false); void startDiscover(); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(resistance_t requestResistance); void forceIncline(double incline); void innerWriteResistance(); QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; uint8_t bikeResistanceOffset = 4; double bikeResistanceGain = 1.0; diff --git a/src/proformelliptical.cpp b/src/proformelliptical.cpp index 9c787275a..32bb317f1 100644 --- a/src/proformelliptical.cpp +++ b/src/proformelliptical.cpp @@ -1,6 +1,8 @@ #include "proformelliptical.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -36,12 +38,15 @@ void proformelliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, con timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -85,7 +90,7 @@ void proformelliptical::update() { case 3: writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"), true); if (requestInclination != -100) { - if(requestInclination < 0) + if (requestInclination < 0) requestInclination = 0; if (requestInclination != currentInclination().value() && requestInclination >= 0 && requestInclination <= 15) { @@ -209,7 +214,13 @@ void proformelliptical::characteristicChanged(const QLowEnergyCharacteristic &ch if (newValue.length() == 20 && newValue.at(0) == 0x01 && newValue.at(1) == 0x12 && ((newValue.at(2) == 0x46) || (newValue.at(2) == 0x5a))) { - Speed = (double)(((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t)newValue.at(14))) / 100.0; + double speed_from_machinery = + (double)(((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t)newValue.at(14))) / 100.0; + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = speed_from_machinery; + } else { + Speed = speedFromWatts(); + } emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); return; } @@ -225,6 +236,7 @@ void proformelliptical::characteristicChanged(const QLowEnergyCharacteristic &ch Resistance = GetResistanceFromPacket(newValue); Cadence = (newValue.at(18) * cadence_gain) + cadence_offset; + m_watt = (double)(((uint16_t)((uint8_t)newValue.at(13)) << 8) + (uint16_t)((uint8_t)newValue.at(12))); if (watts()) KCal += ((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( @@ -243,16 +255,7 @@ void proformelliptical::characteristicChanged(const QLowEnergyCharacteristic &ch #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -394,21 +397,26 @@ void proformelliptical::stateChanged(QLowEnergyService::ServiceState state) { // ******************************************* virtual treadmill init ************************************* QSettings settings; - if (!firstStateChanged && !virtualTreadmill && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstStateChanged && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &proformelliptical::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &proformelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &proformelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -504,10 +512,6 @@ bool proformelliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *proformelliptical::VirtualTreadmill() { return virtualTreadmill; } - -void *proformelliptical::VirtualDevice() { return VirtualTreadmill(); } - void proformelliptical::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { @@ -516,3 +520,5 @@ void proformelliptical::controllerStateChanged(QLowEnergyController::ControllerS m_control->connectToDevice(); } } + +uint16_t proformelliptical::watts() { return m_watt.value(); } diff --git a/src/proformelliptical.h b/src/proformelliptical.h index abe1eb336..1ccfcedde 100644 --- a/src/proformelliptical.h +++ b/src/proformelliptical.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -38,10 +36,7 @@ class proformelliptical : public elliptical { Q_OBJECT public: proformelliptical(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: double GetDistanceFromPacket(QByteArray packet); @@ -54,10 +49,9 @@ class proformelliptical : public elliptical { void sendPoll(); void forceIncline(double incline); void forceSpeed(double speed); + uint16_t watts() override; QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; diff --git a/src/proformellipticaltrainer.cpp b/src/proformellipticaltrainer.cpp index 3b176ebac..dde32b9b9 100644 --- a/src/proformellipticaltrainer.cpp +++ b/src/proformellipticaltrainer.cpp @@ -1,6 +1,9 @@ #include "proformellipticaltrainer.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -10,6 +13,7 @@ #include #include #include +#include using namespace std::chrono_literals; @@ -39,12 +43,15 @@ void proformellipticaltrainer::writeCharacteristic(uint8_t *data, uint8_t data_l timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -409,16 +416,7 @@ void proformellipticaltrainer::characteristicChanged(const QLowEnergyCharacteris #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -567,22 +565,27 @@ void proformellipticaltrainer::stateChanged(QLowEnergyService::ServiceState stat // ******************************************* virtual treadmill init ************************************* QSettings settings; - if (!firstStateChanged && !virtualTreadmill && !virtualBike) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + if (!firstStateChanged && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &proformellipticaltrainer::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &proformellipticaltrainer::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, - bikeResistanceGain); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); connect(virtualBike, &virtualbike::changeInclination, this, &proformellipticaltrainer::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -681,10 +684,6 @@ bool proformellipticaltrainer::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *proformellipticaltrainer::VirtualTreadmill() { return virtualTreadmill; } - -void *proformellipticaltrainer::VirtualDevice() { return VirtualTreadmill(); } - void proformellipticaltrainer::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/proformellipticaltrainer.h b/src/proformellipticaltrainer.h index a55e4231c..4683b61be 100644 --- a/src/proformellipticaltrainer.h +++ b/src/proformellipticaltrainer.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,11 +37,8 @@ class proformellipticaltrainer : public elliptical { public: proformellipticaltrainer(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); - int pelotonToEllipticalResistance(int pelotonResistance); + bool connected() override; + int pelotonToEllipticalResistance(int pelotonResistance) override; private: double GetDistanceFromPacket(QByteArray packet); @@ -59,8 +54,6 @@ class proformellipticaltrainer : public elliptical { void forceSpeed(double speed); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; uint8_t bikeResistanceOffset = 4; double bikeResistanceGain = 1.0; diff --git a/src/proformrower.cpp b/src/proformrower.cpp index bf9fbc49b..2c6d5613f 100644 --- a/src/proformrower.cpp +++ b/src/proformrower.cpp @@ -1,6 +1,9 @@ #include "proformrower.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" +#include "virtualrower.h" #include "virtualtreadmill.h" #include #include @@ -36,140 +39,277 @@ void proformrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QS timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); } void proformrower::forceResistance(resistance_t requestResistance) { - const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02, - 0x00, 0x10, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02, - 0x00, 0x10, 0x03, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02, - 0x00, 0x10, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x58, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xf9, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x9a, 0x09, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x3a, 0x0b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xdb, 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x7c, 0x0e, 0x00, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x1c, 0x10, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xbd, 0x11, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x5e, 0x13, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xfe, 0x14, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x9f, 0x16, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x40, 0x18, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xe0, 0x19, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x81, 0x1b, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x22, 0x1d, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xc2, 0x1e, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x63, 0x20, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x04, 0x22, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xa4, 0x23, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0x45, 0x25, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00}; - const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, - 0x04, 0xe6, 0x26, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}; - - switch (requestResistance) { - case 1: - writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true); - break; - case 2: - writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true); - break; - case 3: - writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true); - break; - case 4: - writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true); - break; - case 5: - writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true); - break; - case 6: - writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true); - break; - case 7: - writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true); - break; - case 8: - writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true); - break; - case 9: - writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true); - break; - case 10: - writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true); - break; - case 11: - writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true); - break; - case 12: - writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true); - break; - case 13: - writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true); - break; - case 14: - writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true); - break; - case 15: - writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true); - break; - case 16: - writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true); - break; - case 17: - writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true); - break; - case 18: - writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true); - break; - case 19: - writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true); - break; - case 20: - writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true); - break; - case 21: - writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true); - break; - case 22: - writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true); - break; - case 23: - writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true); - break; - case 24: - writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true); - break; + QSettings settings; + bool proform_rower_sport_rl = + settings.value(QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl).toBool(); + + if (proform_rower_sport_rl) { + const uint8_t unlock_res[] = {0xfe, 0x02, 0x0d, 0x02}; + + const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x76, 0x01, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x17, 0x03, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xb8, 0x04, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x58, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xf9, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x9a, 0x09, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x3a, 0x0b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xdb, 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x7c, 0x0e, 0x00, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x1c, 0x10, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xbd, 0x11, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x5e, 0x13, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xfe, 0x14, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x9f, 0x16, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x40, 0x18, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xe0, 0x19, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x81, 0x1b, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x22, 0x1d, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xc2, 0x1e, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x63, 0x20, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x04, 0x22, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xa4, 0x23, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x45, 0x25, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xe6, 0x26, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic((uint8_t*)unlock_res, sizeof(unlock_res), QStringLiteral("unlock_resistance"), false, false); + + switch (requestResistance) { + case 1: + writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true); + break; + case 2: + writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true); + break; + case 3: + writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true); + break; + case 4: + writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true); + break; + case 5: + writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true); + break; + case 6: + writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true); + break; + case 7: + writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true); + break; + case 8: + writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true); + break; + case 9: + writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true); + break; + case 10: + writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true); + break; + case 11: + writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true); + break; + case 12: + writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true); + break; + case 13: + writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true); + break; + case 14: + writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true); + break; + case 15: + writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true); + break; + case 16: + writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true); + break; + case 17: + writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true); + break; + case 18: + writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true); + break; + case 19: + writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true); + break; + case 20: + writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true); + break; + case 21: + writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true); + break; + case 22: + writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true); + break; + case 23: + writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true); + break; + case 24: + writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true); + break; + } + } else { + const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02, + 0x00, 0x10, 0x01, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02, + 0x00, 0x10, 0x03, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x02, + 0x00, 0x10, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x58, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xf9, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x9a, 0x09, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x3a, 0x0b, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xdb, 0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x7c, 0x0e, 0x00, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x1c, 0x10, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xbd, 0x11, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x5e, 0x13, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xfe, 0x14, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x9f, 0x16, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x40, 0x18, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xe0, 0x19, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res17[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x81, 0x1b, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res18[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x22, 0x1d, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res19[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xc2, 0x1e, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res20[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x63, 0x20, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res21[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x04, 0x22, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res22[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xa4, 0x23, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res23[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0x45, 0x25, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t res24[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x14, 0x09, 0x02, 0x01, + 0x04, 0xe6, 0x26, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}; + + switch (requestResistance) { + case 1: + writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true); + break; + case 2: + writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true); + break; + case 3: + writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true); + break; + case 4: + writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true); + break; + case 5: + writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true); + break; + case 6: + writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true); + break; + case 7: + writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true); + break; + case 8: + writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true); + break; + case 9: + writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true); + break; + case 10: + writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true); + break; + case 11: + writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true); + break; + case 12: + writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true); + break; + case 13: + writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true); + break; + case 14: + writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true); + break; + case 15: + writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true); + break; + case 16: + writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true); + break; + case 17: + writeCharacteristic((uint8_t *)res17, sizeof(res17), QStringLiteral("resistance17"), false, true); + break; + case 18: + writeCharacteristic((uint8_t *)res18, sizeof(res18), QStringLiteral("resistance18"), false, true); + break; + case 19: + writeCharacteristic((uint8_t *)res19, sizeof(res19), QStringLiteral("resistance19"), false, true); + break; + case 20: + writeCharacteristic((uint8_t *)res20, sizeof(res20), QStringLiteral("resistance20"), false, true); + break; + case 21: + writeCharacteristic((uint8_t *)res21, sizeof(res21), QStringLiteral("resistance21"), false, true); + break; + case 22: + writeCharacteristic((uint8_t *)res22, sizeof(res22), QStringLiteral("resistance22"), false, true); + break; + case 23: + writeCharacteristic((uint8_t *)res23, sizeof(res23), QStringLiteral("resistance23"), false, true); + break; + case 24: + writeCharacteristic((uint8_t *)res24, sizeof(res24), QStringLiteral("resistance24"), false, true); + break; + } } } @@ -189,6 +329,9 @@ void proformrower::update() { update_metrics(true, watts()); { + bool proform_rower_sport_rl = + settings.value(QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl).toBool(); + uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x14, 0x13, 0x02, 0x00, 0x0d, 0x1c, 0x9e, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80}; @@ -211,8 +354,16 @@ void proformrower::update() { writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp")); break; case 3: + if (requestResistance != -1 && proform_rower_sport_rl) { + if (requestResistance != currentResistance().value() && requestResistance >= 0 && + requestResistance <= max_resistance) { + emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); + forceResistance(requestResistance); + } + requestResistance = -1; + } writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"), true); - if (requestResistance != -1) { + if (requestResistance != -1 && !proform_rower_sport_rl) { if (requestResistance != currentResistance().value() && requestResistance >= 0 && requestResistance <= max_resistance) { emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); @@ -278,12 +429,14 @@ double proformrower::GetResistanceFromPacket(QByteArray packet) { case 6: return 4; case 7: + case 8: return 5; case 9: return 6; case 0x0b: return 7; case 0x0c: + case 0x0d: return 8; case 0x0e: return 9; @@ -294,12 +447,14 @@ double proformrower::GetResistanceFromPacket(QByteArray packet) { case 0x13: return 12; case 0x14: + case 0x15: return 13; case 0x16: return 14; case 0x18: return 15; case 0x19: + case 0x1a: return 16; case 0x1b: return 17; @@ -316,6 +471,7 @@ double proformrower::GetResistanceFromPacket(QByteArray packet) { case 0x25: return 23; case 0x26: + case 0x27: return 24; } return 1; @@ -334,9 +490,16 @@ void proformrower::characteristicChanged(const QLowEnergyCharacteristic &charact lastPacket = newValue; if (newValue.length() == 20 && (uint8_t)newValue.at(0) == 0xff && newValue.at(1) == 0x11) { - Cadence = newValue.at(12); + Cadence = (uint8_t)(newValue.at(12)); + StrokesCount += (Cadence.value()) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())) / 60000; emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); - Speed = (double)(((uint16_t)((uint8_t)newValue.at(14)) << 8) + (uint16_t)((uint8_t)newValue.at(13))) / 10.0; + emit debug(QStringLiteral("Strokes Count: ") + QString::number(StrokesCount.value())); + uint16_t s = (((uint16_t)((uint8_t)newValue.at(14)) << 8) + (uint16_t)((uint8_t)newValue.at(13))); + if (s > 0) + Speed = (60.0 / (double)(s)) * 30.0; + else + Speed = 0; emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); return; } @@ -371,16 +534,7 @@ void proformrower::characteristicChanged(const QLowEnergyCharacteristic &charact #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -397,6 +551,9 @@ void proformrower::characteristicChanged(const QLowEnergyCharacteristic &charact } void proformrower::btinit() { + QSettings settings; + bool proform_rower_sport_rl = + settings.value(QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl).toBool(); { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; @@ -414,6 +571,7 @@ void proformrower::btinit() { uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04}; + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x14, 0x28, 0x90, 0x07, 0x01, 0xed, 0xe8, 0xe9, 0xe8, 0xf5, 0xf0, 0x09, 0x00, 0x1d}; uint8_t initData11[] = {0x01, 0x12, 0x28, 0x39, 0x48, 0x55, 0x60, 0x99, 0xb0, 0xad, @@ -437,6 +595,13 @@ void proformrower::btinit() { uint8_t noOpData9[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x13, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData10_proform_rower_sport_rl[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x14, 0x28, 0x90, 0x07, 0x01, 0xf2, 0xf4, 0xf4, 0xf2, 0xfe, 0x08, 0x00, 0x1e, 0x2a}; + uint8_t initData11_proform_rower_sport_rl[] = {0x01, 0x12, 0x3c, 0x4c, 0x5a, 0x66, 0x90, 0x88, 0xa6, 0xc2, 0xe4, 0x04, 0x22, 0x4e, 0x98, 0xb0, 0xce, 0x1a, 0x2c, 0x7c}; + uint8_t initData12_proform_rower_sport_rl[] = {0xff, 0x08, 0x8a, 0xd6, 0x20, 0x98, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + uint8_t noOpData7_proform_rower_sport_rl[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x14, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData8_proform_rower_sport_rl[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); QThread::msleep(400); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); @@ -463,26 +628,53 @@ void proformrower::btinit() { QThread::msleep(400); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); QThread::msleep(400); - writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); - writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + if (proform_rower_sport_rl) { + writeCharacteristic(initData10_proform_rower_sport_rl, sizeof(initData10_proform_rower_sport_rl), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11_proform_rower_sport_rl, sizeof(initData11_proform_rower_sport_rl), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12_proform_rower_sport_rl, sizeof(initData12_proform_rower_sport_rl), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData7_proform_rower_sport_rl, sizeof(noOpData7_proform_rower_sport_rl), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData8_proform_rower_sport_rl, sizeof(noOpData8_proform_rower_sport_rl), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(400); + } else { + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(400); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(400); + } /*writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("init"), false, false); QThread::msleep(400); writeCharacteristic(noOpData8, sizeof(noOpData8), QStringLiteral("init"), false, false); @@ -522,26 +714,33 @@ void proformrower::stateChanged(QLowEnergyService::ServiceState state) { // ******************************************* virtual treadmill init ************************************* QSettings settings; - bool virtual_device_rower = settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); - if (!firstStateChanged && !virtualTreadmill && !virtualBike && !virtualRower) { - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + bool virtual_device_rower = + settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool(); + if (!firstStateChanged && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (virtual_device_rower) { qDebug() << QStringLiteral("creating virtual rower interface..."); - virtualRower = new virtualrower(this, noWriteResistance, noHeartService); + auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService); // connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug); + this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } else if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &proformrower::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &proformrower::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &proformrower::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -636,10 +835,6 @@ bool proformrower::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *proformrower::VirtualTreadmill() { return virtualTreadmill; } - -void *proformrower::VirtualDevice() { return VirtualTreadmill(); } - void proformrower::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/proformrower.h b/src/proformrower.h index f666e143d..9f9b93246 100644 --- a/src/proformrower.h +++ b/src/proformrower.h @@ -27,9 +27,7 @@ #include #include "rower.h" -#include "virtualbike.h" -#include "virtualrower.h" -#include "virtualtreadmill.h" + #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,10 +37,7 @@ class proformrower : public rower { Q_OBJECT public: proformrower(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: const resistance_t max_resistance = 24; @@ -57,11 +52,8 @@ class proformrower : public rower { void forceResistance(resistance_t requestResistance); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; - virtualrower *virtualRower = nullptr; uint8_t counterPoll = 0; - uint16_t watts(); + uint16_t watts() override; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/proformtreadmill.cpp b/src/proformtreadmill.cpp index 38c950d53..0c8626fdb 100644 --- a/src/proformtreadmill.cpp +++ b/src/proformtreadmill.cpp @@ -1,6 +1,8 @@ #include "proformtreadmill.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -36,12 +38,15 @@ void proformtreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, cons timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -57,6 +62,8 @@ void proformtreadmill::forceIncline(double incline) { .toBool(); bool nordictrack_s30_treadmill = settings.value(QZSettings::nordictrack_s30_treadmill, QZSettings::default_nordictrack_s30_treadmill).toBool(); + bool proform_treadmill_8_0 = + settings.value(QZSettings::proform_treadmill_8_0, QZSettings::default_proform_treadmill_8_0).toBool(); bool proform_treadmill_9_0 = settings.value(QZSettings::proform_treadmill_9_0, QZSettings::default_proform_treadmill_9_0).toBool(); bool proform_treadmill_1800i = @@ -65,6 +72,8 @@ void proformtreadmill::forceIncline(double incline) { settings.value(QZSettings::proform_treadmill_se, QZSettings::default_proform_treadmill_se).toBool(); bool norditrack_s25i_treadmill = settings.value(QZSettings::norditrack_s25i_treadmill, QZSettings::default_norditrack_s25i_treadmill).toBool(); + bool proform_treadmill_z1300i = + settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); if (proform_treadmill_1800i) { uint8_t i = abs(incline * 10); @@ -85,7 +94,7 @@ void proformtreadmill::forceIncline(double incline) { if (norditrack_s25i_treadmill) { write[14] = write[11] + write[12] + 0x11; - } else if (proform_treadmill_9_0 || proform_treadmill_se) { + } else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_z1300i) { write[14] = write[11] + write[12] + 0x12; } else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_t65s_83_treadmill) { for (uint8_t i = 0; i < 7; i++) { @@ -109,6 +118,8 @@ void proformtreadmill::forceSpeed(double speed) { settings.value(QZSettings::nordictrack_t65s_treadmill, QZSettings::default_nordictrack_t65s_treadmill).toBool(); bool nordictrack_s30_treadmill = settings.value(QZSettings::nordictrack_s30_treadmill, QZSettings::default_nordictrack_s30_treadmill).toBool(); + bool proform_treadmill_8_0 = + settings.value(QZSettings::proform_treadmill_8_0, QZSettings::default_proform_treadmill_8_0).toBool(); bool proform_treadmill_9_0 = settings.value(QZSettings::proform_treadmill_9_0, QZSettings::default_proform_treadmill_9_0).toBool(); bool proform_treadmill_se = @@ -118,6 +129,8 @@ void proformtreadmill::forceSpeed(double speed) { .toBool(); bool norditrack_s25i_treadmill = settings.value(QZSettings::norditrack_s25i_treadmill, QZSettings::default_norditrack_s25i_treadmill).toBool(); + bool proform_treadmill_z1300i = + settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02}; uint8_t write[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x04, 0x09, 0x02, 0x01, @@ -128,7 +141,8 @@ void proformtreadmill::forceSpeed(double speed) { if (norditrack_s25i_treadmill) { write[14] = write[11] + write[12] + 0x11; - } else if (proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_cadence_lt) { + } else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_treadmill_cadence_lt || + proform_treadmill_z1300i) { write[14] = write[11] + write[12] + 0x11; } else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_t65s_83_treadmill) { for (uint8_t i = 0; i < 7; i++) { @@ -173,6 +187,8 @@ void proformtreadmill::update() { .toBool(); bool proform_treadmill_1800i = settings.value(QZSettings::proform_treadmill_1800i, QZSettings::default_proform_treadmill_1800i).toBool(); + bool proform_treadmill_8_0 = + settings.value(QZSettings::proform_treadmill_8_0, QZSettings::default_proform_treadmill_8_0).toBool(); bool proform_treadmill_9_0 = settings.value(QZSettings::proform_treadmill_9_0, QZSettings::default_proform_treadmill_9_0).toBool(); bool proform_treadmill_se = @@ -187,6 +203,10 @@ void proformtreadmill::update() { settings .value(QZSettings::nordictrack_incline_trainer_x7i, QZSettings::default_nordictrack_incline_trainer_x7i) .toBool(); + bool proform_treadmill_z1300i = + settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); + bool proform_pro_1000_treadmill = + settings.value(QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill).toBool(); // bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i, // QZSettings::default_proform_treadmill_995i).toBool(); @@ -229,7 +249,7 @@ void proformtreadmill::update() { counterPoll = 0; } } else*/ - if (proform_treadmill_9_0) { + if (proform_treadmill_9_0 || proform_treadmill_z1300i) { uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, 0x0d, 0x92, 0x1a, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -254,14 +274,15 @@ void proformtreadmill::update() { if (requestInclination < 0) requestInclination = 0; if (requestInclination != currentInclination().value() && requestInclination >= -3 && - requestInclination <= 15) { + requestInclination <= (proform_treadmill_z1300i ? 12 : 15)) { emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); forceIncline(requestInclination); } requestInclination = -100; } if (requestSpeed != -1) { - if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && + requestSpeed <= (proform_treadmill_z1300i ? 19.3 : 22)) { emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); forceSpeed(requestSpeed); } @@ -920,6 +941,166 @@ void proformtreadmill::update() { if (counterPoll > 5) { counterPoll = 0; } + } else if (proform_treadmill_8_0) { + uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00, + 0x0f, 0x92, 0x1a, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x81, 0x00, 0x10, 0xb8, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x14, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x10, 0x04, 0x10, 0x02, 0x00, + 0x0a, 0x1b, 0x94, 0x30, 0x00, 0x00, 0x40, 0x50, 0x00, 0x80}; + uint8_t noOpData6[] = {0xff, 0x02, 0x18, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + switch (counterPoll) { + case 0: + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); + break; + case 1: + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp")); + break; + case 2: + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp")); + break; + case 3: + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"), false, true); + if (requestInclination != -100) { + if (requestInclination < 0) + requestInclination = 0; + if (requestInclination != currentInclination().value() && requestInclination >= 0 && + requestInclination <= 15) { + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + forceIncline(requestInclination); + } + requestInclination = -100; + } + if (requestSpeed != -1) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); + forceSpeed(requestSpeed); + } + requestSpeed = -1; + } + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit tapeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + break; + case 4: + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp")); + break; + case 5: + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp"), false, true); + if (requestInclination != -100) { + if (requestInclination < 0) + requestInclination = 0; + if (requestInclination != currentInclination().value() && requestInclination >= 0 && + requestInclination <= 15) { + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + forceIncline(requestInclination); + } + requestInclination = -100; + } + if (requestSpeed != -1) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); + forceSpeed(requestSpeed); + } + requestSpeed = -1; + } + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit tapeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + break; + } + counterPoll++; + if (counterPoll > 5) { + counterPoll = 0; + } + } else if (proform_pro_1000_treadmill) { + uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00, 0x0f, 0x80, 0x0a, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x85, 0x00, 0x10, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x14, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x10, 0x04, 0x10, 0x02, 0x00, + 0x0a, 0x1b, 0x94, 0x30, 0x00, 0x00, 0x40, 0x50, 0x00, 0x80}; + uint8_t noOpData6[] = {0xff, 0x02, 0x18, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + switch (counterPoll) { + case 0: + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp")); + break; + case 1: + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp")); + break; + case 2: + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp")); + break; + case 3: + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"), false, true); + break; + case 4: + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp")); + break; + case 5: + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("noOp"), false, true); + if (requestInclination != -100) { + if (requestInclination < 0) + requestInclination = 0; + if (requestInclination != currentInclination().value() && requestInclination >= 0 && + requestInclination <= 15) { + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + forceIncline(requestInclination); + } + requestInclination = -100; + } + if (requestSpeed != -1) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); + forceSpeed(requestSpeed); + } + requestSpeed = -1; + } + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit tapeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + break; + } + counterPoll++; + if (counterPoll > 5) { + counterPoll = 0; + } } else { uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x00, @@ -1003,6 +1184,8 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha bool nordictrack_t65s_83_treadmill = settings.value(QZSettings::nordictrack_t65s_83_treadmill, QZSettings::default_nordictrack_t65s_83_treadmill) .toBool(); + bool proform_pro_1000_treadmill = + settings.value(QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill).toBool(); bool nordictrack10 = settings.value(QZSettings::nordictrack_10_treadmill, QZSettings::default_nordictrack_10_treadmill).toBool(); bool nordictrackt70 = @@ -1015,6 +1198,8 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha settings.value(QZSettings::proform_treadmill_1800i, QZSettings::default_proform_treadmill_1800i).toBool(); bool proform_treadmill_se = settings.value(QZSettings::proform_treadmill_se, QZSettings::default_proform_treadmill_se).toBool(); + bool proform_treadmill_8_0 = + settings.value(QZSettings::proform_treadmill_8_0, QZSettings::default_proform_treadmill_8_0).toBool(); bool proform_treadmill_9_0 = settings.value(QZSettings::proform_treadmill_9_0, QZSettings::default_proform_treadmill_9_0).toBool(); bool proform_cadence_lt = @@ -1025,6 +1210,8 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha bool nordictrack_incline_trainer_x7i = settings.value(QZSettings::nordictrack_incline_trainer_x7i, QZSettings::default_nordictrack_incline_trainer_x7i) .toBool(); + bool proform_treadmill_z1300i = + settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); @@ -1034,11 +1221,11 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha if (newValue.length() != 20 || newValue.at(0) != 0x00 || newValue.at(1) != 0x12 || newValue.at(2) != 0x01 || newValue.at(3) != 0x04 || - ((nordictrack10 || nordictrackt70 || proform_treadmill_1800i || proform_treadmill_9_0 || - nordictrack_incline_trainer_x7i) && + ((nordictrack10 || nordictrackt70 || proform_treadmill_1800i || proform_treadmill_z1300i || + proform_treadmill_8_0 || proform_treadmill_9_0 || nordictrack_incline_trainer_x7i) && (newValue.at(4) != 0x02 || (newValue.at(5) != 0x31 && newValue.at(5) != 0x34))) || ((norditrack_s25i_treadmill) && (newValue.at(4) != 0x02 || (newValue.at(5) != 0x2f))) || - ((nordictrack_t65s_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill || + ((nordictrack_t65s_treadmill || proform_pro_1000_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill || proform_treadmill_se || proform_cadence_lt) && (newValue.at(4) != 0x02 || newValue.at(5) != 0x2e)) || (((uint8_t)newValue.at(12)) == 0xFF && ((uint8_t)newValue.at(13)) == 0xFF && @@ -1079,16 +1266,7 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -1107,7 +1285,13 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha } void proformtreadmill::btinit() { - QSettings settings; +#ifdef Q_OS_WIN + const int sleepms = 600; +#else + const int sleepms = 400; +#endif + + QSettings settings; bool nordictrack10 = settings.value(QZSettings::nordictrack_10_treadmill, QZSettings::default_nordictrack_10_treadmill).toBool(); bool nordictrackt70 = @@ -1120,6 +1304,8 @@ void proformtreadmill::btinit() { settings.value(QZSettings::proform_treadmill_1800i, QZSettings::default_proform_treadmill_1800i).toBool(); bool proform_treadmill_se = settings.value(QZSettings::proform_treadmill_se, QZSettings::default_proform_treadmill_se).toBool(); + bool proform_treadmill_8_0 = + settings.value(QZSettings::proform_treadmill_8_0, QZSettings::default_proform_treadmill_8_0).toBool(); bool proform_treadmill_9_0 = settings.value(QZSettings::proform_treadmill_9_0, QZSettings::default_proform_treadmill_9_0).toBool(); bool proform_cadence_lt = @@ -1133,6 +1319,10 @@ void proformtreadmill::btinit() { bool nordictrack_incline_trainer_x7i = settings.value(QZSettings::nordictrack_incline_trainer_x7i, QZSettings::default_nordictrack_incline_trainer_x7i) .toBool(); + bool proform_treadmill_z1300i = + settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); + bool proform_pro_1000_treadmill = + settings.value(QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill).toBool(); // bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i, // QZSettings::default_proform_treadmill_995i).toBool(); @@ -1169,49 +1359,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else*/ if (nordictrack10) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; @@ -1247,49 +1437,196 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); + } else if (proform_pro_1000_treadmill) { + uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; + uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData5[] = {0xfe, 0x02, 0x0a, 0x02}; + uint8_t initData6[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x82, 0x00, + 0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData7[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, + 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04}; + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x07, 0x01, 0x13, 0xc0, 0x7f, 0x34, 0xeb, 0xa0, 0x6f, 0x2c, 0xe3}; + uint8_t initData11[] = {0x01, 0x12, 0xa0, 0x7f, 0x34, 0x0b, 0xc0, 0x8f, 0x7c, 0x53, 0x00, 0xff, 0xd4, 0x8b, 0x60, 0x4f, 0x2c, 0x03, 0xe0, 0xff}; + uint8_t initData12[] = {0xff, 0x08, 0xd4, 0xab, 0x80, 0x90, 0x02, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + } else if (proform_treadmill_z1300i) { + uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; + uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData5[] = {0xfe, 0x02, 0x0a, 0x02}; + uint8_t initData6[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x82, 0x00, + 0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData7[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, + 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04}; + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x04, + 0x00, 0xb0, 0x4c, 0xda, 0x7e, 0x14, 0xb8, 0x46, 0xfa, 0x98}; + uint8_t initData11[] = {0x01, 0x12, 0x34, 0xd2, 0x86, 0x3c, 0xf0, 0x9e, 0x52, 0xe0, + 0xbc, 0x4a, 0x0e, 0xc4, 0x88, 0x56, 0x0a, 0xc8, 0x84, 0x42}; + uint8_t initData12[] = {0xff, 0x08, 0x36, 0xec, 0xe0, 0x80, 0x02, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, + 0x0d, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0}; + uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); } else if (nordictrack_incline_trainer_x7i) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1324,49 +1661,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (norditrack_s25i_treadmill) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1401,49 +1738,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (proform_cadence_lt) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1478,49 +1815,126 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + } else if (proform_treadmill_8_0) { + uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; + uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x80, 0x88, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x04, 0x04, 0x88, 0x90, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData5[] = {0xfe, 0x02, 0x0a, 0x02}; + uint8_t initData6[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x82, 0x00, + 0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData7[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, + 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData8[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t initData9[] = {0xfe, 0x02, 0x2c, 0x04}; + uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x04, 0x28, 0x90, 0x07, + 0x01, 0xe2, 0x34, 0x84, 0xd2, 0x2e, 0x88, 0xd0, 0x3e, 0x9a}; + uint8_t initData11[] = {0x01, 0x12, 0xfc, 0x5c, 0xba, 0x16, 0x90, 0xf8, 0x46, 0xd2, + 0x24, 0xb4, 0x02, 0x9e, 0x18, 0x60, 0xee, 0x6a, 0xec, 0x6c}; + uint8_t initData12[] = {0xff, 0x08, 0xea, 0x66, 0x20, 0x98, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x0e, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3a, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x17, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x0c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (nordictrackt70) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1555,49 +1969,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (proform_treadmill_9_0) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1644,73 +2058,73 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData8, sizeof(noOpData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData9, sizeof(noOpData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData10, sizeof(noOpData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData11, sizeof(noOpData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData12, sizeof(noOpData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData9, sizeof(noOpData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData10, sizeof(noOpData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (nordictrack_t65s_treadmill) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1745,49 +2159,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (nordictrack_t65s_83_treadmill) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1822,49 +2236,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (proform_treadmill_1800i) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1899,49 +2313,49 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (proform_treadmill_se) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -1969,39 +2383,39 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else if (nordictrack_s30_treadmill) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -2036,51 +2450,51 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } else { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; uint8_t initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, @@ -2105,37 +2519,37 @@ void proformtreadmill::btinit() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false); - QThread::msleep(400); + QThread::msleep(sleepms); } initDone = true; @@ -2169,7 +2583,7 @@ void proformtreadmill::stateChanged(QLowEnergyService::ServiceState state) { // ******************************************* virtual treadmill init ************************************* QSettings settings; - if (!firstStateChanged && !virtualTreadmill && !virtualBike) { + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -2178,15 +2592,17 @@ void proformtreadmill::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &proformtreadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &proformtreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &proformtreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -2282,10 +2698,6 @@ bool proformtreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *proformtreadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *proformtreadmill::VirtualDevice() { return VirtualTreadmill(); } - void proformtreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/proformtreadmill.h b/src/proformtreadmill.h index 0a59b9b7f..cdd9ccc57 100644 --- a/src/proformtreadmill.h +++ b/src/proformtreadmill.h @@ -27,8 +27,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -38,10 +36,7 @@ class proformtreadmill : public treadmill { Q_OBJECT public: proformtreadmill(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: double GetDistanceFromPacket(QByteArray packet); @@ -55,8 +50,6 @@ class proformtreadmill : public treadmill { void forceSpeed(double speed); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; diff --git a/src/proformwifibike.cpp b/src/proformwifibike.cpp index c3077c835..6bf4c3020 100644 --- a/src/proformwifibike.cpp +++ b/src/proformwifibike.cpp @@ -1,6 +1,7 @@ #include "proformwifibike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -43,7 +44,7 @@ proformwifibike::proformwifibike(bool noWriteResistance, bool noHeartService, ui initRequest = true; // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -68,10 +69,11 @@ proformwifibike::proformwifibike(bool noWriteResistance, bool noHeartService, ui #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,& proformwifibike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &proformwifibike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -103,7 +105,7 @@ void proformwifibike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QByteArray((const char *)data, data_len)); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } @@ -209,7 +211,7 @@ uint16_t proformwifibike::wattsFromResistance(resistance_t resistance) { // must be double because it's an inclination void proformwifibike::forceResistance(double requestResistance) { - if(tdf2) { + if (tdf2) { QString send = "{\"type\":\"set\",\"values\":{\"Master State\":\"4\"}}"; qDebug() << "forceResistance" << send; websocket.sendTextMessage(send); @@ -217,6 +219,9 @@ void proformwifibike::forceResistance(double requestResistance) { double inc = qRound(requestResistance / 0.5) * 0.5; QString send = "{\"type\":\"set\",\"values\":{\"Incline\":\"" + QString::number(inc) + "\"}}"; + if (!inclinationAvailableByHardware()) + send = "{\"type\":\"set\",\"values\":{\"Resistance\":\"" + QString::number(requestResistance) + "\"}}"; + qDebug() << "forceResistance" << send; websocket.sendTextMessage(send); } @@ -251,6 +256,7 @@ void proformwifibike::innerWriteResistance() { if (requestResistance != currentResistance().value()) { emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); + auto virtualBike = this->VirtualBike(); if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && (requestPower == 0 || requestPower == -1)) { forceResistance(requestResistance); @@ -260,7 +266,7 @@ void proformwifibike::innerWriteResistance() { } if (requestPower > 0 && erg_mode) { - if(last_mode.compare("WATTS_GOAL")) { + if (last_mode.compare("WATTS_GOAL")) { last_mode = "WATTS_GOAL"; setWorkoutType(last_mode); } @@ -275,17 +281,17 @@ void proformwifibike::innerWriteResistance() { } if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() < 0) { if (settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble() != 0.0) { - qDebug() - << QStringLiteral("request watt value was ") << r << QStringLiteral("but it will be transformed to") - << r - settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); + qDebug() << QStringLiteral("request watt value was ") << r + << QStringLiteral("but it will be transformed to") + << r - settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); } r -= settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble(); } setTargetWatts(r); } - if (requestInclination != -100 && !erg_mode) { - if(last_mode.compare("MANUAL")) { + if (requestInclination != -100 && !erg_mode && inclinationAvailableByHardware()) { + if (last_mode.compare("MANUAL")) { last_mode = "MANUAL"; setWorkoutType(last_mode); } @@ -330,16 +336,7 @@ void proformwifibike::update() { } } -bool proformwifibike::inclinationAvailableByHardware() { - QSettings settings; - bool proform_studio = settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool(); - bool proform_tdf_10 = settings.value(QZSettings::proform_tdf_10, QZSettings::default_proform_tdf_10).toBool(); - - if (proform_studio || proform_tdf_10) - return true; - else - return false; -} +bool proformwifibike::inclinationAvailableByHardware() { return max_incline_supported > 0; } resistance_t proformwifibike::pelotonToBikeResistance(int pelotonResistance) { if (pelotonResistance <= 10) { @@ -461,14 +458,20 @@ void proformwifibike::characteristicChanged(const QString &newValue) { } } - if (!values[QStringLiteral("Current Watts")].isUndefined()) { - double watt = values[QStringLiteral("Current Watts")].toString().toDouble(); - m_watt = watt; - emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); - } else if (!values[QStringLiteral("Watt attuali")].isUndefined()) { - double watt = values[QStringLiteral("Watt attuali")].toString().toDouble(); - m_watt = watt; - emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); + // some buggy TDF1 bikes send spurious wattage at the end with cadence = 0 + if (Cadence.value() > 0) { + if (!values[QStringLiteral("Current Watts")].isUndefined()) { + double watt = values[QStringLiteral("Current Watts")].toString().toDouble(); + if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) + m_watt = watt; + emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); + } else if (!values[QStringLiteral("Watt attuali")].isUndefined()) { + double watt = values[QStringLiteral("Watt attuali")].toString().toDouble(); + m_watt = watt; + emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); + } } if (!values[QStringLiteral("Actual Incline")].isUndefined()) { @@ -483,6 +486,45 @@ void proformwifibike::characteristicChanged(const QString &newValue) { emit debug(QStringLiteral("Target Watts: ") + QString::number(watts())); } + if (!values[QStringLiteral("Resistance")].isUndefined()) { + Resistance = values[QStringLiteral("Resistance")].toString().toDouble(); + emit debug(QStringLiteral("Resistance: ") + QString::number(Resistance.value())); + } + + if (!values[QStringLiteral("Maximum Incline")].isUndefined()) { + max_incline_supported = values[QStringLiteral("Maximum Incline")].toString().toDouble(); + emit debug(QStringLiteral("Maximum Incline Supported: ") + QString::number(max_incline_supported)); + } + + if (settings.value(QZSettings::gears_from_bike, QZSettings::default_gears_from_bike).toBool()) { + if (!values[QStringLiteral("key")].isUndefined()) { + QJsonObject key = values[QStringLiteral("key")].toObject(); + QJsonValue code = key.value("code"); + QJsonValue name = key.value("name"); + QJsonValue held = key.value("held"); + if(held.toString().contains(QStringLiteral("-1"))) { + double value = 0; + if (name.toString().contains(QStringLiteral("LEFT EXTERNAL GEAR DOWN"))) { + qDebug() << "LEFT EXTERNAL GEAR DOWN"; + value = -0.5; + } else if (name.toString().contains(QStringLiteral("LEFT EXTERNAL GEAR UP"))) { + qDebug() << "LEFT EXTERNAL GEAR UP"; + value = 0.5; + } else if (name.toString().contains(QStringLiteral("RIGHT EXTERNAL GEAR UP"))) { + qDebug() << "RIGHT EXTERNAL GEAR UP"; + value = 5.0; + } else if (name.toString().contains(QStringLiteral("RIGHT EXTERNAL GEAR DOWN"))) { + qDebug() << "RIGHT EXTERNAL GEAR DOWN"; + value = -5.0; + } + if (value != 0.0) { + forceResistance(currentInclination().value() + value); // to force an immediate change + setGears(gears() + value); + } + } + } + } + if (watts()) KCal += ((((0.048 * ((double)watts()) + 1.19) * @@ -502,8 +544,6 @@ void proformwifibike::characteristicChanged(const QString &newValue) { emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); } - lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); - #ifdef Q_OS_ANDROID if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) Heart = (uint8_t)KeepAwakeHelper::heart(); @@ -511,19 +551,12 @@ void proformwifibike::characteristicChanged(const QString &newValue) { #endif { if (disable_hr_frommachinery && heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); @@ -551,10 +584,4 @@ void proformwifibike::deviceDiscovered(const QBluetoothDeviceInfo &device) { bool proformwifibike::connected() { return websocket.state() == QAbstractSocket::ConnectedState; } -void *proformwifibike::VirtualBike() { return virtualBike; } - -void *proformwifibike::VirtualDevice() { return VirtualBike(); } - -uint16_t proformwifibike::watts() { - return m_watt.value(); -} +uint16_t proformwifibike::watts() { return m_watt.value(); } diff --git a/src/proformwifibike.h b/src/proformwifibike.h index 33b4ec952..f64559362 100644 --- a/src/proformwifibike.h +++ b/src/proformwifibike.h @@ -34,7 +34,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -45,19 +44,17 @@ class proformwifibike : public bike { public: proformwifibike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool inclinationAvailableByHardware(); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool inclinationAvailableByHardware() override; + bool connected() override; private: QWebSocket websocket; resistance_t max_resistance = 100; resistance_t min_resistance = -20; + double max_incline_supported = 20; void connectToDevice(); uint16_t wattsFromResistance(resistance_t resistance); double GetDistanceFromPacket(QByteArray packet); @@ -67,14 +64,13 @@ class proformwifibike : public bike { bool wait_for_response = false); void startDiscover(); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(double requestResistance); void innerWriteResistance(); void setTargetWatts(double watts); void setWorkoutType(QString type); QTimer *refresh; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; uint8_t bikeResistanceOffset = 4; double bikeResistanceGain = 1.0; diff --git a/src/proformwifitreadmill.cpp b/src/proformwifitreadmill.cpp index 8f137976b..39e27aad0 100644 --- a/src/proformwifitreadmill.cpp +++ b/src/proformwifitreadmill.cpp @@ -1,7 +1,7 @@ #include "proformwifitreadmill.h" -#include "ios/lockscreen.h" #include "keepawakehelper.h" #include "virtualbike.h" +#include "virtualtreadmill.h" #include #include #include @@ -41,7 +41,7 @@ proformwifitreadmill::proformwifitreadmill(bool noWriteResistance, bool noHeartS initRequest = true; // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualTreadMill && !virtualBike) { + if (!firstStateChanged && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -50,15 +50,17 @@ proformwifitreadmill::proformwifitreadmill(bool noWriteResistance, bool noHeartS if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &proformwifitreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &proformwifitreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &proformwifitreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstStateChanged = 1; } @@ -217,6 +219,12 @@ void proformwifitreadmill::characteristicChanged(const QString &newValue) { emit debug(QStringLiteral("Current Inclination: ") + QString::number(incline)); } + if (!values[QStringLiteral("Incline")].isUndefined()) { + double incline = values[QStringLiteral("Incline")].toString().toDouble(); + Inclination = incline; + emit debug(QStringLiteral("Current Inclination: ") + QString::number(incline)); + } + if (watts()) KCal += ((((0.048 * ((double)watts()) + 1.19) * @@ -245,16 +253,7 @@ void proformwifitreadmill::characteristicChanged(const QString &newValue) { #endif { if (disable_hr_frommachinery && heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -285,10 +284,4 @@ void proformwifitreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) bool proformwifitreadmill::connected() { return websocket.state() == QAbstractSocket::ConnectedState; } -void *proformwifitreadmill::VirtualBike() { return virtualBike; } - -void *proformwifitreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *proformwifitreadmill::VirtualDevice() { return VirtualTreadMill(); } - uint16_t proformwifitreadmill::watts() { return m_watts; } diff --git a/src/proformwifitreadmill.h b/src/proformwifitreadmill.h index b3c113006..3072dbfb3 100644 --- a/src/proformwifitreadmill.h +++ b/src/proformwifitreadmill.h @@ -34,8 +34,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -46,12 +44,8 @@ class proformwifitreadmill : public treadmill { public: proformwifitreadmill(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualTreadMill(); - void *VirtualBike(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + bool connected() override; + virtual bool canStartStop() override { return false; } private: QWebSocket websocket; @@ -68,8 +62,6 @@ class proformwifitreadmill : public treadmill { uint16_t watts(); QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t counterPoll = 0; uint8_t bikeResistanceOffset = 4; double bikeResistanceGain = 1.0; diff --git a/src/purchasing/inapp/inappstore.cpp b/src/purchasing/inapp/inappstore.cpp index 3ec84ea46..35c1b826e 100644 --- a/src/purchasing/inapp/inappstore.cpp +++ b/src/purchasing/inapp/inappstore.cpp @@ -62,7 +62,7 @@ class IAPRegisterMetaTypes { public: -#ifndef Q_OS_WINRT +#ifndef WIN32 IAPRegisterMetaTypes() { qRegisterMetaType("InAppProduct::ProductType"); } #endif } _registerIAPMetaTypes; diff --git a/src/qdomyos-zwift-lib.pro b/src/qdomyos-zwift-lib.pro index 67f4f56fd..8dbfdeb8c 100644 --- a/src/qdomyos-zwift-lib.pro +++ b/src/qdomyos-zwift-lib.pro @@ -11,3 +11,5 @@ unix { } !isEmpty(target.path): INSTALLS += target + + diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index 6a9795999..fc1b39aef 100644 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -18,11 +18,18 @@ qtHaveModule(httpserver) { QT+= webview DEFINES += CHARTJS } +# win32: { +# DEFINES += CHARTJS +# } } CONFIG += c++17 console app_bundle optimize_full ltcg CONFIG += qmltypes + +#win32: CONFIG += webengine +#unix:!android: CONFIG += webengine + QML_IMPORT_NAME = org.cagnulein.qdomyoszwift QML_IMPORT_MAJOR_VERSION = 1 # Additional import path used to resolve QML modules in Qt Creator's code model @@ -32,6 +39,8 @@ QML_IMPORT_PATH = QML_DESIGNER_IMPORT_PATH = win32:QMAKE_LFLAGS_DEBUG += -static-libstdc++ -static-libgcc +win32:QMAKE_LFLAGS_RELEASE += -static-libstdc++ -static-libgcc + QMAKE_LFLAGS_RELEASE += -s QMAKE_CXXFLAGS += -fno-sized-deallocation unix:android: { @@ -51,7 +60,7 @@ INCLUDEPATH += qmdnsengine/src/include # any Qt feature that has been marked deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. -DEFINES += QT_DEPRECATED_WARNINGS IO_UNDER_QT SMTP_BUILD +DEFINES += QT_DEPRECATED_WARNINGS IO_UNDER_QT SMTP_BUILD NOMINMAX # You can also make your code fail to compile if it uses deprecated APIs. @@ -63,11 +72,23 @@ DEFINES += QT_DEPRECATED_WARNINGS IO_UNDER_QT SMTP_BUILD # include(../qtzeroconf/qtzeroconf.pri) SOURCES += \ + $$PWD/bkoolbike.cpp \ + $$PWD/csafe.cpp \ + $$PWD/csaferower.cpp \ + $$PWD/fakerower.cpp \ + $$PWD/virtualdevice.cpp \ $$PWD/androidactivityresultreceiver.cpp \ $$PWD/androidadblog.cpp \ $$PWD/apexbike.cpp \ + $$PWD/handleurl.cpp \ + $$PWD/iconceptelliptical.cpp \ + $$PWD/localipaddress.cpp \ $$PWD/pelotonbike.cpp \ + $$PWD/schwinn170bike.cpp \ $$PWD/wahookickrheadwind.cpp \ + $$PWD/windows_zwift_workout_paddleocr_thread.cpp \ + $$PWD/ypooelliptical.cpp \ + $$PWD/ziprotreadmill.cpp \ Computrainer.cpp \ PathController.cpp \ characteristicnotifier2a53.cpp \ @@ -246,6 +267,7 @@ SOURCES += \ m3ibike.cpp \ domyosbike.cpp \ scanrecordresult.cpp \ + windows_zwift_incline_paddleocr_thread.cpp \ zwiftworkout.cpp macx: SOURCES += macos/lockscreen.mm !ios: SOURCES += mainwindow.cpp charts.cpp @@ -258,12 +280,24 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin INCLUDEPATH += fit-sdk/ HEADERS += \ + $$PWD/bkoolbike.h \ + $$PWD/csafe.h \ + $$PWD/csaferower.h \ + $$PWD/windows_zwift_workout_paddleocr_thread.h \ + $$PWD/fakerower.h \ + virtualdevice.h \ $$PWD/androidactivityresultreceiver.h \ $$PWD/androidadblog.h \ $$PWD/apexbike.h \ $$PWD/discoveryoptions.h \ + $$PWD/handleurl.h \ + $$PWD/iconceptelliptical.h \ + $$PWD/localipaddress.h \ $$PWD/pelotonbike.h \ + $$PWD/schwinn170bike.h \ $$PWD/wahookickrheadwind.h \ + $$PWD/ypooelliptical.h \ + $$PWD/ziprotreadmill.h \ Computrainer.h \ PathController.h \ characteristicnotifier2a53.h \ @@ -659,8 +693,10 @@ HEADERS += \ wobjectimpl.h \ yesoulbike.h \ scanrecordresult.h \ + windows_zwift_incline_paddleocr_thread.h \ zwiftworkout.h + exists(secret.h): HEADERS += secret.h !ios: HEADERS += charts.h @@ -674,9 +710,18 @@ RESOURCES += \ qml.qrc DISTFILES += \ + $$PWD/android/libs/android_antlib_4-16-0.aar \ + $$PWD/android/libs/connectiq-mobile-sdk-android-1.5.aar \ + $$PWD/android/res/xml/device_filter.xml \ + $$PWD/android/src/CSafeRowerUSBHID.java \ + $$PWD/android/src/Garmin.java \ + $$PWD/android/src/HidBridge.java \ + $$PWD/android/src/IQMessageReceiverWrapper.java \ $$PWD/android/src/MediaProjection.java \ $$PWD/android/src/NotificationUtils.java \ $$PWD/android/src/ScreenCaptureService.java \ + $$PWD/android/src/WearableController.java \ + $$PWD/android/src/WearableMessageListenerService.java \ .clang-format \ AppxManifest.xml \ android/AndroidManifest.xml \ @@ -685,7 +730,6 @@ DISTFILES += \ android/gradle/wrapper/gradle-wrapper.properties \ android/gradlew \ android/gradlew.bat \ - android/libs/android_antlib_4-14-0.jar \ android/res/layout/floating_layout.xml \ android/res/values/libs.xml \ android/src/Ant.java \ @@ -701,6 +745,7 @@ DISTFILES += \ android/src/MyActivity.java \ android/src/PowerChannelController.java \ android/src/SpeedChannelController.java \ + android/src/SDMChannelController.java \ android/src/Usbserial.java \ android/src/com/cgutman/adblib/AdbBase64.java \ android/src/com/cgutman/adblib/AdbConnection.java \ @@ -740,6 +785,7 @@ ios { ios { OBJECTIVE_SOURCES += ios/lockscreen.mm \ + ios/ios_app_delegate.mm \ fit-sdk/FitDecode.mm \ fit-sdk/FitDeveloperField.mm \ fit-sdk/FitEncode.mm \ @@ -756,6 +802,7 @@ ios { QMAKE_INFO_PLIST = ios/Info.plist QMAKE_ASSET_CATALOGS = $$PWD/ios/Images.xcassets QMAKE_ASSET_CATALOGS_APP_ICON = "AppIcon" + QMAKE_ASSET_CATALOGS_BUILD_PATH = $$PWD/ios/ TARGET = qdomyoszwift QMAKE_TARGET_BUNDLE_PREFIX = org.cagnulein @@ -768,4 +815,4 @@ INCLUDEPATH += purchasing/inapp WINRT_MANIFEST = AppxManifest.xml -VERSION = 2.13.4 +VERSION = 2.16.22 diff --git a/src/qdomyos-zwift.pro.user b/src/qdomyos-zwift.pro.user new file mode 100644 index 000000000..a156e9b8c --- /dev/null +++ b/src/qdomyos-zwift.pro.user @@ -0,0 +1,1349 @@ + + + + + + EnvironmentId + {49de5078-9d78-40d6-831d-94a49a41b09c} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + *.md, *.MD, Makefile + false + true + + + + ProjectExplorer.Project.PluginSettings + + + true + true + true + true + true + + + 0 + true + + true + Builtin.Questionable + + true + Builtin.DefaultTidyAndClazy + 4 + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Android.Device.Type + Android Qt 5.15.0 (android) Clang Multi-Abi + Android Qt 5.15.0 (android) Clang Multi-Abi + {712d43d8-c043-4a4a-954b-8c1d1814d550} + 1 + 0 + 0 + + true + 0 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Debug + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + armeabi-v7a + arm64-v8a + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + 2 + + + true + 2 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Release + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + armeabi-v7a + arm64-v8a + x86 + x86_64 + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + /Volumes/Backup/qdomyos-zwift/android_release.keystore + true + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + 2 + + + true + 0 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Profile + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + armeabi-v7a + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + + true + 2 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Release_amazon + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Release_amazon + + + true + QtProjectManager.QMakeBuildStep + + false + + armeabi-v7a + arm64-v8a + x86 + x86_64 + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + /Volumes/Backup/qdomyos-zwift/android_release.keystore + true + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + AMAZON=1 + + Amazon + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + 2 + + + true + 2 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Release_License + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Android_Qt_5_15_0_android_Clang_Multi_Abi-Release_License + + + true + QtProjectManager.QMakeBuildStep + DEFINES+=LICENSE + false + + armeabi-v7a + arm64-v8a + x86 + x86_64 + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + + true + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + /Volumes/Backup/qdomyos-zwift/android_release.keystore + true + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + ApkLicense + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + 2 + + 5 + + + + true + Qt4ProjectManager.AndroidDeployQtStep + false + + 1 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + Qt4ProjectManager.AndroidDeployConfiguration2 + + 1 + + + armeabi-v7a + armeabi + + d052f34 + 23 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + + + + 0 + + qdomyos-zwift + Qt4ProjectManager.AndroidRunConfiguration:/Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + /Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + + false + + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.Target.1 + + Desktop + MacOS + MacOS + {908b5b6f-94bb-4e80-bc56-0dc1c85b27a4} + 1 + 0 + 0 + + true + 0 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-MacOS-Debug + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-MacOS-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 2 + 2 + + + true + 1 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-MacOS-Release + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-MacOS-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 2 + + + true + 0 + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-MacOS-Profile + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-MacOS-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + 2 + + Qt4ProjectManager.Qt4RunConfiguration:/Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + /Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + + false + + false + true + false + true + false + false + true + + + + 1 + + + + ProjectExplorer.Project.Target.2 + + Ios.Device.Type + iOS + iOS + {0d01c6fb-716f-42e8-ba52-8b0fbf1ad8fd} + 0 + 0 + 0 + + true + 0 + true + + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 2 + 2 + + + true + 2 + true + + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-iOS-Release + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-iOS-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 2 + + + true + 0 + true + + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-iOS-Profile + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-iOS-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + + true + Qt4ProjectManager.IosDeployStep + + 1 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + Qt4ProjectManager.IosDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + + + + 0 + + Run on iOS Device + Qt4ProjectManager.IosRunConfiguration:/Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + /Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + + false + + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.Target.3 + + Ios.Device.Type + Qt 5.15.2 for iOS + Qt 5.15.2 for iOS + {299d4f36-6e55-4615-8efb-0f5c4fc89dfd} + 0 + 0 + 0 + + true + 0 + true + + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 2 + 2 + + + true + 2 + true + + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Release + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Release + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 2 + + + true + 0 + true + + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Profile + /Users/cagnulein/qdomyos-zwift/build-qdomyos-zwift-Qt_5_15_2_for_iOS-Profile + + + true + QtProjectManager.QMakeBuildStep + + false + + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + + true + Qt4ProjectManager.IosDeployStep + + 1 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + Qt4ProjectManager.IosDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + + + + 0 + + Run on iOS Device + Qt4ProjectManager.IosRunConfiguration:/Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + /Users/cagnulein/qdomyos-zwift/src/qdomyos-zwift.pro + + false + + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 4 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/src/qfit.cpp b/src/qfit.cpp index 01ab99021..eeb81e678 100644 --- a/src/qfit.cpp +++ b/src/qfit.cpp @@ -18,7 +18,7 @@ using namespace std; qfit::qfit(QObject *parent) : QObject(parent) {} void qfit::save(const QString &filename, QList session, bluetoothdevice::BLUETOOTH_TYPE type, - uint32_t processFlag, FIT_SPORT overrideSport) { + uint32_t processFlag, FIT_SPORT overrideSport, QString workoutName, QString bluetooth_device_name) { QSettings settings; bool strava_virtual_activity = settings.value(QZSettings::strava_virtual_activity, QZSettings::default_strava_virtual_activity).toBool(); @@ -59,7 +59,10 @@ void qfit::save(const QString &filename, QList session, bluetoothde fit::FileIdMesg fileIdMesg; // Every FIT file requires a File ID message fileIdMesg.SetType(FIT_FILE_ACTIVITY); - fileIdMesg.SetManufacturer(FIT_MANUFACTURER_DEVELOPMENT); + if(bluetooth_device_name.toUpper().startsWith("DOMYOS")) + fileIdMesg.SetManufacturer(FIT_MANUFACTURER_DECATHLON); + else + fileIdMesg.SetManufacturer(FIT_MANUFACTURER_DEVELOPMENT); fileIdMesg.SetProduct(1); fileIdMesg.SetSerialNumber(12345); fileIdMesg.SetTimeCreated(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L); @@ -195,6 +198,33 @@ void qfit::save(const QString &filename, QList session, bluetoothde encode.Open(file); encode.Write(fileIdMesg); encode.Write(devIdMesg); + + if (workoutName.length() > 0) { + fit::TrainingFileMesg trainingFile; + trainingFile.SetTimestamp(sessionMesg.GetTimestamp()); + trainingFile.SetTimeCreated(sessionMesg.GetTimestamp()); + trainingFile.SetType(FIT_FILE_WORKOUT); + encode.Write(trainingFile); + + fit::WorkoutMesg workout; + workout.SetSport(sessionMesg.GetSport()); + workout.SetSubSport(sessionMesg.GetSubSport()); + workout.SetWktName(workoutName.toStdWString()); + workout.SetNumValidSteps(1); + encode.Write(workout); + + fit::WorkoutStepMesg workoutStep; + workoutStep.SetDurationTime(sessionMesg.GetTotalTimerTime()); + workoutStep.SetTargetValue(0); + workoutStep.SetCustomTargetValueHigh(0); + workoutStep.SetCustomTargetValueLow(0); + workoutStep.SetMessageIndex(0); + workoutStep.SetDurationType(FIT_WKT_STEP_DURATION_TIME); + workoutStep.SetTargetType(FIT_WKT_STEP_TARGET_SPEED); + workoutStep.SetIntensity(FIT_INTENSITY_INTERVAL); + encode.Write(workoutStep); + } + encode.Write(eventMesg); fit::DateTime date((time_t)session.first().time.toSecsSinceEpoch()); diff --git a/src/qfit.h b/src/qfit.h index 655c04e34..4b483a0b1 100644 --- a/src/qfit.h +++ b/src/qfit.h @@ -17,7 +17,7 @@ class qfit : public QObject { public: explicit qfit(QObject *parent = nullptr); static void save(const QString &filename, QList session, bluetoothdevice::BLUETOOTH_TYPE type, - uint32_t processFlag = QFIT_PROCESS_NONE, FIT_SPORT overrideSport = FIT_SPORT_INVALID); + uint32_t processFlag = QFIT_PROCESS_NONE, FIT_SPORT overrideSport = FIT_SPORT_INVALID, QString workoutName = "", QString bluetooth_device_name = ""); static void open(const QString &filename, QList* output); signals: diff --git a/src/qmdnsengine b/src/qmdnsengine index 71019847f..575d34344 160000 --- a/src/qmdnsengine +++ b/src/qmdnsengine @@ -1 +1 @@ -Subproject commit 71019847ffbb933186eb14b10a6840283bb85d30 +Subproject commit 575d343444780a88a25ddf9bed8bda0c23502385 diff --git a/src/qml.qrc b/src/qml.qrc index 640092b7a..1193f956b 100644 --- a/src/qml.qrc +++ b/src/qml.qrc @@ -93,5 +93,19 @@ inner_templates/floating/radikalmedium.otf settings-treadmill-inclination-override.qml WebStravaAuth.qml + Toast.qml + ToastManager.qml + inner_templates/chartjs/elliptical.png + inner_templates/chartjs/row.png + inner_templates/chartjs/run.png + inner_templates/chartjs/walk.png + inner_templates/chartjs/bike.png + inner_templates/chartjs/html2canvas.min.js + inner_templates/chartjs/qzlogo.png + inner_templates/chartjs/dochartlive.js + inner_templates/chartjs/chartlive.htm + ChartFooter.qml + ChartFooterInnerJS.qml + ChartFooterInnerNoJS.qml diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index 77d0060ba..196661c8a 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -579,13 +579,14 @@ const QString QZSettings::sportstech_sx600 = QStringLiteral("sportstech_sx600"); const QString QZSettings::sole_elliptical_inclination = QStringLiteral("sole_elliptical_inclination"); const QString QZSettings::proform_hybrid_trainer_xt = QStringLiteral("proform_hybrid_trainer_xt"); const QString QZSettings::gears_restore_value = QStringLiteral("gears_restore_value"); -const QString QZSettings::gears_current_value = QStringLiteral("gears_current_value"); +const QString QZSettings::gears_current_value = QStringLiteral("gears_current_value_f"); const QString QZSettings::tile_pace_last500m_enabled = QStringLiteral("tile_pace_last500m_enabled"); const QString QZSettings::tile_pace_last500m_order = QStringLiteral("tile_pace_last500m_order"); const QString QZSettings::treadmill_difficulty_gain_or_offset = QStringLiteral("treadmill_difficulty_gain_or_offset"); const QString QZSettings::pafers_treadmill_bh_iboxster_plus = QStringLiteral("pafers_treadmill_bh_iboxster_plus"); const QString QZSettings::proform_cycle_trainer_400 = QStringLiteral("proform_cycle_trainer_400"); const QString QZSettings::peloton_workout_ocr = QStringLiteral("peloton_workout_ocr"); +const QString QZSettings::peloton_companion_workout_ocr = QStringLiteral("peloton_companion_workout_ocr"); const QString QZSettings::peloton_bike_ocr = QStringLiteral("peloton_bike_ocr"); const QString QZSettings::fitshow_treadmill_miles = QStringLiteral("fitshow_treadmill_miles"); const QString QZSettings::proform_hybrid_trainer_PFEL03815 = QStringLiteral("proform_hybrid_trainer_PFEL03815"); @@ -631,8 +632,55 @@ const QString QZSettings::nordictrack_incline_trainer_x7i = QStringLiteral("nord const QString QZSettings::strava_auth_external_webbrowser = QStringLiteral("strava_auth_external_webbrowser"); const QString QZSettings::gears_from_bike = QStringLiteral("gears_from_bike"); const QString QZSettings::peloton_spinups_autoresistance = QStringLiteral("peloton_spinups_autoresistance"); +const QString QZSettings::eslinker_costaway = QStringLiteral("eslinker_costaway"); +const QString QZSettings::treadmill_inclination_ovveride_gain = QStringLiteral("treadmill_inclination_ovveride_gain"); +const QString QZSettings::treadmill_inclination_ovveride_offset = + QStringLiteral("treadmill_inclination_ovveride_offset"); +const QString QZSettings::bh_spada_2_watt = QStringLiteral("bh_spada_2_watt"); +const QString QZSettings::tacx_neo2_peloton = QStringLiteral("tacx_neo2_peloton"); +const QString QZSettings::sole_treadmill_inclination_fast = QStringLiteral("sole_treadmill_inclination_fast"); +const QString QZSettings::zwift_ocr = QStringLiteral("zwift_ocr"); +const QString QZSettings::fit_file_saved_on_quit = QStringLiteral("fit_file_saved_on_quit"); +const QString QZSettings::gem_module_inclination = QStringLiteral("gem_module_inclination"); +const QString QZSettings::treadmill_simulate_inclination_with_speed = + QStringLiteral("treadmill_simulate_inclination_with_speed"); +const QString QZSettings::garmin_companion = QStringLiteral("garmin_companion"); +const QString QZSettings::iconcept_elliptical = QStringLiteral("iconcept_elliptical"); +const QString QZSettings::gears_gain = QStringLiteral("gears_gain"); +const QString QZSettings::proform_treadmill_8_0 = QStringLiteral("proform_treadmill_8_0"); +const QString QZSettings::zero_zt2500_treadmill = QStringLiteral("zero_zt2500_treadmill"); +const QString QZSettings::kingsmith_encrypt_v5 = QStringLiteral("kingsmith_encrypt_v5"); +const QString QZSettings::peloton_rower_level = QStringLiteral("peloton_rower_level"); +const QString QZSettings::tile_target_pace_enabled = QStringLiteral("tile_target_pace_enabled"); +const QString QZSettings::tile_target_pace_order = QStringLiteral("tile_target_pace_order"); +const QString QZSettings::tts_act_target_pace = QStringLiteral("tts_act_target_pace"); +const QString QZSettings::csafe_rower = QStringLiteral("csafe_rower"); +const QString QZSettings::default_csafe_rower = QStringLiteral(""); +const QString QZSettings::ftms_rower = QStringLiteral("ftms_rower"); +const QString QZSettings::default_ftms_rower = QStringLiteral("Disabled"); +const QString QZSettings::zwift_workout_ocr = QStringLiteral("zwift_workout_ocr"); +const QString QZSettings::proform_bike_sb = QStringLiteral("proform_bike_sb"); +const QString QZSettings::fakedevice_rower = QStringLiteral("fakedevice_rower"); +const QString QZSettings::zwift_ocr_climb_portal = QStringLiteral("zwift_ocr_climb_portal"); +const QString QZSettings::poll_device_time = QStringLiteral("poll_device_time"); +const QString QZSettings::proform_bike_PFEVEX71316_1 = QStringLiteral("proform_bike_PFEVEX71316_1"); +const QString QZSettings::schwinn_bike_resistance_v3 = QStringLiteral("schwinn_bike_resistance_v3"); +const QString QZSettings::watt_ignore_builtin = QStringLiteral("watt_ignore_builtin"); +const QString QZSettings::proform_treadmill_z1300i = QStringLiteral("proform_treadmill_z1300i"); +const QString QZSettings::ftms_bike = QStringLiteral("ftms_bike"); +const QString QZSettings::default_ftms_bike = QStringLiteral("Disabled"); +const QString QZSettings::ftms_treadmill = QStringLiteral("ftms_treadmill"); +const QString QZSettings::default_ftms_treadmill = QStringLiteral("Disabled"); +const QString QZSettings::ant_speed_offset = QStringLiteral("ant_speed_offset"); +const QString QZSettings::ant_speed_gain = QStringLiteral("ant_speed_gain"); +const QString QZSettings::proform_rower_sport_rl = QStringLiteral("proform_rower_sport_rl"); +const QString QZSettings::strava_date_prefix = QStringLiteral("strava_date_prefix"); +const QString QZSettings::race_mode = QStringLiteral("race_mode"); +const QString QZSettings::proform_pro_1000_treadmill = QStringLiteral("proform_pro_1000_treadmill"); +const QString QZSettings::saris_trainer = QStringLiteral("saris_trainer"); + +const uint32_t allSettingsCount = 568; -const uint32_t allSettingsCount = 527; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, {QZSettings::bluetooth_no_reconnection, QZSettings::default_bluetooth_no_reconnection}, @@ -1112,7 +1160,7 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::sole_elliptical_inclination, QZSettings::default_sole_elliptical_inclination}, {QZSettings::proform_hybrid_trainer_xt, QZSettings::default_proform_hybrid_trainer_xt}, {QZSettings::gears_restore_value, QZSettings::default_gears_restore_value}, - {QZSettings::gears_current_value, QZSettings::gears_current_value}, + {QZSettings::gears_current_value, QZSettings::default_gears_current_value}, {QZSettings::tile_pace_last500m_enabled, QZSettings::default_tile_pace_last500m_enabled}, {QZSettings::tile_pace_last500m_order, QZSettings::default_tile_pace_last500m_order}, {QZSettings::treadmill_difficulty_gain_or_offset, QZSettings::default_treadmill_difficulty_gain_or_offset}, @@ -1164,6 +1212,48 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::strava_auth_external_webbrowser, QZSettings::default_strava_auth_external_webbrowser}, {QZSettings::gears_from_bike, QZSettings::default_gears_from_bike}, {QZSettings::peloton_spinups_autoresistance, QZSettings::default_peloton_spinups_autoresistance}, + {QZSettings::eslinker_costaway, QZSettings::default_eslinker_costaway}, + {QZSettings::treadmill_inclination_ovveride_gain, QZSettings::default_treadmill_inclination_ovveride_gain}, + {QZSettings::treadmill_inclination_ovveride_offset, QZSettings::default_treadmill_inclination_ovveride_offset}, + {QZSettings::bh_spada_2_watt, QZSettings::default_bh_spada_2_watt}, + {QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton}, + {QZSettings::sole_treadmill_inclination_fast, QZSettings::default_sole_treadmill_inclination_fast}, + {QZSettings::zwift_ocr, QZSettings::default_zwift_ocr}, + {QZSettings::fit_file_saved_on_quit, QZSettings::default_fit_file_saved_on_quit}, + {QZSettings::gem_module_inclination, QZSettings::default_gem_module_inclination}, + {QZSettings::treadmill_simulate_inclination_with_speed, + QZSettings::default_treadmill_simulate_inclination_with_speed}, + {QZSettings::garmin_companion, QZSettings::default_garmin_companion}, + {QZSettings::peloton_companion_workout_ocr, QZSettings::default_companion_peloton_workout_ocr}, + {QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical}, + {QZSettings::gears_gain, QZSettings::default_gears_gain}, + {QZSettings::proform_treadmill_8_0, QZSettings::default_proform_treadmill_8_0}, + {QZSettings::zero_zt2500_treadmill, QZSettings::default_zero_zt2500_treadmill}, + {QZSettings::kingsmith_encrypt_v5, QZSettings::default_kingsmith_encrypt_v5}, + {QZSettings::peloton_rower_level, QZSettings::default_peloton_rower_level}, + {QZSettings::tile_target_pace_enabled, QZSettings::default_tile_target_pace_enabled}, + {QZSettings::tile_target_pace_order, QZSettings::default_tile_target_pace_order}, + {QZSettings::tts_act_target_pace, QZSettings::default_tts_act_target_pace}, + {QZSettings::csafe_rower, QZSettings::default_csafe_rower}, + {QZSettings::ftms_rower, QZSettings::default_ftms_rower}, + {QZSettings::zwift_workout_ocr, QZSettings::default_zwift_workout_ocr}, + {QZSettings::proform_bike_sb, QZSettings::default_proform_bike_sb}, + {QZSettings::fakedevice_rower, QZSettings::default_fakedevice_rower}, + {QZSettings::zwift_ocr_climb_portal, QZSettings::default_zwift_ocr_climb_portal}, + {QZSettings::poll_device_time, QZSettings::default_poll_device_time}, + {QZSettings::proform_bike_PFEVEX71316_1, QZSettings::default_proform_bike_PFEVEX71316_1}, + {QZSettings::schwinn_bike_resistance_v3, QZSettings::default_schwinn_bike_resistance_v3}, + {QZSettings::watt_ignore_builtin, QZSettings::default_watt_ignore_builtin}, + {QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i}, + {QZSettings::ftms_bike, QZSettings::default_ftms_bike}, + {QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill}, + {QZSettings::ant_speed_offset, QZSettings::default_ant_speed_offset}, + {QZSettings::ant_speed_gain, QZSettings::default_ant_speed_gain}, + {QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl}, + {QZSettings::strava_date_prefix, QZSettings::default_strava_date_prefix}, + {QZSettings::race_mode, QZSettings::default_race_mode}, + {QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill}, + {QZSettings::saris_trainer, QZSettings::default_saris_trainer}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index c898b2ccc..8a2735d32 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -1652,7 +1652,7 @@ class QZSettings { static constexpr bool default_gears_restore_value = false; static const QString gears_current_value; - static constexpr int default_gears_current_value = 0; + static constexpr double default_gears_current_value = 0; static const QString tile_pace_last500m_enabled; static constexpr bool default_tile_pace_last500m_enabled = true; @@ -1776,6 +1776,135 @@ class QZSettings { static const QString peloton_spinups_autoresistance; static constexpr bool default_peloton_spinups_autoresistance = true; + static const QString eslinker_costaway; + static constexpr bool default_eslinker_costaway = false; + + static const QString treadmill_inclination_ovveride_gain; + static constexpr double default_treadmill_inclination_ovveride_gain = 1.0; + + static const QString treadmill_inclination_ovveride_offset; + static constexpr double default_treadmill_inclination_ovveride_offset = 0.0; + + static const QString bh_spada_2_watt; + static constexpr bool default_bh_spada_2_watt = false; + + static const QString tacx_neo2_peloton; + static constexpr bool default_tacx_neo2_peloton = false; + + static const QString sole_treadmill_inclination_fast; + static constexpr bool default_sole_treadmill_inclination_fast = false; + + static const QString zwift_ocr; + static constexpr bool default_zwift_ocr = false; + + static const QString fit_file_saved_on_quit; + static constexpr bool default_fit_file_saved_on_quit = false; + + static const QString gem_module_inclination; + static constexpr bool default_gem_module_inclination = false; + + static const QString treadmill_simulate_inclination_with_speed; + static constexpr bool default_treadmill_simulate_inclination_with_speed = false; + + static const QString garmin_companion; + static constexpr bool default_garmin_companion = false; + + static const QString peloton_companion_workout_ocr; + static constexpr bool default_companion_peloton_workout_ocr = false; + + static const QString iconcept_elliptical; + static constexpr bool default_iconcept_elliptical = false; + + static const QString gears_gain; + static constexpr double default_gears_gain = 1.0; + + static const QString proform_treadmill_8_0; + static constexpr bool default_proform_treadmill_8_0 = false; + + static const QString zero_zt2500_treadmill; + static constexpr bool default_zero_zt2500_treadmill = false; + + static const QString kingsmith_encrypt_v5; + static constexpr bool default_kingsmith_encrypt_v5 = false; + + static const QString peloton_rower_level; + static constexpr int default_peloton_rower_level = 1; + + static const QString tile_target_pace_enabled; + static constexpr bool default_tile_target_pace_enabled = false; + + static const QString tile_target_pace_order; + static constexpr int default_tile_target_pace_order = 50; + + static const QString tts_act_target_pace; + static constexpr bool default_tts_act_target_pace = false; + + static const QString csafe_rower; + static const QString default_csafe_rower; + + static const QString ftms_rower; + static const QString default_ftms_rower; + + static const QString zwift_workout_ocr; + static constexpr bool default_zwift_workout_ocr = false; + + static const QString proform_bike_sb; + static constexpr bool default_proform_bike_sb = false; + + static const QString fakedevice_rower; + static constexpr bool default_fakedevice_rower = false; + + static const QString zwift_ocr_climb_portal; + static constexpr bool default_zwift_ocr_climb_portal = false; + + static const QString poll_device_time; + static constexpr int default_poll_device_time = 200; + + static const QString proform_bike_PFEVEX71316_1; + static constexpr bool default_proform_bike_PFEVEX71316_1 = false; + + static const QString schwinn_bike_resistance_v3; + static constexpr bool default_schwinn_bike_resistance_v3 = false; + + static const QString watt_ignore_builtin; + static constexpr bool default_watt_ignore_builtin = true; + + static const QString proform_treadmill_z1300i; + static constexpr bool default_proform_treadmill_z1300i = false; + + static const QString ftms_bike; + static const QString default_ftms_bike; + + static const QString ftms_treadmill; + static const QString default_ftms_treadmill; + + static const QString proform_rower_sport_rl; + static constexpr bool default_proform_rower_sport_rl = false; + + static const QString strava_date_prefix; + static constexpr bool default_strava_date_prefix = false; + + /** + * @brief Adjusts value in a metric object that's configured specifically for measuring SPEED on ANT+. + */ + static const QString ant_speed_offset; + static constexpr float default_ant_speed_offset = 0; + + /** + * @brief Adjusts value in a metric object that's configured specifically for measuring SPEED on ANT+. + */ + static const QString ant_speed_gain; + static constexpr float default_ant_speed_gain = 1; + + static const QString race_mode; + static constexpr bool default_race_mode = false; + + static const QString proform_pro_1000_treadmill; + static constexpr bool default_proform_pro_1000_treadmill = false; + + static const QString saris_trainer; + static constexpr bool default_saris_trainer = false; + /** * @brief Write the QSettings values using the constants from this namespace. * @param showDefaults Optionally indicates if the default should be shown with the key. diff --git a/src/renphobike.cpp b/src/renphobike.cpp index 92e052057..b01c2a675 100644 --- a/src/renphobike.cpp +++ b/src/renphobike.cpp @@ -1,5 +1,5 @@ #include "renphobike.h" -#include "ios/lockscreen.h" +#include "ftmsbike.h" #include "virtualbike.h" #include #include @@ -9,9 +9,9 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" renphobike::renphobike(bool noWriteResistance, bool noHeartService) { @@ -47,10 +47,15 @@ void renphobike::writeCharacteristic(uint8_t *data, uint8_t data_len, QString in timeout.singleShot(300, &loop, SLOT(quit())); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); if (!disable_log) - debug(" >> " + QByteArray((const char *)data, data_len).toHex(' ') + " // " + info); + debug(" >> " + writeBuffer->toHex(' ') + " // " + info); loop.exec(); } @@ -72,9 +77,11 @@ void renphobike::forceResistance(resistance_t requestResistance) { // requestPower = powerFromResistanceRequest(requestResistance); uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00}; QSettings settings; - bool renpho_bike_double_resistance = settings.value(QZSettings::renpho_bike_double_resistance, QZSettings::default_renpho_bike_double_resistance).toBool(); + bool renpho_bike_double_resistance = + settings.value(QZSettings::renpho_bike_double_resistance, QZSettings::default_renpho_bike_double_resistance) + .toBool(); - if(renpho_bike_double_resistance) + if (renpho_bike_double_resistance) write[1] = ((uint8_t)(requestResistance)); else write[1] = ((uint8_t)(requestResistance * 2)); @@ -102,7 +109,7 @@ void renphobike::update() { // gattWriteCharacteristic.isValid() && // gattNotify1Characteristic.isValid() && /*initDone*/) { - update_metrics(true, watts()); + update_metrics(false, watts()); if (!autoResistanceEnable) { uint8_t write[] = {FTMS_STOP_PAUSE, 0x01}; @@ -110,6 +117,7 @@ void renphobike::update() { QStringLiteral("stopping control ") + QString::number(requestPower)); return; } else { + auto virtualBike = this->VirtualBike(); if (requestPower != -1) { debug("writing power request " + QString::number(requestPower)); QSettings settings; @@ -172,7 +180,9 @@ void renphobike::characteristicChanged(const QLowEnergyCharacteristic &character QSettings settings; QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); - bool renpho_bike_double_resistance = settings.value(QZSettings::renpho_bike_double_resistance, QZSettings::default_renpho_bike_double_resistance).toBool(); + bool renpho_bike_double_resistance = + settings.value(QZSettings::renpho_bike_double_resistance, QZSettings::default_renpho_bike_double_resistance) + .toBool(); debug(" << " + newValue.toHex(' ')); @@ -266,7 +276,7 @@ void renphobike::characteristicChanged(const QLowEnergyCharacteristic &character Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)))) / 2; - if(renpho_bike_double_resistance) + if (renpho_bike_double_resistance) Resistance = Resistance.value() * 2; emit resistanceRead(Resistance.value()); m_pelotonResistance = bikeResistanceToPeloton(Resistance.value()); @@ -349,16 +359,7 @@ void renphobike::characteristicChanged(const QLowEnergyCharacteristic &character lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); if (heartRateBeltName.startsWith("Disabled")) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } #ifdef Q_OS_IOS @@ -474,7 +475,7 @@ void renphobike::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -499,10 +500,11 @@ void renphobike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&renphobike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &renphobike::changeInclination); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &renphobike::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -521,8 +523,8 @@ uint16_t renphobike::ergModificator(uint16_t powerRequested) { powerRequested = ((powerRequested / watt_gain) - watt_offset); qDebug() << QStringLiteral("to") << powerRequested; - if (power_sensor && virtualBike) { - if (QDateTime::currentMSecsSinceEpoch() > (virtualBike->whenLastFTMSFrameReceived() + 5000)) { + if (power_sensor && this->VirtualBike()) { + if (QDateTime::currentMSecsSinceEpoch() > (this->VirtualBike()->whenLastFTMSFrameReceived() + 5000)) { double f = ((double)powerRequested * (double)powerRequested) / m_watt.average5s(); lastPowerRequestedFactor = f / powerRequested; powerRequested = f; @@ -570,9 +572,23 @@ void renphobike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &chara lastFTMSPacketReceived.append(r & 0xFF); lastFTMSPacketReceived.append(((r & 0xFF00) >> 8) & 0x00FF); qDebug() << QStringLiteral("sending") << lastFTMSPacketReceived.toHex(' '); + // handling gears + } else if (lastFTMSPacketReceived.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS) { + qDebug() << "applying gears mod" << m_gears; + int16_t slope = (((uint8_t)lastFTMSPacketReceived.at(3)) + (lastFTMSPacketReceived.at(4) << 8)); + if (m_gears != 0) { + slope += (m_gears * 50); + lastFTMSPacketReceived[3] = slope & 0xFF; + lastFTMSPacketReceived[4] = slope >> 8; + } + } + + if (writeBuffer) { + delete writeBuffer; } + writeBuffer = new QByteArray(lastFTMSPacketReceived); - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, lastFTMSPacketReceived); + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); } } @@ -700,9 +716,11 @@ double renphobike::bikeResistanceToPeloton(double resistance) { bool renpho_peloton_conversion_v2 = settings.value(QZSettings::renpho_peloton_conversion_v2, QZSettings::default_renpho_peloton_conversion_v2) .toBool(); - bool renpho_bike_double_resistance = settings.value(QZSettings::renpho_bike_double_resistance, QZSettings::default_renpho_bike_double_resistance).toBool(); + bool renpho_bike_double_resistance = + settings.value(QZSettings::renpho_bike_double_resistance, QZSettings::default_renpho_bike_double_resistance) + .toBool(); - if(renpho_bike_double_resistance) + if (renpho_bike_double_resistance) resistance = resistance / 2.0; if (!renpho_peloton_conversion_v2) { @@ -726,10 +744,6 @@ bool renphobike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *renphobike::VirtualBike() { return virtualBike; } - -void *renphobike::VirtualDevice() { return VirtualBike(); } - uint16_t renphobike::watts() { if (currentCadence().value() == 0) return 0; diff --git a/src/renphobike.h b/src/renphobike.h index 0d69c4f66..1930d8f2d 100644 --- a/src/renphobike.h +++ b/src/renphobike.h @@ -27,8 +27,6 @@ #include #include "bike.h" -#include "ftmsbike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -38,13 +36,10 @@ class renphobike : public bike { Q_OBJECT public: renphobike(bool noWriteResistance, bool noHeartService); - resistance_t pelotonToBikeResistance(int pelotonResistance); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; // uint8_t resistanceFromPowerRequest(uint16_t power); - bool connected(); - resistance_t maxResistance() { return max_resistance; } - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; + resistance_t maxResistance() override { return max_resistance; } private: const resistance_t max_resistance = 80; @@ -53,13 +48,11 @@ class renphobike : public bike { bool wait_for_response = false); void startDiscover(); uint16_t ergModificator(uint16_t powerRequested); - uint16_t watts(); + uint16_t watts() override; void forceResistance(resistance_t requestResistance); void forcePower(int16_t requestPower); - QTimer *refresh; - virtualbike *virtualBike = 0; - + QTimer *refresh; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; QLowEnergyService *gattFTMSService = nullptr; diff --git a/src/rower.cpp b/src/rower.cpp index 606bd5a9d..44e6d38ca 100644 --- a/src/rower.cpp +++ b/src/rower.cpp @@ -5,6 +5,12 @@ rower::rower() {} +void rower::changeSpeed(double speed) { + qDebug() << "changeSpeed" << speed; + RequestedSpeed = speed; + if (autoResistanceEnable) + requestSpeed = speed; +} void rower::changeResistance(resistance_t resistance) { if (autoResistanceEnable) { requestResistance = resistance * m_difficult; @@ -36,6 +42,7 @@ resistance_t rower::pelotonToBikeResistance(int pelotonResistance) { return pelo resistance_t rower::resistanceFromPowerRequest(uint16_t power) { return power / 10; } // in order to have something void rower::cadenceSensor(uint8_t cadence) { Cadence.setValue(cadence); } void rower::powerSensor(uint16_t power) { m_watt.setValue(power, false); } +double rower::requestedSpeed() { return requestSpeed; } bluetoothdevice::BLUETOOTH_TYPE rower::deviceType() { return bluetoothdevice::ROWING; } @@ -120,7 +127,7 @@ void rower::setLap() { QTime rower::averagePace() { QSettings settings; - //bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); + // bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); double unit_conversion = 1.0; /*if (miles) { unit_conversion = 0.621371; @@ -138,7 +145,7 @@ QTime rower::averagePace() { QTime rower::maxPace() { QSettings settings; - //bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); + // bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); double unit_conversion = 1.0; /*if (miles) { unit_conversion = 0.621371; @@ -152,28 +159,12 @@ QTime rower::maxPace() { } } - // min/500m -QTime rower::currentPace() { - QSettings settings; - // bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); - const double unit_conversion = 1.0; - // rowers are always in meters! - /*if (miles) { - unit_conversion = 0.621371; - }*/ - if (Speed.value() == 0) { - return QTime(0, 0, 0, 0); - } else { - double speed = Speed.value() * unit_conversion * 2.0; //*2 in order to change from min/km to min/500m - return QTime(0, (int)(1.0 / (speed / 60.0)), - (((double)(1.0 / (speed / 60.0)) - ((double)((int)(1.0 / (speed / 60.0))))) * 60.0), 0); - } -} +QTime rower::currentPace() { return speedToPace(Speed.value()); } // min/500m QTime rower::lastPace500m() { - + QSettings settings; // bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); const double unit_conversion = 1.0; @@ -183,20 +174,20 @@ QTime rower::lastPace500m() { }*/ // last 500m speed calculation - if(!paused && Speed.value() > 0) { + if (!paused && Speed.value() > 0) { double o = odometer(); speedLast500mValues.append(new rowerSpeedDistance(o, Speed.value())); - while(o > speedLast500mValues.first()->distance + 0.5) { + while (o > speedLast500mValues.first()->distance + 0.5) { delete speedLast500mValues.first(); speedLast500mValues.removeFirst(); } } - if(speedLast500mValues.count() == 0) - return QTime(0,0,0,0); - + if (speedLast500mValues.count() == 0) + return QTime(0, 0, 0, 0); + double avg = 0; - for(int i=0; ispeed; avg = avg / (double)speedLast500mValues.count(); @@ -204,3 +195,24 @@ QTime rower::lastPace500m() { return QTime(0, (int)(1.0 / (speed / 60.0)), (((double)(1.0 / (speed / 60.0)) - ((double)((int)(1.0 / (speed / 60.0))))) * 60.0), 0); } + +// min/500m +QTime rower::lastRequestedPace() { return speedToPace(lastRequestedSpeed().value()); } + +// min/500m +QTime rower::speedToPace(double Speed) { + QSettings settings; + // bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); + const double unit_conversion = 1.0; + // rowers are always in meters! + /*if (miles) { + unit_conversion = 0.621371; + }*/ + if (Speed == 0) { + return QTime(0, 0, 0, 0); + } else { + double speed = Speed * unit_conversion * 2.0; //*2 in order to change from min/km to min/500m + return QTime(0, (int)(1.0 / (speed / 60.0)), + (((double)(1.0 / (speed / 60.0)) - ((double)((int)(1.0 / (speed / 60.0))))) * 60.0), 0); + } +} diff --git a/src/rower.h b/src/rower.h index 5813a373d..4e53878d9 100644 --- a/src/rower.h +++ b/src/rower.h @@ -1,7 +1,6 @@ #ifndef ROWER_H #define ROWER_H -#include "bike.h" #include "bluetoothdevice.h" #include @@ -14,33 +13,38 @@ class rower : public bluetoothdevice { metric lastRequestedPelotonResistance(); metric lastRequestedCadence(); metric lastRequestedPower(); + metric lastRequestedSpeed() { return RequestedSpeed; } + QTime lastRequestedPace(); virtual QTime lastPace500m(); - virtual metric currentResistance(); + metric currentResistance() override; virtual metric currentStrokesCount(); virtual metric currentStrokesLength(); - virtual QTime currentPace(); - virtual QTime averagePace(); - virtual QTime maxPace(); - virtual uint8_t fanSpeed(); - virtual double currentCrankRevolutions(); - virtual uint16_t lastCrankEventTime(); - virtual bool connected(); + QTime currentPace() override; + QTime averagePace() override; + QTime maxPace() override; + virtual double requestedSpeed(); + uint8_t fanSpeed() override; + double currentCrankRevolutions() override; + uint16_t lastCrankEventTime() override; + bool connected() override; virtual uint16_t watts(); virtual resistance_t pelotonToBikeResistance(int pelotonResistance); virtual resistance_t resistanceFromPowerRequest(uint16_t power); - bluetoothdevice::BLUETOOTH_TYPE deviceType(); + bluetoothdevice::BLUETOOTH_TYPE deviceType() override; metric pelotonResistance(); - void clearStats(); - void setLap(); - void setPaused(bool p); + void clearStats() override; + void setLap() override; + void setPaused(bool p) override; + QTime speedToPace(double Speed); public slots: - virtual void changeResistance(resistance_t res); + void changeResistance(resistance_t res) override; virtual void changeCadence(int16_t cad); - virtual void changePower(int32_t power); + void changePower(int32_t power) override; virtual void changeRequestedPelotonResistance(int8_t resistance); - virtual void cadenceSensor(uint8_t cadence); - virtual void powerSensor(uint16_t power); + void cadenceSensor(uint8_t cadence) override; + void powerSensor(uint16_t power) override; + virtual void changeSpeed(double speed); signals: void bikeStarted(); @@ -54,6 +58,8 @@ class rower : public bluetoothdevice { double requestInclination = -100; metric RequestedCadence; metric RequestedPower; + metric RequestedSpeed; + volatile double requestSpeed = -1; metric StrokesLength; metric StrokesCount; uint16_t LastCrankEventTime = 0; @@ -63,13 +69,16 @@ class rower : public bluetoothdevice { metric m_pelotonResistance; class rowerSpeedDistance { - public: - rowerSpeedDistance(double distance, double speed) {this->distance = distance; this->speed = speed;} + public: + rowerSpeedDistance(double distance, double speed) { + this->distance = distance; + this->speed = speed; + } double distance; double speed; }; - QList speedLast500mValues; + QList speedLast500mValues; }; #endif // ROWER_H diff --git a/src/schwinn170bike.cpp b/src/schwinn170bike.cpp new file mode 100644 index 000000000..e4a109c20 --- /dev/null +++ b/src/schwinn170bike.cpp @@ -0,0 +1,582 @@ +#include "schwinn170bike.h" + +#include "ios/lockscreen.h" +#include "virtualbike.h" +#include +#include +#include +#include +#include + +#include +#include +#ifdef Q_OS_ANDROID +#include +#endif +#include "keepawakehelper.h" +#include + +using namespace std::chrono_literals; + +schwinn170bike::schwinn170bike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain) { + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->bikeResistanceGain = bikeResistanceGain; + this->bikeResistanceOffset = bikeResistanceOffset; + initDone = false; + connect(refresh, &QTimer::timeout, this, &schwinn170bike::update); + refresh->start(200ms); +} + +void schwinn170bike::writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic characteristic, + uint8_t *data, uint8_t data_len, QString info, bool disable_log, + bool wait_for_response) { + QEventLoop loop; + QTimer timeout; + if (wait_for_response) { + connect(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic, QByteArray)), &loop, SLOT(quit())); + timeout.singleShot(300, &loop, SLOT(quit())); + } else { + connect(service, SIGNAL(characteristicWritten(QLowEnergyCharacteristic, QByteArray)), &loop, SLOT(quit())); + timeout.singleShot(300, &loop, SLOT(quit())); + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + service->writeCharacteristic(characteristic, *writeBuffer); + + if (!disable_log) + debug(" >> " + writeBuffer->toHex(' ') + " // " + info); + + loop.exec(); +} + +void schwinn170bike::update() { + if (m_control->state() == QLowEnergyController::UnconnectedState) { + + emit disconnected(); + return; + } + + if (initRequest) { + + initRequest = false; + } else { + + update_metrics(false, watts()); + + // updating the treadmill console every second + if (sec1Update++ == (500 / refresh->interval())) { + + sec1Update = 0; + // updateDisplay(elapsed); + } + + if (requestResistance != -1) { + if (requestResistance > max_resistance) + requestResistance = max_resistance; + else if (requestResistance == 0) { + requestResistance = 1; + } + + if (requestResistance != currentResistance().value()) { + emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); + + // forceResistance(requestResistance); + } + requestResistance = -1; + } + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit bikeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + } +} + +void schwinn170bike::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void schwinn170bike::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + double heart = 0.0; + + // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length(); + Q_UNUSED(characteristic); + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + + qDebug() << QStringLiteral(" << ") << newValue.toHex(' ') << characteristic.uuid(); + + if (newValue.length() == 20) { + Resistance = newValue.at(18); + + emit resistanceRead(Resistance.value()); + return; + } + + if (newValue.length() != 14) + return; + + lastPacket = newValue; + + m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(7)) << 8) | (uint16_t)((uint8_t)newValue.at(6)))) / 100.0; + emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); + + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); + + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = ((double)(((uint16_t)((uint8_t)newValue.at(4)) << 8) | (uint16_t)((uint8_t)newValue.at(3)))) / 100.0; + } else { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); + + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + + if (watts()) + KCal += ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 + + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else +#endif + { + } + + if (Cadence.value() > 0) { + + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + + // if we change this, also change the wattsFromResistance function. We can create a standard function in order to + // have all the costants in one place (I WANT MORE TIME!!!) + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + double res = + (((sqrt(pow(br, 2.0) - + 4.0 * ar * + (cr - (m_watt.value() * 132.0 / (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + + double resistance; + if (isnan(res)) { + res = 0; + } + if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance).toBool()) + resistance = pelotonToBikeResistance(res); + else + resistance = res; + if (qFabs(resistance - Resistance.value()) >= + (double)settings.value(QZSettings::schwinn_resistance_smooth, QZSettings::default_schwinn_resistance_smooth) + .toInt()) { + m_pelotonResistance = res; + } else { + // to calculate correctly the averages + m_pelotonResistance = m_pelotonResistance.value(); + + qDebug() << QStringLiteral("resistance not updated cause to schwinn_resistance_smooth setting"); + } + + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + if (heart == 0.0) { + update_hr_from_external(); + } else { + Heart = heart; + } + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + + emit debug(QStringLiteral("Current Calculated Resistance: ") + QString::number(Resistance.value())); + emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs)); + emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); + + if (m_control->error() != QLowEnergyController::NoError) { + qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); + } +} + +void schwinn170bike::stateChanged(QLowEnergyService::ServiceState state) { + QSettings settings; + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state(); + if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) { + qDebug() << QStringLiteral("not all services discovered"); + return; + } + } + + if (state != QLowEnergyService::ServiceState::ServiceDiscovered) { + qDebug() << QStringLiteral("ignoring this state"); + return; + } + + qDebug() << QStringLiteral("all services discovered!"); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + if (s->state() == QLowEnergyService::ServiceDiscovered) { + // establish hook into notifications + connect(s, &QLowEnergyService::characteristicChanged, this, &schwinn170bike::characteristicChanged); + connect(s, &QLowEnergyService::characteristicWritten, this, &schwinn170bike::characteristicWritten); + connect(s, &QLowEnergyService::characteristicRead, this, &schwinn170bike::characteristicRead); + connect( + s, static_cast(&QLowEnergyService::error), + this, &schwinn170bike::errorService); + connect(s, &QLowEnergyService::descriptorWritten, this, &schwinn170bike::descriptorWritten); + connect(s, &QLowEnergyService::descriptorRead, this, &schwinn170bike::descriptorRead); + + qDebug() << s->serviceUuid() << QStringLiteral("connected!"); + + auto characteristics_list = s->characteristics(); + for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) { + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle(); + auto descriptors_list = c.descriptors(); + for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) { + qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); + } + + if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) { + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Indicate) == + QLowEnergyCharacteristic::Indicate) { + QByteArray descriptor; + descriptor.append((char)0x02); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Read) == QLowEnergyCharacteristic::Read) { + // s->readCharacteristic(c); + // qDebug() << s->serviceUuid() << c.uuid() << "reading!"; + } else if ((c.properties() & QLowEnergyCharacteristic::Write) == QLowEnergyCharacteristic::Write && + (c.uuid() == QBluetoothUuid(QStringLiteral("5ec4e520-9804-11e3-b4b9-0002a5d5c51b")) || + c.uuid() == QBluetoothUuid(QStringLiteral("1717b3c0-9803-11e3-90e1-0002a5d5c51b")))) { + qDebug() << s->serviceUuid() << c.uuid() << "writing!"; + uint8_t init[] = {0x07, 0x01, 0xd3, 0x00, 0x1f, 0x05, 0x01}; + writeCharacteristic(s, c, init, sizeof(init), "init"); + } else if ((c.properties() & QLowEnergyCharacteristic::WriteNoResponse) == + QLowEnergyCharacteristic::WriteNoResponse) { + qDebug() << s->serviceUuid() << c.uuid() << "writing!"; + uint8_t init[] = {0x07, 0x01, 0xd3, 0x00, 0x1f, 0x05, 0x01}; + writeCharacteristic(s, c, init, sizeof(init), "init"); + } + } + } + } + + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + QSettings settings; + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + if (virtual_device_enabled) { + emit debug(QStringLiteral("creating virtual bike interface...")); + auto virtualBike = + new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + // connect(virtualBike,&virtualbike::debug ,this,&ftmsbike::debug); + connect(virtualBike, &virtualbike::changeInclination, this, &schwinn170bike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + firstStateChanged = 1; + // ******************************************************************************************************** +} + +void schwinn170bike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' ')); + + initRequest = true; + emit connectedAndDiscovered(); +} + +void schwinn170bike::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + qDebug() << QStringLiteral("descriptorRead ") << descriptor.name() << descriptor.uuid() << newValue.toHex(' '); +} + +void schwinn170bike::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + + Q_UNUSED(characteristic); + emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' ')); +} + +void schwinn170bike::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + qDebug() << QStringLiteral("characteristicRead ") << characteristic.uuid() << newValue.toHex(' '); +} + +void schwinn170bike::serviceScanDone(void) { + emit debug(QStringLiteral("serviceScanDone")); + + initRequest = false; + auto services_list = m_control->services(); + for (const QBluetoothUuid &s : qAsConst(services_list)) { + gattCommunicationChannelService.append(m_control->createServiceObject(s)); + connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, + &schwinn170bike::stateChanged); + gattCommunicationChannelService.constLast()->discoverDetails(); + } +} + +void schwinn170bike::errorService(QLowEnergyService::ServiceError err) { + + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("schwinn170bike::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void schwinn170bike::error(QLowEnergyController::Error err) { + + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("schwinn170bike::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void schwinn170bike::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + + device.address().toString() + ')'); + { + bluetoothDevice = device; + + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); + connect(m_control, &QLowEnergyController::serviceDiscovered, this, &schwinn170bike::serviceDiscovered); + connect(m_control, &QLowEnergyController::discoveryFinished, this, &schwinn170bike::serviceScanDone); + connect(m_control, + static_cast(&QLowEnergyController::error), + this, &schwinn170bike::error); + connect(m_control, &QLowEnergyController::stateChanged, this, &schwinn170bike::controllerStateChanged); + + connect(m_control, + static_cast(&QLowEnergyController::error), + this, [this](QLowEnergyController::Error error) { + Q_UNUSED(error); + Q_UNUSED(this); + emit debug(QStringLiteral("Cannot connect to remote device.")); + emit disconnected(); + }); + connect(m_control, &QLowEnergyController::connected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("Controller connected. Search services...")); + m_control->discoverServices(); + }); + connect(m_control, &QLowEnergyController::disconnected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("LowEnergy controller disconnected")); + emit disconnected(); + }); + + // Connect + m_control->connectToDevice(); + return; + } +} + +bool schwinn170bike::connected() { + if (!m_control) { + + return false; + } + return m_control->state() == QLowEnergyController::DiscoveredState; +} + +uint16_t schwinn170bike::watts() { + if (currentCadence().value() == 0) { + return 0; + } + + return m_watt.value(); +} + +void schwinn170bike::controllerStateChanged(QLowEnergyController::ControllerState state) { + qDebug() << QStringLiteral("controllerStateChanged") << state; + if (state == QLowEnergyController::UnconnectedState && m_control) { + qDebug() << QStringLiteral("trying to connect back again..."); + + initDone = false; + m_control->connectToDevice(); + } +} + +resistance_t schwinn170bike::pelotonToBikeResistance(int pelotonResistance) { + QSettings settings; + bool schwinn_bike_resistance_v2 = + settings.value(QZSettings::schwinn_bike_resistance_v2, QZSettings::default_schwinn_bike_resistance_v2).toBool(); + if (!schwinn_bike_resistance_v2) { + if (pelotonResistance > 54) + return pelotonResistance; + if (pelotonResistance < 26) + return pelotonResistance / 5; + + // y = 0,04x2 - 1,32x + 11,8 + return ((0.04 * pow(pelotonResistance, 2)) - (1.32 * pelotonResistance) + 11.8); + } else { + if (pelotonResistance > 20) + return (((double)pelotonResistance - 20.0) * 1.25); + else + return 1; + } +} + +uint16_t schwinn170bike::wattsFromResistance(double resistance) { + QSettings settings; + + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + for (uint16_t i = 1; i < 2000; i += 5) { + double res = + (((sqrt(pow(br, 2.0) - + 4.0 * ar * + (cr - ((double)i * 132.0 / (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + + if (!isnan(res) && res >= resistance) { + return i; + } + } + + return 0; +} + +void schwinn170bike::resistanceFromFTMSAccessory(resistance_t res) { + ResistanceFromFTMSAccessory = res; + qDebug() << QStringLiteral("resistanceFromFTMSAccessory") << res; +} + +/* +uint8_t schwinn170bike::resistanceFromPowerRequest(uint16_t power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value() << power; + +if (Cadence.value() == 0) + return 1; + +for (int i = 1; i < max_resistance; i++) { + if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) { + resistance_t res = pelotonToBikeResistance(i); + qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i) + << wattsFromResistance(i + 1) << QStringLiteral("power=") << power << QStringLiteral("res=") + << res; + // if the SS2K didn't send resistance at all or + // only if the resistance requested is higher and the current wattage is lower than the target + // only if the resistance requested is lower and the current wattage is higher than the target + // the main issue about schwinn is that the formula to get the wattage from the resistance is not so good + // so we need to put some constraint in the ERG mode + if (ResistanceFromFTMSAccessory.value() == 0 || + ((power > m_watt.value() && res > (resistance_t)ResistanceFromFTMSAccessory.value()) || + ((power < m_watt.value() && res < (resistance_t)ResistanceFromFTMSAccessory.value())))) { + return res; + } else { + if (power > m_watt.value()) + return ResistanceFromFTMSAccessory.value() + 1; + else + return ResistanceFromFTMSAccessory.value() - 1; + } + } +} +if (power < wattsFromResistance(1)) + return 1; +else + return 1; +} +*/ diff --git a/src/schwinn170bike.h b/src/schwinn170bike.h new file mode 100644 index 000000000..884a4c21e --- /dev/null +++ b/src/schwinn170bike.h @@ -0,0 +1,105 @@ +#ifndef SCHWINN170BIKE_H +#define SCHWINN170BIKE_H + +#include +#include +#include +#include +#include +#include +#include +#include +//#include +//#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include + +#include "bike.h" +#include "virtualbike.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class schwinn170bike : public bike { + Q_OBJECT + public: + schwinn170bike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + bool ergManagedBySS2K() override { return true; } + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; + + private: + void writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic characteristic, uint8_t *data, + uint8_t data_len, QString info, bool disable_log = false, bool wait_for_response = false); + uint16_t wattsFromResistance(double resistance); + void startDiscover(); + uint16_t watts() override; + + QTimer *refresh; + + QList gattCommunicationChannelService; + + uint8_t sec1Update = 0; + QByteArray lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + + const resistance_t max_resistance = 100; + uint8_t bikeResistanceOffset = 4; + double bikeResistanceGain = 1.0; + + metric ResistanceFromFTMSAccessory; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + void resistanceFromFTMSAccessory(resistance_t res) override; + + private slots: + + void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void stateChanged(QLowEnergyService::ServiceState state); + void controllerStateChanged(QLowEnergyController::ControllerState state); + + void serviceDiscovered(const QBluetoothUuid &gatt); + void serviceScanDone(void); + void update(); + void error(QLowEnergyController::Error err); + void errorService(QLowEnergyService::ServiceError); +}; + +#endif // SCHWINN170BIKE_H diff --git a/src/schwinnic4bike.cpp b/src/schwinnic4bike.cpp index 90c47222f..6166496bb 100644 --- a/src/schwinnic4bike.cpp +++ b/src/schwinnic4bike.cpp @@ -1,6 +1,6 @@ #include "schwinnic4bike.h" -#include "ios/lockscreen.h" + #include "virtualbike.h" #include #include @@ -11,9 +11,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -319,10 +320,19 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara if (isnan(res)) { res = 0; } - if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance).toBool()) + + bool schwinn_bike_resistance_v2 = + settings.value(QZSettings::schwinn_bike_resistance_v2, QZSettings::default_schwinn_bike_resistance_v2).toBool(); + bool schwinn_bike_resistance_v3 = + settings.value(QZSettings::schwinn_bike_resistance_v3, QZSettings::default_schwinn_bike_resistance_v3).toBool(); + + if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance).toBool() || schwinn_bike_resistance_v2 || + schwinn_bike_resistance_v3) { resistance = pelotonToBikeResistance(res); - else + } else { resistance = res; + } + if (qFabs(resistance - Resistance.value()) >= (double)settings.value(QZSettings::schwinn_resistance_smooth, QZSettings::default_schwinn_resistance_smooth) .toInt()) { @@ -341,19 +351,8 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { if (heart == 0.0) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { - Heart = heart; } } @@ -371,6 +370,7 @@ void schwinnic4bike::characteristicChanged(const QLowEnergyCharacteristic &chara #endif #endif + emit debug(QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value())); emit debug(QStringLiteral("Current Calculated Resistance: ") + QString::number(Resistance.value())); emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs)); emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); @@ -408,7 +408,7 @@ void schwinnic4bike::stateChanged(QLowEnergyService::ServiceState state) { emit connectedAndDiscovered(); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -442,10 +442,11 @@ void schwinnic4bike::stateChanged(QLowEnergyService::ServiceState state) { double bikeResistanceGain = settings.value(QZSettings::bike_resistance_gain_f, QZSettings::default_bike_resistance_gain_f) .toDouble(); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&schwinnic4bike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &schwinnic4bike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -542,10 +543,6 @@ bool schwinnic4bike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *schwinnic4bike::VirtualBike() { return virtualBike; } - -void *schwinnic4bike::VirtualDevice() { return VirtualBike(); } - uint16_t schwinnic4bike::watts() { if (currentCadence().value() == 0) { return 0; @@ -568,7 +565,15 @@ resistance_t schwinnic4bike::pelotonToBikeResistance(int pelotonResistance) { QSettings settings; bool schwinn_bike_resistance_v2 = settings.value(QZSettings::schwinn_bike_resistance_v2, QZSettings::default_schwinn_bike_resistance_v2).toBool(); - if (!schwinn_bike_resistance_v2) { + bool schwinn_bike_resistance_v3 = + settings.value(QZSettings::schwinn_bike_resistance_v3, QZSettings::default_schwinn_bike_resistance_v3).toBool(); + if (schwinn_bike_resistance_v3) { + // y = -35,3 + 1,91x + -0,0358x^2 + 4,3E-04x^3 + if (pelotonResistance < 30) + return 0; + + return -35.3 + 1.91 * pelotonResistance - 0.0358 * pow(pelotonResistance, 2) + 4.3E-04 * pow(pelotonResistance, 3); + } else if (!schwinn_bike_resistance_v2) { if (pelotonResistance > 54) return pelotonResistance; if (pelotonResistance < 26) diff --git a/src/schwinnic4bike.h b/src/schwinnic4bike.h index f1f571641..ff9c279cc 100644 --- a/src/schwinnic4bike.h +++ b/src/schwinnic4bike.h @@ -29,7 +29,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,23 +38,19 @@ class schwinnic4bike : public bike { Q_OBJECT public: schwinnic4bike(bool noWriteResistance, bool noHeartService); - resistance_t pelotonToBikeResistance(int pelotonResistance); - bool ergManagedBySS2K() { return true; } - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + bool ergManagedBySS2K() override { return true; } + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, bool wait_for_response = false); uint16_t wattsFromResistance(double resistance); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService; QLowEnergyCharacteristic gattNotify1Characteristic; @@ -85,7 +80,7 @@ class schwinnic4bike : public bike { public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); - void resistanceFromFTMSAccessory(resistance_t res); + void resistanceFromFTMSAccessory(resistance_t res) override; private slots: diff --git a/src/settings-tiles.qml b/src/settings-tiles.qml index 23cbd6021..e86610ebf 100644 --- a/src/settings-tiles.qml +++ b/src/settings-tiles.qml @@ -185,6 +185,8 @@ ScrollView { property int tile_avg_watt_lap_order: 48 property bool tile_pace_last500m_enabled: true property int tile_pace_last500m_order: 49 + property bool tile_target_pace_enabled: false + property int tile_target_pace_order: 50 } @@ -219,7 +221,7 @@ ScrollView { id: okSpeedOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_speed_order = speedOrderTextField.displayText + onClicked: {settings.tile_speed_order = speedOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -228,7 +230,7 @@ ScrollView { text: qsTr("Speed in kilometers per hour. (To set your speed units to miles, go to Settings > General Options > Use Miles unit in UI).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -264,7 +266,7 @@ ScrollView { id: okinclinationOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_inclination_order = inclinationOrderTextField.displayText + onClicked: {settings.tile_inclination_order = inclinationOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -294,7 +296,7 @@ ScrollView { text: qsTr("Bike pedal cadence changes color to indicate how your cadence compares to the cadence called out in Peloton classes. The tile displays the following colors: white if there is no target cadence in the program, red if your cadence is lower than the target, green if your cadence matches the target, and orange if your cadence is higher than the target.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -325,7 +327,7 @@ ScrollView { id: okcadenceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_cadence_order = cadenceOrderTextField.displayText + onClicked: {settings.tile_cadence_order = cadenceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -335,7 +337,7 @@ ScrollView { text: qsTr("Bike pedal cadence in rotations per minute (RPM) or Treadmill cadence if a shoe-mounted cadence sensor or Apple Watch QZ app is used.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -371,7 +373,7 @@ ScrollView { id: okelevationOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_elevation_order = elevationOrderTextField.displayText + onClicked: {settings.tile_elevation_order = elevationOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -403,7 +405,7 @@ ScrollView { id: okcaloriesOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_calories_order = caloriesOrderTextField.displayText + onClicked: {settings.tile_calories_order = caloriesOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -412,7 +414,7 @@ ScrollView { text: qsTr("Estimated calories burned during session, calculated on weight, age, and watts.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -448,7 +450,7 @@ ScrollView { id: okodometerOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_odometer_order = odometerOrderTextField.displayText + onClicked: {settings.tile_odometer_order = odometerOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -457,7 +459,7 @@ ScrollView { text: qsTr("Estimated distance traveled during the session.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -493,7 +495,7 @@ ScrollView { id: okpaceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_pace_order = paceOrderTextField.displayText + onClicked: {settings.tile_pace_order = paceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -502,7 +504,7 @@ ScrollView { text: qsTr("Current pace per mile or kilometer (Treadmill, Elliptical and Rower)") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -538,7 +540,7 @@ ScrollView { id: okresistanceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_resistance_order = resistanceOrderTextField.displayText + onClicked: {settings.tile_resistance_order = resistanceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -547,7 +549,7 @@ ScrollView { text: qsTr("Displays your bike’s resistance. The +/- buttons can be used to change resistance, if your bike is compatible.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -583,7 +585,7 @@ ScrollView { id: okwattOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_watt_order = wattOrderTextField.displayText + onClicked: {settings.tile_watt_order = wattOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -592,7 +594,7 @@ ScrollView { text: qsTr("Displays the watts generated by your current effort. Watt is also referred to as output (for example, in Peloton). If your equipment does not communicate watts, QZ will calculate watts using resistance and cadence.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -628,7 +630,7 @@ ScrollView { id: okweightLossOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_weight_loss_order = weightLossOrderTextField.displayText + onClicked: {settings.tile_weight_loss_order = weightLossOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -637,7 +639,7 @@ ScrollView { text: qsTr("Estimation of weight loss during the session.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -674,7 +676,7 @@ ScrollView { id: okavgwattOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_avgwatt_order = avgwattOrderTextField.displayText + onClicked: {settings.tile_avgwatt_order = avgwattOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -683,7 +685,7 @@ ScrollView { text: qsTr("Average watts produced for the session.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -720,7 +722,7 @@ ScrollView { id: okavgwattLapOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_avg_watt_lap_order = avgwattLapOrderTextField.displayText + onClicked: {settings.tile_avg_watt_lap_order = avgwattLapOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -752,7 +754,7 @@ ScrollView { id: okftpOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_ftp_order = ftpOrderTextField.displayText + onClicked: {settings.tile_ftp_order = ftpOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -761,7 +763,7 @@ ScrollView { text: qsTr("Percentage of current FTP and current FTP zone.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -797,7 +799,7 @@ ScrollView { id: okheartrateOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_heart_order = heartrateOrderTextField.displayText + onClicked: {settings.tile_heart_order = heartrateOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -829,7 +831,7 @@ ScrollView { id: okfanOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_fan_order = fanOrderTextField.displayText + onClicked: {settings.tile_fan_order = fanOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -838,7 +840,7 @@ ScrollView { text: qsTr("Built-in treadmill fan speed (Treadmill only)") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -874,7 +876,7 @@ ScrollView { id: okjoulsOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_jouls_order = joulsOrderTextField.displayText + onClicked: {settings.tile_jouls_order = joulsOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -883,7 +885,7 @@ ScrollView { text: qsTr("Cumulative power produced during the session in kilojoules.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -919,7 +921,7 @@ ScrollView { id: okelapsedOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_elapsed_order = elapsedOrderTextField.displayText + onClicked: {settings.tile_elapsed_order = elapsedOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -928,7 +930,7 @@ ScrollView { text: qsTr("Total time from start of the session.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -964,7 +966,7 @@ ScrollView { id: okmovingTimeOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_moving_time_order = movingTimeOrderTextField.displayText + onClicked: {settings.tile_moving_time_order = movingTimeOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -973,7 +975,7 @@ ScrollView { text: qsTr("Total time moving during the session.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1009,7 +1011,7 @@ ScrollView { id: okpelotonOffsetOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_peloton_offset_order = pelotonOffsetOrderTextField.displayText + onClicked: {settings.tile_peloton_offset_order = pelotonOffsetOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1018,7 +1020,7 @@ ScrollView { text: qsTr("Allows you to sync resistance and cadence target changes with the Peloton coach’s callouts. If the targets are changing in QZ after the coach’s callouts, use the ‘+’ button to add seconds (essentially speeding QZ up). Use the ‘-’ button to slow QZ down. Use this tile in conjunction with the Remaining Time/Row tile (see below).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1054,7 +1056,7 @@ ScrollView { id: okPelotonRemainingOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_peloton_remaining_order = pelotonRemainingOrderTextField.displayText + onClicked: {settings.tile_peloton_remaining_order = pelotonRemainingOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1063,7 +1065,7 @@ ScrollView { text: qsTr("Displays time remaining in Peloton class.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1100,7 +1102,7 @@ ScrollView { id: okpelotonDifficultyOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_peloton_difficulty_order = pelotonDifficultyOrderTextField.displayText + onClicked: {settings.tile_peloton_difficulty_order = pelotonDifficultyOrderTextField.displayText; toast.show("Setting saved!"); } } } }*/ @@ -1132,7 +1134,7 @@ ScrollView { id: oklapElapsedOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_lapelapsed_order = lapElapsedOrderTextField.displayText + onClicked: {settings.tile_lapelapsed_order = lapElapsedOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1179,7 +1181,7 @@ ScrollView { id: okpeloton_resistanceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_peloton_resistance_order = peloton_resistanceOrderTextField.displayText + onClicked: {settings.tile_peloton_resistance_order = peloton_resistanceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1189,7 +1191,7 @@ ScrollView { text: qsTr("Resistance of your bike converted to the Peloton bike scale of 1 to 100.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1225,7 +1227,7 @@ ScrollView { id: oktarget_resistanceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_resistance_order = target_resistanceOrderTextField.displayText + onClicked: {settings.tile_target_resistance_order = target_resistanceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1234,7 +1236,7 @@ ScrollView { text: qsTr("Displays target resistance in your bike’s resistance scale. For example, during a Peloton class or Zwift session, you want the resistance displayed in this tile to match the Resistance Tile.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1271,7 +1273,7 @@ ScrollView { id: oktarget_peloton_resistanceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_peloton_resistance_order = target_peloton_resistanceOrderTextField.displayText + onClicked: {settings.tile_target_peloton_resistance_order = target_peloton_resistanceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1280,7 +1282,7 @@ ScrollView { text: qsTr("Displays target resistance converted to the Peloton bike scale of 1 to 100. For example, during a Peloton class, you want the resistance displayed in this tile to match the Peloton Resistance Tile.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1316,7 +1318,7 @@ ScrollView { id: oktarget_cadenceOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_cadence_order = target_cadenceOrderTextField.displayText + onClicked: {settings.tile_target_cadence_order = target_cadenceOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1325,7 +1327,7 @@ ScrollView { text: qsTr("Displays target cadence.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1361,7 +1363,7 @@ ScrollView { id: oktarget_powerOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_power_order = target_powerOrderTextField.displayText + onClicked: {settings.tile_target_power_order = target_powerOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1370,7 +1372,7 @@ ScrollView { text: qsTr("Displays target output (watts) when this information is provided by third-party apps.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1407,7 +1409,7 @@ ScrollView { id: oktarget_zoneOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_zone_order = target_zoneOrderTextField.displayText + onClicked: {settings.tile_target_zone_order = target_zoneOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1416,7 +1418,7 @@ ScrollView { text: qsTr("Displays the target power zone when this information is provided by third-party apps.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1452,10 +1454,43 @@ ScrollView { id: oktarget_speedOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_speed_order = target_speedOrderTextField.displayText + onClicked: {settings.tile_target_speed_order = target_speedOrderTextField.displayText; toast.show("Setting saved!"); } } } } + + AccordionCheckElement { + id: targetPaceEnabledAccordion + title: qsTr("Target Pace") + linkedBoolSetting: "tile_target_pace_enabled" + settings: settings + accordionContent: RowLayout { + spacing: 10 + Label { + id: labeltargetpaceOrder + text: qsTr("order index:") + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + ComboBox { + id: target_paceOrderTextField + model: rootItem.tile_order + displayText: settings.tile_target_pace_order + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActivated: { + displayText = target_paceOrderTextField.currentValue + } + } + Button { + id: oktarget_paceOrderButton + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: {settings.tile_target_pace_order = target_paceOrderTextField.displayText; toast.show("Setting saved!"); } + } + } + } + AccordionCheckElement { id: targetInclineEnabledAccordion title: qsTr("Target Incline") @@ -1483,7 +1518,7 @@ ScrollView { id: oktarget_inclineOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_target_incline_order = target_inclineOrderTextField.displayText + onClicked: {settings.tile_target_incline_order = target_inclineOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1514,7 +1549,7 @@ ScrollView { id: okwatt_kgOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_watt_kg_order = watt_kgOrderTextField.displayText + onClicked: {settings.tile_watt_kg_order = watt_kgOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1523,7 +1558,7 @@ ScrollView { text: qsTr("Calculates your output (watts) divided by your weight. This is the primary metric used by Zwift and similar apps to calculate your virtual speed. NOTE: This is a much better metric to use than Output/Watts when comparing your effort to other users. This is why Peloton’s leaderboard, which uses only Output, is flawed.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1559,7 +1594,7 @@ ScrollView { id: okgearsOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_gears_order = gearsOrderTextField.displayText + onClicked: {settings.tile_gears_order = gearsOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1568,7 +1603,7 @@ ScrollView { text: qsTr("Allows you to change resistance while in Auto-Follow Mode.This tile allows you override the target resistance sent by third-party apps. For example, you would use the Gears Tile to increase resistance and generate more watts for sprinting in Zwift.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1604,7 +1639,7 @@ ScrollView { id: okremainingTimeTrainingProgramRowOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_remainingtimetrainprogramrow_order = remainingTimeTrainingProgramRowOrderTextField.displayText + onClicked: {settings.tile_remainingtimetrainprogramrow_order = remainingTimeTrainingProgramRowOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1613,7 +1648,7 @@ ScrollView { text: qsTr("Displays the time remaining until the next cadence and/or resistance interval.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1649,7 +1684,7 @@ ScrollView { id: oknextRowsTrainingProgramOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_nextrowstrainprogram_order = nextRowsTrainingProgramOrderTextField.displayText + onClicked: {settings.tile_nextrowstrainprogram_order = nextRowsTrainingProgramOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1658,7 +1693,7 @@ ScrollView { text: qsTr("Displays the next Peloton interval with duration and FTP Zone (in Power Zone classes) or Peloton Resistance (non–Power Zone classes).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1694,7 +1729,7 @@ ScrollView { id: okmetsOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_mets_order = metsOrderTextField.displayText + onClicked: {settings.tile_mets_order = metsOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1703,7 +1738,7 @@ ScrollView { text: qsTr("Displays metabolic equivalents (METs), a measurement of energy expenditure and amount of oxygen used by the body compared to the body at rest. (e.g., 4 METS requires the body to use 4 times as much oxygen than when at rest, which means it requires more energy and burns more calories).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1739,7 +1774,7 @@ ScrollView { id: oktargetmetsOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_targetmets_order = targetmetsOrderTextField.displayText + onClicked: {settings.tile_targetmets_order = targetmetsOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1771,7 +1806,7 @@ ScrollView { id: okdatetimeOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_datetime_order = datetimeOrderTextField.displayText + onClicked: {settings.tile_datetime_order = datetimeOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1780,7 +1815,7 @@ ScrollView { text: qsTr("Displays the current time.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1816,7 +1851,7 @@ ScrollView { id: okstrokes_countOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_strokes_count_order = strokes_countOrderTextField.displayText + onClicked: {settings.tile_strokes_count_order = strokes_countOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1825,7 +1860,7 @@ ScrollView { text: qsTr("(Rower only) Displays the number of strokes rowed.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1861,7 +1896,7 @@ ScrollView { id: okstrokes_lengthOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_strokes_length_order = strokes_lengthOrderTextField.displayText + onClicked: {settings.tile_strokes_length_order = strokes_lengthOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1870,7 +1905,7 @@ ScrollView { text: qsTr("(Rower only) Displays the stroke length.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1906,7 +1941,7 @@ ScrollView { id: oksteeringAngleOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_steering_angle_order = steeringAngleOrderTextField.displayText + onClicked: {settings.tile_steering_angle_order = steeringAngleOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1915,7 +1950,7 @@ ScrollView { text: qsTr("(Elite Rizer only) Displays steering angle.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1951,7 +1986,7 @@ ScrollView { id: okpidHROrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_pid_hr_order = pidHROrderTextField.displayText + onClicked: {settings.tile_pid_hr_order = pidHROrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -1960,7 +1995,7 @@ ScrollView { text: qsTr("Use this tile to display the target heart rate zone in which you’ve chosen to work out in Settings > Training Program Options.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1996,7 +2031,7 @@ ScrollView { id: okextInclineOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_ext_incline_order = extInclineOrderTextField.displayText + onClicked: {settings.tile_ext_incline_order = extInclineOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2005,7 +2040,7 @@ ScrollView { text: qsTr("(Elite Rizer only) Allows control of the incline of external inclination equipment.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2041,7 +2076,7 @@ ScrollView { id: okStrideLengthOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_instantaneous_stride_length_order = strideLengthOrderTextField.displayText + onClicked: {settings.tile_instantaneous_stride_length_order = strideLengthOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2050,7 +2085,7 @@ ScrollView { text: qsTr("(requires a compatible footpod with accelerometer; treadmill only) Displays stride while walking or running.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2086,7 +2121,7 @@ ScrollView { id: okGroundContactOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_ground_contact_order = groundContactOrderTextField.displayText + onClicked: {settings.tile_ground_contact_order = groundContactOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2095,7 +2130,7 @@ ScrollView { text: qsTr("(requires a compatible footpod with accelerometer; treadmill only) Displays time foot is on contact with ground while walking or running.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2131,7 +2166,7 @@ ScrollView { id: okVerticalOscillationOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_vertical_oscillation_order = verticalOscillationOrderTextField.displayText + onClicked: {settings.tile_vertical_oscillation_order = verticalOscillationOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2140,7 +2175,7 @@ ScrollView { text: qsTr("(requires a compatible footpod with accelerometer; treadmill only) Displays the up and down movement while walking or running.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2176,7 +2211,7 @@ ScrollView { id: okPacelast500mOrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_pace_last500m_order = pacelast500mOrderTextField.displayText + onClicked: {settings.tile_pace_last500m_order = pacelast500mOrderTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2209,7 +2244,7 @@ ScrollView { id: okPresetResistance1OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_1_order = presetResistance1TextField.displayText + onClicked: {settings.tile_preset_resistance_1_order = presetResistance1TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2230,7 +2265,7 @@ ScrollView { id: okPresetResistance1ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_1_value = presetResistance1ValueTextField.displayText + onClicked: {settings.tile_preset_resistance_1_value = presetResistance1ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2251,7 +2286,7 @@ ScrollView { id: okPresetResistance1LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_1_label = presetResistance1LabelTextField.displayText + onClicked: {settings.tile_preset_resistance_1_label = presetResistance1LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2285,7 +2320,7 @@ ScrollView { id: okPresetResistance1ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_1_color = presetResistance1ColorTextField.displayText + onClicked: {settings.tile_preset_resistance_1_color = presetResistance1ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2318,7 +2353,7 @@ ScrollView { id: okPresetResistance2OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_2_order = presetResistance2TextField.displayText + onClicked: {settings.tile_preset_resistance_2_order = presetResistance2TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2339,7 +2374,7 @@ ScrollView { id: okPresetResistance2ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_2_value = presetResistance2ValueTextField.displayText + onClicked: {settings.tile_preset_resistance_2_value = presetResistance2ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2360,7 +2395,7 @@ ScrollView { id: okPresetResistance2LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_2_label = presetResistance2LabelTextField.displayText + onClicked: {settings.tile_preset_resistance_2_label = presetResistance2LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2394,7 +2429,7 @@ ScrollView { id: okPresetResistance2ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_2_color = presetResistance2ColorTextField.displayText + onClicked: {settings.tile_preset_resistance_2_color = presetResistance2ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2427,7 +2462,7 @@ ScrollView { id: okPresetResistance3OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_3_order = presetResistance3TextField.displayText + onClicked: {settings.tile_preset_resistance_3_order = presetResistance3TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2448,7 +2483,7 @@ ScrollView { id: okPresetResistance3ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_3_value = presetResistance3ValueTextField.displayText + onClicked: {settings.tile_preset_resistance_3_value = presetResistance3ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2469,7 +2504,7 @@ ScrollView { id: okPresetResistance3LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_3_label = presetResistance3LabelTextField.displayText + onClicked: {settings.tile_preset_resistance_3_label = presetResistance3LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2503,7 +2538,7 @@ ScrollView { id: okPresetResistance3ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_3_color = presetResistance3ColorTextField.displayText + onClicked: {settings.tile_preset_resistance_3_color = presetResistance3ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2536,7 +2571,7 @@ ScrollView { id: okPresetResistance4OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_4_order = presetResistance4TextField.displayText + onClicked: {settings.tile_preset_resistance_4_order = presetResistance4TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2557,7 +2592,7 @@ ScrollView { id: okPresetResistance4ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_4_value = presetResistance4ValueTextField.displayText + onClicked: {settings.tile_preset_resistance_4_value = presetResistance4ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2578,7 +2613,7 @@ ScrollView { id: okPresetResistance4LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_4_label = presetResistance4LabelTextField.displayText + onClicked: {settings.tile_preset_resistance_4_label = presetResistance4LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2612,7 +2647,7 @@ ScrollView { id: okPresetResistance4ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_4_color = presetResistance4ColorTextField.displayText + onClicked: {settings.tile_preset_resistance_4_color = presetResistance4ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2645,7 +2680,7 @@ ScrollView { id: okPresetResistance5OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_5_order = presetResistance5TextField.displayText + onClicked: {settings.tile_preset_resistance_5_order = presetResistance5TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2666,7 +2701,7 @@ ScrollView { id: okPresetResistance5ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_5_value = presetResistance5ValueTextField.displayText + onClicked: {settings.tile_preset_resistance_5_value = presetResistance5ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2687,7 +2722,7 @@ ScrollView { id: okPresetResistance5LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_5_label = presetResistance5LabelTextField.displayText + onClicked: {settings.tile_preset_resistance_5_label = presetResistance5LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2721,7 +2756,7 @@ ScrollView { id: okPresetResistance5ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_resistance_5_color = presetResistance5ColorTextField.displayText + onClicked: {settings.tile_preset_resistance_5_color = presetResistance5ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2754,7 +2789,7 @@ ScrollView { id: okPresetSpeed1OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_1_order = presetSpeed1TextField.displayText + onClicked: {settings.tile_preset_speed_1_order = presetSpeed1TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2775,7 +2810,7 @@ ScrollView { id: okPresetSpeed1ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_1_value = presetSpeed1ValueTextField.displayText + onClicked: {settings.tile_preset_speed_1_value = presetSpeed1ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2796,7 +2831,7 @@ ScrollView { id: okPresetSpeed1LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_1_label = presetSpeed1LabelTextField.displayText + onClicked: {settings.tile_preset_speed_1_label = presetSpeed1LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2830,7 +2865,7 @@ ScrollView { id: okPresetSpeed1ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_1_color = presetSpeed1ColorTextField.displayText + onClicked: {settings.tile_preset_speed_1_color = presetSpeed1ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2863,7 +2898,7 @@ ScrollView { id: okPresetSpeed2OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_2_order = presetSpeed2TextField.displayText + onClicked: {settings.tile_preset_speed_2_order = presetSpeed2TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2884,7 +2919,7 @@ ScrollView { id: okPresetSpeed2ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_2_value = presetSpeed2ValueTextField.displayText + onClicked: {settings.tile_preset_speed_2_value = presetSpeed2ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2905,7 +2940,7 @@ ScrollView { id: okPresetSpeed2LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_2_label = presetSpeed2LabelTextField.displayText + onClicked: {settings.tile_preset_speed_2_label = presetSpeed2LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2939,7 +2974,7 @@ ScrollView { id: okPresetSpeed2ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_2_color = presetSpeed2ColorTextField.displayText + onClicked: {settings.tile_preset_speed_2_color = presetSpeed2ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -2972,7 +3007,7 @@ ScrollView { id: okPresetSpeed3OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_3_order = presetSpeed3TextField.displayText + onClicked: {settings.tile_preset_speed_3_order = presetSpeed3TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2993,7 +3028,7 @@ ScrollView { id: okPresetSpeed3ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_3_value = presetSpeed3ValueTextField.displayText + onClicked: {settings.tile_preset_speed_3_value = presetSpeed3ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3014,7 +3049,7 @@ ScrollView { id: okPresetSpeed3LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_3_label = presetSpeed3LabelTextField.displayText + onClicked: {settings.tile_preset_speed_3_label = presetSpeed3LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3048,7 +3083,7 @@ ScrollView { id: okPresetSpeed3ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_3_color = presetSpeed3ColorTextField.displayText + onClicked: {settings.tile_preset_speed_3_color = presetSpeed3ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3081,7 +3116,7 @@ ScrollView { id: okPresetSpeed4OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_4_order = presetSpeed4TextField.displayText + onClicked: {settings.tile_preset_speed_4_order = presetSpeed4TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3102,7 +3137,7 @@ ScrollView { id: okPresetSpeed4ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_4_value = presetSpeed4ValueTextField.displayText + onClicked: {settings.tile_preset_speed_4_value = presetSpeed4ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3123,7 +3158,7 @@ ScrollView { id: okPresetSpeed4LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_4_label = presetSpeed4LabelTextField.displayText + onClicked: {settings.tile_preset_speed_4_label = presetSpeed4LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3157,7 +3192,7 @@ ScrollView { id: okPresetSpeed4ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_4_color = presetSpeed4ColorTextField.displayText + onClicked: {settings.tile_preset_speed_4_color = presetSpeed4ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3190,7 +3225,7 @@ ScrollView { id: okPresetSpeed5OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_5_order = presetSpeed5TextField.displayText + onClicked: {settings.tile_preset_speed_5_order = presetSpeed5TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3211,7 +3246,7 @@ ScrollView { id: okPresetSpeed5ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_5_value = presetSpeed5ValueTextField.displayText + onClicked: {settings.tile_preset_speed_5_value = presetSpeed5ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3232,7 +3267,7 @@ ScrollView { id: okPresetSpeed5LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_5_label = presetSpeed5LabelTextField.displayText + onClicked: {settings.tile_preset_speed_5_label = presetSpeed5LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3266,7 +3301,7 @@ ScrollView { id: okPresetSpeed5ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_speed_5_color = presetSpeed5ColorTextField.displayText + onClicked: {settings.tile_preset_speed_5_color = presetSpeed5ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3299,7 +3334,7 @@ ScrollView { id: okPresetInclination1OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_1_order = presetInclination1TextField.displayText + onClicked: {settings.tile_preset_inclination_1_order = presetInclination1TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3320,7 +3355,7 @@ ScrollView { id: okPresetInclination1ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_1_value = presetInclination1ValueTextField.displayText + onClicked: {settings.tile_preset_inclination_1_value = presetInclination1ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3341,7 +3376,7 @@ ScrollView { id: okPresetInclination1LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_1_label = presetInclination1LabelTextField.displayText + onClicked: {settings.tile_preset_inclination_1_label = presetInclination1LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3375,7 +3410,7 @@ ScrollView { id: okPresetInclination1ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_1_color = presetInclination1ColorTextField.displayText + onClicked: {settings.tile_preset_inclination_1_color = presetInclination1ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3408,7 +3443,7 @@ ScrollView { id: okPresetInclination2OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_2_order = presetInclination2TextField.displayText + onClicked: {settings.tile_preset_inclination_2_order = presetInclination2TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3429,7 +3464,7 @@ ScrollView { id: okPresetInclination2ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_2_value = presetInclination2ValueTextField.displayText + onClicked: {settings.tile_preset_inclination_2_value = presetInclination2ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3450,7 +3485,7 @@ ScrollView { id: okPresetInclination2LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_2_label = presetInclination2LabelTextField.displayText + onClicked: {settings.tile_preset_inclination_2_label = presetInclination2LabelTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3485,7 +3520,7 @@ ScrollView { id: okPresetInclination2ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_2_color = presetInclination2ColorTextField.displayText + onClicked: {settings.tile_preset_inclination_2_color = presetInclination2ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3517,7 +3552,7 @@ ScrollView { id: okPresetInclination3OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_3_order = presetInclination3TextField.displayText + onClicked: {settings.tile_preset_inclination_3_order = presetInclination3TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3538,7 +3573,7 @@ ScrollView { id: okPresetInclination3ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_3_value = presetInclination3ValueTextField.displayText + onClicked: {settings.tile_preset_inclination_3_value = presetInclination3ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3559,7 +3594,7 @@ ScrollView { id: okPresetInclination3LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_3_label = presetInclination3LabelTextField.displayText + onClicked: {settings.tile_preset_inclination_3_label = presetInclination3LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3593,7 +3628,7 @@ ScrollView { id: okPresetInclination3ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_3_color = presetInclination3ColorTextField.displayText + onClicked: {settings.tile_preset_inclination_3_color = presetInclination3ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3626,7 +3661,7 @@ ScrollView { id: okPresetInclination4OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_4_order = presetInclination4TextField.displayText + onClicked: {settings.tile_preset_inclination_4_order = presetInclination4TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3647,7 +3682,7 @@ ScrollView { id: okPresetInclination4ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_4_value = presetInclination4ValueTextField.displayText + onClicked: {settings.tile_preset_inclination_4_value = presetInclination4ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3668,7 +3703,7 @@ ScrollView { id: okPresetInclination4LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_4_label = presetInclination4LabelTextField.displayText + onClicked: {settings.tile_preset_inclination_4_label = presetInclination4LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3702,7 +3737,7 @@ ScrollView { id: okPresetInclination4ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_4_color = presetInclination4ColorTextField.displayText + onClicked: {settings.tile_preset_inclination_4_color = presetInclination4ColorTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3735,7 +3770,7 @@ ScrollView { id: okPresetInclination5OrderButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_5_order = presetInclination5TextField.displayText + onClicked: {settings.tile_preset_inclination_5_order = presetInclination5TextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3756,7 +3791,7 @@ ScrollView { id: okPresetInclination5ValueButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_5_value = presetInclination5ValueTextField.displayText + onClicked: {settings.tile_preset_inclination_5_value = presetInclination5ValueTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3777,7 +3812,7 @@ ScrollView { id: okPresetInclination5LabelButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_5_label = presetInclination5LabelTextField.displayText + onClicked: {settings.tile_preset_inclination_5_label = presetInclination5LabelTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -3811,7 +3846,7 @@ ScrollView { id: okPresetInclination5ColorButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tile_preset_inclination_5_color = presetInclination5ColorTextField.displayText + onClicked: {settings.tile_preset_inclination_5_color = presetInclination5ColorTextField.displayText; toast.show("Setting saved!"); } } } } diff --git a/src/settings-treadmill-inclination-override.qml b/src/settings-treadmill-inclination-override.qml index 98e913896..fc2d75938 100644 --- a/src/settings-treadmill-inclination-override.qml +++ b/src/settings-treadmill-inclination-override.qml @@ -46,6 +46,9 @@ ScrollView { property double treadmill_inclination_override_140: 14.0 property double treadmill_inclination_override_145: 14.5 property double treadmill_inclination_override_150: 15.0 + + property double treadmill_inclination_ovveride_gain: 1.0 + property double treadmill_inclination_ovveride_offset: 0.0 } @@ -63,6 +66,53 @@ ScrollView { verticalAlignment: Text.AlignVCenter color: Material.color(Material.Red) } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Inclination Override Gain:") + Layout.fillWidth: true + } + TextField { + id: treadmillOverrideGainTextField + text: settings.treadmill_inclination_ovveride_gain + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.treadmill_inclination_ovveride_gain = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: {settings.treadmill_inclination_ovveride_gain = treadmillOverrideGainTextField.text; toast.show("Setting saved!"); } + } + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Inclination Override Offset:") + Layout.fillWidth: true + } + TextField { + id: treadmillOverrideOffsetTextField + text: settings.treadmill_inclination_ovveride_offset + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.treadmill_inclination_ovveride_offset = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: {settings.treadmill_inclination_ovveride_offset = treadmillOverrideOffsetTextField.text; toast.show("Setting saved!"); } + } + } + RowLayout { spacing: 10 Label { @@ -82,7 +132,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_0 = treadmillOverride0TextField.text + onClicked: {settings.treadmill_inclination_override_0 = treadmillOverride0TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -104,7 +154,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_05 = treadmillOverride05TextField.text + onClicked: {settings.treadmill_inclination_override_05 = treadmillOverride05TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -126,7 +176,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_10 = treadmillOverride10TextField.text + onClicked: {settings.treadmill_inclination_override_10 = treadmillOverride10TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -148,7 +198,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_15 = treadmillOverride15TextField.text + onClicked: {settings.treadmill_inclination_override_15 = treadmillOverride15TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -170,7 +220,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_20 = treadmillOverride20TextField.text + onClicked: {settings.treadmill_inclination_override_20 = treadmillOverride20TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -192,7 +242,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_25 = treadmillOverride25TextField.text + onClicked: {settings.treadmill_inclination_override_25 = treadmillOverride25TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -214,7 +264,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_30 = treadmillOverride30TextField.text + onClicked: {settings.treadmill_inclination_override_30 = treadmillOverride30TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -236,7 +286,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_35 = treadmillOverride35TextField.text + onClicked: {settings.treadmill_inclination_override_35 = treadmillOverride35TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -258,7 +308,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_40 = treadmillOverride40TextField.text + onClicked: {settings.treadmill_inclination_override_40 = treadmillOverride40TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -280,7 +330,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_45 = treadmillOverride45TextField.text + onClicked: {settings.treadmill_inclination_override_45 = treadmillOverride45TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -302,7 +352,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_50 = treadmillOverride50TextField.text + onClicked: {settings.treadmill_inclination_override_50 = treadmillOverride50TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -324,7 +374,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_55 = treadmillOverride55TextField.text + onClicked: {settings.treadmill_inclination_override_55 = treadmillOverride55TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -346,7 +396,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_60 = treadmillOverride60TextField.text + onClicked: {settings.treadmill_inclination_override_60 = treadmillOverride60TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -368,7 +418,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_65 = treadmillOverride65TextField.text + onClicked: {settings.treadmill_inclination_override_65 = treadmillOverride65TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -390,7 +440,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_70 = treadmillOverride70TextField.text + onClicked: {settings.treadmill_inclination_override_70 = treadmillOverride70TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -412,7 +462,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_75 = treadmillOverride75TextField.text + onClicked: {settings.treadmill_inclination_override_75 = treadmillOverride75TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -434,7 +484,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_80 = treadmillOverride80TextField.text + onClicked: {settings.treadmill_inclination_override_80 = treadmillOverride80TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -456,7 +506,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_85 = treadmillOverride85TextField.text + onClicked: {settings.treadmill_inclination_override_85 = treadmillOverride85TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -478,7 +528,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_90 = treadmillOverride90TextField.text + onClicked: {settings.treadmill_inclination_override_90 = treadmillOverride90TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -500,7 +550,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_95 = treadmillOverride95TextField.text + onClicked: {settings.treadmill_inclination_override_95 = treadmillOverride95TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -522,7 +572,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_100 = treadmillOverride100TextField.text + onClicked: {settings.treadmill_inclination_override_100 = treadmillOverride100TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -544,7 +594,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_105 = treadmillOverride105TextField.text + onClicked: {settings.treadmill_inclination_override_105 = treadmillOverride105TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -566,7 +616,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_110 = treadmillOverride110TextField.text + onClicked: {settings.treadmill_inclination_override_110 = treadmillOverride110TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -588,7 +638,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_115 = treadmillOverride115TextField.text + onClicked: {settings.treadmill_inclination_override_115 = treadmillOverride115TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -610,7 +660,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_120 = treadmillOverride120TextField.text + onClicked: {settings.treadmill_inclination_override_120 = treadmillOverride120TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -632,7 +682,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_125 = treadmillOverride125TextField.text + onClicked: {settings.treadmill_inclination_override_125 = treadmillOverride125TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -654,7 +704,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_130 = treadmillOverride130TextField.text + onClicked: {settings.treadmill_inclination_override_130 = treadmillOverride130TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -676,7 +726,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_135 = treadmillOverride135TextField.text + onClicked: {settings.treadmill_inclination_override_135 = treadmillOverride135TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -698,7 +748,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_140 = treadmillOverride140TextField.text + onClicked: {settings.treadmill_inclination_override_140 = treadmillOverride140TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -720,7 +770,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_145 = treadmillOverride145TextField.text + onClicked: {settings.treadmill_inclination_override_145 = treadmillOverride145TextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -742,7 +792,7 @@ ScrollView { Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_inclination_override_150 = treadmillOverride150TextField.text + onClicked: {settings.treadmill_inclination_override_150 = treadmillOverride150TextField.text; toast.show("Setting saved!"); } } } } diff --git a/src/settings-tts.qml b/src/settings-tts.qml index ddf6d835b..10bba1a76 100644 --- a/src/settings-tts.qml +++ b/src/settings-tts.qml @@ -57,6 +57,7 @@ ScrollView { property bool tts_avg_watt_kg: false property bool tts_max_watt_kg: false property bool tts_description_enabled: true + property bool tts_act_target_pace: false } @@ -109,7 +110,7 @@ ScrollView { id: okTTSSummarySec text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tts_summary_sec = ttsSummarySecTextField.text + onClicked: { settings.tts_summary_sec = ttsSummarySecTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -616,6 +617,20 @@ ScrollView { Layout.fillWidth: true onClicked: settings.tts_act_target_speed = checked } + SwitchDelegate { + id: ttsActualTargetPaceDelegate + text: qsTr("Actual Target Pace") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.tts_act_target_pace + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.tts_act_target_pace = checked + } SwitchDelegate { id: ttsActualTargetInclineDelegate text: qsTr("Actual Target Incline") diff --git a/src/settings.qml b/src/settings.qml index 5b5a059cf..0a514dfcc 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts 1.3 import QtQuick.Controls 2.15 import QtQuick.Controls.Material 2.0 import Qt.labs.settings 1.0 +import QtQuick.Dialogs 1.0 //Page { ScrollView { @@ -12,7 +13,7 @@ import Qt.labs.settings 1.0 anchors.fill: parent //anchors.bottom: footerSettings.top //anchors.bottomMargin: footerSettings.height + 10 - id: settingsPane + id: settingsPane Settings { id: settings @@ -641,7 +642,7 @@ import Qt.labs.settings 1.0 // from version 2.12.43 property bool proform_hybrid_trainer_xt: false property bool gears_restore_value: false - property int gears_current_value: 0 + property int gears_current_value: 0 // unused // from version 2.12.44 property bool tile_pace_last500m_enabled: true @@ -721,6 +722,122 @@ import Qt.labs.settings 1.0 // from version 2.13.4 property bool peloton_spinups_autoresistance: true + + // from version 2.13.10 + property bool eslinker_costaway: false + + // from version 2.13.14 + property double treadmill_inclination_ovveride_gain: 1.0 + property double treadmill_inclination_ovveride_offset: 0.0 + + // from version 2.13.15 + property bool bh_spada_2_watt: false + property bool tacx_neo2_peloton: false + + // from version 2.13.16 + property bool sole_treadmill_inclination_fast: false + + // from version 2.13.17 + property bool zwift_ocr: false + + // from version 2.13.18 + property bool gem_module_inclination: false + + // from version 2.13.19 + property bool treadmill_simulate_inclination_with_speed: false + + // from version 2.13.26 + property bool garmin_companion: false + + // from version 2.13.27 + property bool peloton_companion_workout_ocr: false + + // from version 2.13.31 + property bool iconcept_elliptical: false + + // from version 2.13.37 + property bool theme_tile_icon_enabled: true + property string theme_tile_background_color: "#303030" + property string theme_status_bar_background_color: "#800080" + + // from version 2.13.43 + property string theme_background_color: "#303030" + property bool theme_tile_shadow_enabled: true + property string theme_tile_shadow_color: "#9C27B0" + + // from version 2.13.44 + property double gears_gain: 1.0 + property double gears_current_value_f: 0 + + // from version 2.13.45 + property bool proform_treadmill_8_0: false + + // from version 2.13.50 + property bool zero_zt2500_treadmill: false + + // from version 2.13.52 + property bool kingsmith_encrypt_v5: false + + // from version 2.13.58 + property int peloton_rower_level: 1 + + // from version 2.13.61 + property bool tile_target_pace_enabled: false + property int tile_target_pace_order: 50 + property bool tts_act_target_pace: false + + // from version 2.13.62 + property string csafe_rower: "" + + // from version 2.13.63 + property string ftms_rower: "Disabled" + + // from version 2.13.71 + property int theme_tile_secondline_textsize: 12 + + // from version 2.13.80 + property bool fakedevice_rower: false + + // from version 2.13.81 + property bool proform_bike_sb: false + + // from version 2.13.86 + property bool zwift_workout_ocr: false + + // from version 2.13.96 + property bool zwift_ocr_climb_portal: false + property int poll_device_time: 200 + + // from version 2.13.99 + property bool proform_bike_PFEVEX71316_1: false + property bool schwinn_bike_resistance_v3: false + + // from version 2.15.2 + property bool watt_ignore_builtin: true + + // from version 2.16.4 + property bool proform_treadmill_z1300i: false + + // from version 2.16.5 + property string ftms_bike: "Disabled" + property string ftms_treadmill: "Disabled" + + // from version 2.16.6 + property real ant_speed_offset: 0 + property real ant_speed_gain: 1 + + // from version 2.16.12 + property bool proform_rower_sport_rl: false + + // from version 2.16.13 + property bool strava_date_prefix: false + + // from version 2.16.17 + property bool race_mode: false + + // from version 2.16.22 + property bool proform_pro_1000_treadmill: false + property bool saris_trainer: false } function paddingZeros(text, limit) { @@ -746,21 +863,13 @@ import Qt.labs.settings 1.0 } } + Component.onCompleted: window.settings_restart_to_apply = false; + ColumnLayout { id: column1 spacing: 0 anchors.fill: parent - Label { - Layout.preferredWidth: parent.width - id: rebootLabel - text: qsTr("Reboot the app in order to apply the settings") - textFormat: Text.PlainText - wrapMode: Text.WordWrap - verticalAlignment: Text.AlignVCenter - color: Material.color(Material.Red) - } - AccordionElement { id: generalOptionsAccordion title: qsTr("General Options") @@ -793,14 +902,14 @@ import Qt.labs.settings 1.0 id: okUiZoomButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ui_zoom = uiZoomTextField.text + onClicked: { settings.ui_zoom = uiZoomTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } Label { text: qsTr("This changes the size of the tiles that display your metrics. The default is 100%. To fit more tiles on your screen, choose a smaller percentage. To make them larger, choose a percentage over 100%. Do not enter the percent symbol") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -830,14 +939,14 @@ import Qt.labs.settings 1.0 id: okWeightButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.weight = (settings.miles_unit?weightTextField.text / 2.20462:weightTextField.text) + onClicked: { settings.weight = (settings.miles_unit?weightTextField.text / 2.20462:weightTextField.text); toast.show("Setting saved!"); } } } Label { text: qsTr("Enter your weight in kilograms so QZ can more accurately calculate calories burned. NOTE: If you choose to use miles as the unit for distance traveled, you will be asked to enter your weight in pounds (lbs).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -867,7 +976,7 @@ import Qt.labs.settings 1.0 id: okAgeButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.age = ageTextField.text + onClicked: { settings.age = ageTextField.text; toast.show("Setting saved!"); } } } @@ -875,7 +984,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enter your age so that calories burned can be more accurately calculated.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -907,7 +1016,7 @@ import Qt.labs.settings 1.0 id: okSex text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.sex = sexTextField.displayText + onClicked: { settings.sex = sexTextField.displayText; toast.show("Setting saved!"); } } } @@ -915,7 +1024,7 @@ import Qt.labs.settings 1.0 text: qsTr("Select your gender so that calories burned can be more accurately calculated.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -946,7 +1055,7 @@ import Qt.labs.settings 1.0 id: okFTPButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ftp = ftpTextField.text + onClicked: { settings.ftp = ftpTextField.text; toast.show("Setting saved!"); } } } @@ -954,7 +1063,7 @@ import Qt.labs.settings 1.0 text: qsTr("If you train to specific output (or watts) levels, for example in Peloton Power Zone classes,and have taken an FTP test (Functional Threshold Power), enter your FTP here. This number is used to calculate your Power Zones (Zones 1 to 7 for Peloton and 1 to 6 for Zwift).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -983,7 +1092,7 @@ import Qt.labs.settings 1.0 id: okNicknameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.user_nickname = nicknameTextField.text + onClicked: { settings.user_nickname = nicknameTextField.text; toast.show("Setting saved!"); } } } @@ -991,7 +1100,7 @@ import Qt.labs.settings 1.0 text: qsTr("No need to enter data here. It is for a possible future QZ feature.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1020,7 +1129,7 @@ import Qt.labs.settings 1.0 id: okEmailButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.user_email = emailTextField.text + onClicked: { settings.user_email = emailTextField.text; toast.show("Setting saved!"); } } } @@ -1028,7 +1137,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enter your email address to receive an automated email with stats and charts when you hit STOP at the end of each workout. Make sure there are no spaces before or after the email address; this is the most common reason the automated email is not sent. Privacy Note: Email addresses are not collected by the developer and are only saved locally on your device.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1056,7 +1165,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn on if you want QZ to display distance traveled in miles. Default is off and set to kilometers.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1084,7 +1193,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn on to set QZ to always open in PAUSE mode. This is important for Peloton classes so that you can sync the start of your QZ workout with the start of the Peloton class. Turn off to have QZ start tracking and timing your workout as soon as it opens.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1112,7 +1221,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn this on for: - Peloton Bootcamp classes or other workouts that are on and off the bike or treadmill. QZ will continue to track your workout even when you step away from your equipment. - Capturing non-equipment-based workouts, such as yoga or strength training. NOTE: All such workouts are labeled as “Rides” in Strava, but you can edit the label in Strava.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1128,7 +1237,7 @@ import Qt.labs.settings 1.0 text: qsTr("Zwift users: keep this setting off") font.bold: yes font.italic: yes - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1155,14 +1264,14 @@ import Qt.labs.settings 1.0 checked: settings.bike_heartrate_service Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.bike_heartrate_service = checked + onClicked: { settings.bike_heartrate_service = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("(For Android Version 10 and above, this setting cannot be changed. This setting can be changed for Android Version 9 and below and for iOS.) When this setting is turned off, QZ sends heart rate data in a format designed to improve compatibility with third-party apps, such as Zwift and Peloton. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1190,7 +1299,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn this on to prevent a built-in heart rate monitor (HRM) on your exercise equipment from sending that data to QZ. This allows QZ to connect to your external HRM, such as a chest band or Apple Watch.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1218,7 +1327,7 @@ import Qt.labs.settings 1.0 text: qsTr("This prevents your bike or treadmill from sending its calories-burned calculation to QZ and defaults to QZ’s more accurate calculation.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1251,7 +1360,7 @@ import Qt.labs.settings 1.0 id: okHeartBeltNameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.heart_rate_belt_name = heartBeltNameTextField.displayText; + onClicked: { settings.heart_rate_belt_name = heartBeltNameTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -1260,7 +1369,7 @@ import Qt.labs.settings 1.0 text: qsTr("Apple Watch users: leave it disabled! Just open the app on your watch") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1303,7 +1412,7 @@ import Qt.labs.settings 1.0 id: okHeartRateZone1Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.heart_rate_zone1 = heartRateZone1TextField.text + onClicked: { settings.heart_rate_zone1 = heartRateZone1TextField.text; toast.show("Setting saved!"); } } } @@ -1328,7 +1437,7 @@ import Qt.labs.settings 1.0 id: okHeartRateZone2Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.heart_rate_zone2 = heartRateZone2TextField.text + onClicked: { settings.heart_rate_zone2 = heartRateZone2TextField.text; toast.show("Setting saved!"); } } } @@ -1353,7 +1462,7 @@ import Qt.labs.settings 1.0 id: okHeartRateZone3Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.heart_rate_zone3 = heartRateZone3TextField.text + onClicked: { settings.heart_rate_zone3 = heartRateZone3TextField.text; toast.show("Setting saved!"); } } } @@ -1378,7 +1487,7 @@ import Qt.labs.settings 1.0 id: okHeartRateZone4Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.heart_rate_zone4 = heartRateZone4TextField.text + onClicked: { settings.heart_rate_zone4 = heartRateZone4TextField.text; toast.show("Setting saved!"); } } } @@ -1426,7 +1535,7 @@ import Qt.labs.settings 1.0 id: okHeartRateMaxOverrideValue text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.heart_max_override_value = heartRateMaxOverrideValueTextField.text + onClicked: { settings.heart_max_override_value = heartRateMaxOverrideValueTextField.text; toast.show("Setting saved!"); } } } } @@ -1436,7 +1545,7 @@ import Qt.labs.settings 1.0 text: qsTr("QZ uses a standard age-based calculation for maximum heart rate and then sets the heart rate zones based on that max heart rate. If you know your actual max heart rate (the highest your heart rate is known to reach), turn this option on and enter your actual max heart rate. Then click OK.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1451,7 +1560,7 @@ import Qt.labs.settings 1.0 text: qsTr("Choose the percentages for where you want your zones 1-4 to end and click OK.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1489,7 +1598,7 @@ import Qt.labs.settings 1.0 id: okPowerFromHeartPWR1 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.power_hr_pwr1 = powerFromHeartPWR1TextField.text + onClicked: { settings.power_hr_pwr1 = powerFromHeartPWR1TextField.text; toast.show("Setting saved!"); } } } @@ -1514,7 +1623,7 @@ import Qt.labs.settings 1.0 id: okPowerFromHeartHR1 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.power_hr_hr1 = powerFromHeartHR1TextField.text + onClicked: { settings.power_hr_hr1 = powerFromHeartHR1TextField.text; toast.show("Setting saved!"); } } } @@ -1539,7 +1648,7 @@ import Qt.labs.settings 1.0 id: okPowerFromHeartPWR2 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.power_hr_pwr2 = powerFromHeartPWR2TextField.text + onClicked: { settings.power_hr_pwr2 = powerFromHeartPWR2TextField.text; toast.show("Setting saved!"); } } } @@ -1564,7 +1673,7 @@ import Qt.labs.settings 1.0 id: okPowerFromHeartHR2 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.power_hr_hr2 = powerFromHeartHR2TextField.text + onClicked: { settings.power_hr_hr2 = powerFromHeartHR2TextField.text; toast.show("Setting saved!"); } } } } @@ -1573,7 +1682,7 @@ import Qt.labs.settings 1.0 text: qsTr("Expand the bars to the right to display the options under this setting. These settings are used to calculate power (watts) for bikes that do not have power meters. Instead QZ estimates power from your cadence and heart rate. You can calibrate how QZ calculates your power from heart rate as follows: If you know that at a stable pace you produce 100W of power at a heart rate of 150 BPM and 150W at 170 BPM, you can add these values under Sessions 1 and 2 Watt and HR and QZ will calculate your power based on that trend line.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1613,7 +1722,7 @@ import Qt.labs.settings 1.0 text: qsTr("QZ calculates speed based on your pedal cadence (RPMs). Enable this setting if you want your speed to be calculated based on your power output (watts), as Zwift and some other apps do. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1639,7 +1748,7 @@ import Qt.labs.settings 1.0 text: qsTr("QZ will remember the last Gears value and it will restore on startup") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1668,7 +1777,7 @@ import Qt.labs.settings 1.0 id: okRollingResistanceButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.rolling_resistance = rollingreistanceTextField.text + onClicked: { settings.rolling_resistance = rollingreistanceTextField.text; toast.show("Setting saved!"); } } } Label { @@ -1700,7 +1809,7 @@ import Qt.labs.settings 1.0 id: okBikeWeightButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.bike_weight = (settings.miles_unit?bikeweightTextField.text / 2.20462:bikeweightTextField.text) + onClicked: { settings.bike_weight = (settings.miles_unit?bikeweightTextField.text / 2.20462:bikeweightTextField.text); toast.show("Setting saved!"); } } } @@ -1708,7 +1817,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enables QZ to include the weight of your bike when calculating speed. For example, if you are competing against yourself on VZfit, adding bike weight will “level the playing field” against your virtual self. If you have set QZ to calculate distance in miles, enter the bike weight in pounds (lbs). Default unit is kilograms (kgs).") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1738,7 +1847,7 @@ import Qt.labs.settings 1.0 id: okCRRGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.crrGain = crrGainTextField.text + onClicked: { settings.crrGain = crrGainTextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -1762,7 +1871,7 @@ import Qt.labs.settings 1.0 id: okCWGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.cwGain = cwGainTextField.text + onClicked: { settings.cwGain = cwGainTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -1784,7 +1893,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enable this setting ONLY when using Zwift in ERG (workout) Mode. QZ will communicate the target resistance (or automatically adjust your resistance if your bike has this capability) to match the target watts based on your cadence (RPM). In ERG Mode, the changes in road slope will not affect target resistance, as is the case in Simulation Mode. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1814,7 +1923,7 @@ import Qt.labs.settings 1.0 id: okBikeResistanceOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.bike_resistance_offset = bikeResistanceOffsetTextField.text + onClicked: { settings.bike_resistance_offset = bikeResistanceOffsetTextField.text; toast.show("Setting saved!"); } } } @@ -1822,7 +1931,7 @@ import Qt.labs.settings 1.0 text: qsTr("This setting sets your “flat road” in Zwift. All communicated resistance changes will be based on this setting. The value entered is personal preference and will be dependent on your level of fitness. The suggested value for Echelon bikes is between 18 and 20. Default is 4.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1852,7 +1961,7 @@ import Qt.labs.settings 1.0 id: okBikeResistanceGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.bike_resistance_gain_f = bikeResistanceGainTextField.text + onClicked: { settings.bike_resistance_gain_f = bikeResistanceGainTextField.text; toast.show("Setting saved!"); } } } @@ -1860,7 +1969,7 @@ import Qt.labs.settings 1.0 text: qsTr("(for bikes and treadmills when using “treadmill as a bike” setting). This setting scales the resistance from your bike or the speed from your treadmill before sending it to Zwift. Default is 1.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1890,7 +1999,7 @@ import Qt.labs.settings 1.0 id: okzwiftErgFilterButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.zwift_erg_filter = zwiftErgFilterTextField.text + onClicked: { settings.zwift_erg_filter = zwiftErgFilterTextField.text; toast.show("Setting saved!"); } } } @@ -1898,7 +2007,7 @@ import Qt.labs.settings 1.0 text: qsTr("In ERG Mode or during a Power Zone workout on Peloton, the app sends a “target output” request. If the output requested doesn’t match your current output (calculated using cadence and resistance level), your target resistance will change to help you get closer to the target output. If the filter is set to higher values, you will get less adjustment of the target resistance and you will have to increase your cadence to match the target output. The Up and Down Watt Filter settings are the upper and lower margin before the adjustment of resistance is communicated. Example: if the up and down filters are set to 10 and the target output is 100 watts, a change of your resistance will only be communicated if your bike produces less than 90 Watts or more than 110 Watts. Default is 10.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1928,7 +2037,7 @@ import Qt.labs.settings 1.0 id: okzwiftErgDownFilterButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.zwift_erg_filter_down = zwiftErgDownFilterTextField.text + onClicked: { settings.zwift_erg_filter_down = zwiftErgDownFilterTextField.text; toast.show("Setting saved!"); } } } @@ -1936,7 +2045,7 @@ import Qt.labs.settings 1.0 text: qsTr("See above. Default is 10.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1949,7 +2058,7 @@ import Qt.labs.settings 1.0 spacing: 10 Label { id: labelZwiftErgResistanceDown - text: qsTr("ERG Min. Resistance:") + text: qsTr("Min. Resistance:") Layout.fillWidth: true } TextField { @@ -1966,7 +2075,7 @@ import Qt.labs.settings 1.0 id: okzwiftErgResistanceDownButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.zwift_erg_resistance_down = zwiftErgResistanceDownTextField.text + onClicked: { settings.zwift_erg_resistance_down = zwiftErgResistanceDownTextField.text; toast.show("Setting saved!"); } } } @@ -1974,7 +2083,7 @@ import Qt.labs.settings 1.0 text: qsTr("Use this setting to set a minimum target resistance. For example, if you do not want to ride at a resistance below 25, enter a value of 25 and QZ will not set a target resistance below 25. Default is 0.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1987,7 +2096,7 @@ import Qt.labs.settings 1.0 spacing: 10 Label { id: labelZwiftErgResistanceUp - text: qsTr("ERG Max. Resistance:") + text: qsTr("Max. Resistance:") Layout.fillWidth: true } TextField { @@ -2004,7 +2113,7 @@ import Qt.labs.settings 1.0 id: okzwiftErgResistanceUpButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.zwift_erg_resistance_up = zwiftErgResistanceUpTextField.text + onClicked: { settings.zwift_erg_resistance_up = zwiftErgResistanceUpTextField.text; toast.show("Setting saved!"); } } } @@ -2012,7 +2121,7 @@ import Qt.labs.settings 1.0 text: qsTr("Similar to the above, but sets a maximum target resistance. Default is 999.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2042,7 +2151,7 @@ import Qt.labs.settings 1.0 id: okBikeResistanceStartButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.bike_resistance_start = bikeResistanceStartTextField.text + onClicked: { settings.bike_resistance_start = bikeResistanceStartTextField.text; toast.show("Setting saved!"); } } } @@ -2050,7 +2159,82 @@ import Qt.labs.settings 1.0 text: qsTr("(only for bikes with electronically-controlled resistance): Enter the resistance level you want QZ to set at startup. Default is 1.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Gears Gain:") + Layout.fillWidth: true + } + TextField { + id: gearsGainTextField + text: settings.gears_gain + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + //inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.gears_gain = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.gears_gain = gearsGainTextField.text; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("Applies a multiplier to the gears tile. Default is 1.") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + Label { + text: qsTr("FTMS Bike:") + Layout.fillWidth: true + } + RowLayout { + spacing: 10 + ComboBox { + id: ftmsBikeTextField + model: rootItem.bluetoothDevices + displayText: settings.ftms_bike + Layout.fillHeight: false + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActivated: { + console.log("combomodel activated" + ftmsBikeTextField.currentIndex) + displayText = ftmsBikeTextField.currentValue + } + + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.ftms_bike = ftmsBikeTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("If you have a generic FTMS bike and the tiles doesn't appear on the main QZ screen, select here the bluetooth name of your bike.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2064,7 +2248,7 @@ import Qt.labs.settings 1.0 text: qsTr("Expand the bars to the right to display the options under this setting. Select your specific model (if it is listed) and leave all other settings on default. If you encounter problems or have a question about the QZ settings for your equipment, open a support ticket on GitHub or ask the QZ community on the QZ Facebook Group.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2097,7 +2281,7 @@ import Qt.labs.settings 1.0 } SwitchDelegate { id: schwinnBikeResistanceV2Delegate - text: qsTr("Resistance Alternative Calc.") + text: qsTr("Res. Alternative Calc. v2") spacing: 0 bottomPadding: 0 topPadding: 0 @@ -2109,6 +2293,19 @@ import Qt.labs.settings 1.0 Layout.fillWidth: true onClicked: settings.schwinn_bike_resistance_v2 = checked } + SwitchDelegate { + text: qsTr("Res. Alternative Calc. v3") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.schwinn_bike_resistance_v3 + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.schwinn_bike_resistance_v3 = checked + } RowLayout { spacing: 10 Label { @@ -2130,14 +2327,14 @@ import Qt.labs.settings 1.0 id: okschwinnResistanceSmoothButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.schwinn_resistance_smooth = scwhinnResistanceSmoothTextField.text + onClicked: { settings.schwinn_resistance_smooth = scwhinnResistanceSmoothTextField.text; toast.show("Setting saved!"); } } } Label { text: qsTr("Since this bike doesn't send resistance over bluetooth, QZ is calculating it using cadence and wattage. The result could be a little 'jumpy' and so, with this setting, you can filter the resistance tile value. The unit is a pure resistance level, so putting 5 means that you will see a resistance changes only when the resistance is changing by 5 levels.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2174,7 +2371,7 @@ import Qt.labs.settings 1.0 id: okhorizonGr7CadenceMultiplierButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.horizon_gr7_cadence_multiplier = horizonGr7CadenceMultiplierTextField.text + onClicked: { settings.horizon_gr7_cadence_multiplier = horizonGr7CadenceMultiplierTextField.text; toast.show("Setting saved!"); } } } } @@ -2213,7 +2410,7 @@ import Qt.labs.settings 1.0 id: okEchelonWattTable text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.echelon_watttable = echelonWattTableTextField.displayText + onClicked: { settings.echelon_watttable = echelonWattTableTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -2237,7 +2434,7 @@ import Qt.labs.settings 1.0 id: okechelonResistanceGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.echelon_resistance_gain = echelonResistanceGainTextField.text + onClicked: { settings.echelon_resistance_gain = echelonResistanceGainTextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -2261,7 +2458,7 @@ import Qt.labs.settings 1.0 id: okechelonResistanceOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.echelon_resistance_offset = echelonResistanceOffsetTextField.text + onClicked: { settings.echelon_resistance_offset = echelonResistanceOffsetTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -2375,9 +2572,30 @@ import Qt.labs.settings 1.0 checked: settings.hammer_racer_s Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.hammer_racer_s = checked + onClicked: { settings.hammer_racer_s = checked; window.settings_restart_to_apply = true; } + } + } + + AccordionElement { + title: qsTr("Saris/Cycleops Hammer trainer Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: SwitchDelegate { + text: qsTr("Enable support") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.saris_trainer + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.saris_trainer = checked; window.settings_restart_to_apply = true; } } } + AccordionElement { id: cardioFitBikeAccordion title: qsTr("CardioFIT Bike Options") @@ -2396,7 +2614,7 @@ import Qt.labs.settings 1.0 checked: settings.sp_ht_9600ie Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sp_ht_9600ie = checked + onClicked: { settings.sp_ht_9600ie = checked; window.settings_restart_to_apply = true; } } } AccordionElement { @@ -2439,7 +2657,7 @@ import Qt.labs.settings 1.0 checked: settings.snode_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.snode_bike = checked + onClicked: { settings.snode_bike = checked; window.settings_restart_to_apply = true; } } } AccordionElement { @@ -2461,7 +2679,7 @@ import Qt.labs.settings 1.0 checked: settings.fitplus_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fitplus_bike = checked + onClicked: { settings.fitplus_bike = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: virtufitEtappeBikeDelegate @@ -2475,7 +2693,7 @@ import Qt.labs.settings 1.0 checked: settings.virtufit_etappe Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.virtufit_etappe = checked + onClicked: { settings.virtufit_etappe = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: sportstechSx600BikeDelegate @@ -2489,7 +2707,7 @@ import Qt.labs.settings 1.0 checked: settings.sportstech_sx600 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sportstech_sx600 = checked + onClicked: { settings.sportstech_sx600 = checked; window.settings_restart_to_apply = true; } } } AccordionElement { @@ -2521,7 +2739,7 @@ import Qt.labs.settings 1.0 id: okflywheelBikeFilterButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.flywheel_filter = flywheelBikeFilterTextField.text + onClicked: { settings.flywheel_filter = flywheelBikeFilterTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -2536,7 +2754,7 @@ import Qt.labs.settings 1.0 checked: settings.flywheel_life_fitness_ic8 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.flywheel_life_fitness_ic8 = checked + onClicked: { settings.flywheel_life_fitness_ic8 = checked; window.settings_restart_to_apply = true; } } } } @@ -2567,7 +2785,7 @@ import Qt.labs.settings 1.0 id: okDomyosBikeCadenceFilter text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.domyos_bike_cadence_filter = domyosBikeCadenceFilterTextField.text + onClicked: { settings.domyos_bike_cadence_filter = domyosBikeCadenceFilterTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -2598,10 +2816,29 @@ import Qt.labs.settings 1.0 Layout.fillWidth: true onClicked: settings.domyos_bike_500_profile_v1 = checked } + } + AccordionElement { + title: qsTr("Tacx Neo Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + SwitchDelegate { + text: qsTr("Peloton Configuration") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.tacx_neo2_peloton + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.tacx_neo2_peloton = checked + } } AccordionElement { id: proformBikeAccordion - title: qsTr("Proform Bike Options") + title: qsTr("Proform/Norditrack Options") indicatRectColor: Material.color(Material.Grey) textColor: Material.color(Material.Yellow) color: Material.backgroundColor @@ -2626,7 +2863,7 @@ import Qt.labs.settings 1.0 id: okproformBikeWheelRatioButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.proform_wheel_ratio = proformBikeWheelRatioTextField.text + onClicked: { settings.proform_wheel_ratio = proformBikeWheelRatioTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -2641,7 +2878,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_tour_de_france_clc Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_tour_de_france_clc = checked + onClicked: { settings.proform_tour_de_france_clc = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proformStudiodelegate @@ -2655,7 +2892,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_studio Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_studio = checked + onClicked: { settings.proform_studio = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proformTDF10odelegate @@ -2669,7 +2906,20 @@ import Qt.labs.settings 1.0 checked: settings.proform_tdf_10 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_tdf_10 = checked + onClicked: { settings.proform_tdf_10 = checked; window.settings_restart_to_apply = true; } + } + SwitchDelegate { + text: qsTr("TDF 1.0 PFEVEX71316.1") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_bike_PFEVEX71316_1 + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_bike_PFEVEX71316_1 = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: nordictrackGX27odelegate @@ -2683,7 +2933,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_gx_2_7 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_gx_2_7 = checked + onClicked: { settings.nordictrack_gx_2_7 = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proformTdfJonseedWattdelegate @@ -2711,7 +2961,20 @@ import Qt.labs.settings 1.0 checked: settings.proform_cycle_trainer_400 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_cycle_trainer_400 = checked + onClicked: { settings.proform_cycle_trainer_400 = checked; window.settings_restart_to_apply = true; } + } + SwitchDelegate { + text: qsTr("Proform SB") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_bike_sb + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_bike_sb = checked; window.settings_restart_to_apply = true; } } RowLayout { @@ -2735,7 +2998,7 @@ import Qt.labs.settings 1.0 id: okproformTDF4IPButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.proformtdf4ip = proformTDF4IPTextField.text + onClicked: { settings.proformtdf4ip = proformTDF4IPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -2759,7 +3022,7 @@ import Qt.labs.settings 1.0 id: okproformTDFCompanionIPButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.tdf_10_ip = proformTDFCompanionIPTextField.text + onClicked: { settings.tdf_10_ip = proformTDFCompanionIPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -2774,7 +3037,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_ifit_adb_remote Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_ifit_adb_remote = checked + onClicked: { settings.nordictrack_ifit_adb_remote = checked; window.settings_restart_to_apply = true; } } } @@ -2805,7 +3068,7 @@ import Qt.labs.settings 1.0 id: okcomputrainerSerialPortButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.computrainer_serialport = computrainerSerialPortTextField.text + onClicked: { settings.computrainer_serialport = computrainerSerialPortTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } } @@ -2834,7 +3097,7 @@ import Qt.labs.settings 1.0 checked: settings.m3i_bike_qt_search Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.m3i_bike_qt_search = checked + onClicked: { settings.m3i_bike_qt_search = checked; window.settings_restart_to_apply = true; } } RowLayout { @@ -2858,7 +3121,7 @@ import Qt.labs.settings 1.0 id: okm3iBikeIdButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.m3i_bike_id = m3iBikeIdTextField.text + onClicked: { settings.m3i_bike_id = m3iBikeIdTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -2883,7 +3146,7 @@ import Qt.labs.settings 1.0 id: okm3iBikeSpeedBuffsizeButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.m3i_bike_speed_buffsize = m3iBikeSpeedBuffsizeTextField.text + onClicked: { settings.m3i_bike_speed_buffsize = m3iBikeSpeedBuffsizeTextField.text; toast.show("Setting saved!"); } } } @@ -2922,7 +3185,7 @@ import Qt.labs.settings 1.0 text: qsTr("Set 100mm as wheel circumference in settings of ant+ speed sensor") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2941,14 +3204,87 @@ import Qt.labs.settings 1.0 checked: settings.ant_cadence Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.ant_cadence = checked + onClicked: { settings.ant_cadence = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Turn this on if you need to use ANT+ along with Bluetooth. Power is also sent.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("ANT+ Speed Offset") + Layout.fillWidth: true + } + TextField { + id: antspeedOffsetTextField + text: settings.ant_speed_offset + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + onAccepted: settings.ant_speed_offset = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.ant_speed_offset = antspeedOffsetTextField.text; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("You can increase/decrease your speed sent over ANT+. The number you enter as an Offset adds that amount to your speed.") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + + RowLayout { + spacing: 10 + Label { + text: qsTr("ANT+ Speed Gain:") + Layout.fillWidth: true + } + TextField { + id: antspeedGainTextField + text: settings.ant_speed_gain + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + //inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.ant_speed_gain = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.ant_speed_gain = antspeedGainTextField.text; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("You can increase/decrease your speed output sent over ANT+. For example, to use a rower to cycle in Zwift, you could double your speed output to better match your cycling speed. The number you enter is a multiplier applied to your actual speed.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2969,14 +3305,14 @@ import Qt.labs.settings 1.0 checked: settings.ant_heart Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.ant_heart = checked + onClicked: { settings.ant_heart = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("This setting enables receiving the heart rate from an external HRM over ANT+ instead of from QZ.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3034,14 +3370,14 @@ import Qt.labs.settings 1.0 checked: settings.top_bar_enabled Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.top_bar_enabled = checked + onClicked: { settings.top_bar_enabled = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Allows continuous display of the Start/Pause and Stop buttons across the top of the screen during your workouts. Default is on.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3070,7 +3406,7 @@ import Qt.labs.settings 1.0 id: okFloatingWidthButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.floating_width = floatingWidthField.text + onClicked: { settings.floating_width = floatingWidthField.text; toast.show("Setting saved!"); } } } @@ -3078,7 +3414,7 @@ import Qt.labs.settings 1.0 text: qsTr("Android Only: width of the floating window.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3107,7 +3443,7 @@ import Qt.labs.settings 1.0 id: okFloatingHeightButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.floating_height = floatingHeightField.text + onClicked: { settings.floating_height = floatingHeightField.text; toast.show("Setting saved!"); } } } @@ -3115,7 +3451,7 @@ import Qt.labs.settings 1.0 text: qsTr("Android Only: height of the floating window.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3144,7 +3480,7 @@ import Qt.labs.settings 1.0 id: okFloatingTransparencyButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.floating_transparency = floatingTransparencyField.text + onClicked: { settings.floating_transparency = floatingTransparencyField.text; toast.show("Setting saved!"); } } } @@ -3152,7 +3488,7 @@ import Qt.labs.settings 1.0 text: qsTr("Android Only: transparency percentage of the floating window.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3180,7 +3516,7 @@ import Qt.labs.settings 1.0 text: qsTr("Android Only: if enabled the floating window will start as soon as the fitness devices is connected.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3188,6 +3524,206 @@ import Qt.labs.settings 1.0 Layout.fillWidth: true color: Material.color(Material.Lime) } + + Button { + text: "Open Floating on a Browser" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: openFloatingWindowBrowser(); + } + + AccordionElement { + id: themesOptionsAccordion + title: qsTr("UI Themes") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 10 + SwitchDelegate { + id: tilesIconsDelegate + text: qsTr("Tiles Icons") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.theme_tile_icon_enabled + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.theme_tile_icon_enabled = checked + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Background Color:") + Layout.fillWidth: true + } + TextField { + id: backgroundColorTextField + text: settings.theme_background_color + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + onPressed: { + if(OS_VERSION !== "Android") backgroundColorDialog.visible = true + } + } + Button { + id: okBackgroundColor + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.theme_background_color = backgroundColorTextField.text; toast.show("Setting saved!"); } + } + ColorDialog { + id: backgroundColorDialog + title: "Please choose a color" + onAccepted: { + backgroundColorTextField.text = this.color + visible = false; + } + onRejected: visible = false; + } + } + + RowLayout { + spacing: 10 + Label { + id: labelBackgroundColor + text: qsTr("Tiles Background Color:") + Layout.fillWidth: true + } + TextField { + id: tilebackgroundColorTextField + text: settings.theme_tile_background_color + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + onPressed: { + if(OS_VERSION !== "Android") tilebackgroundColorDialog.visible = true + } + } + Button { + id: oktileBackgroundColor + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.theme_tile_background_color = tilebackgroundColorTextField.text; toast.show("Setting saved!"); } + } + ColorDialog { + id: tilebackgroundColorDialog + title: "Please choose a color" + onAccepted: { + tilebackgroundColorTextField.text = this.color + visible = false; + } + onRejected: visible = false; + } + } + + SwitchDelegate { + text: qsTr("Tiles Shadow") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.theme_tile_shadow_enabled + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.theme_tile_shadow_enabled = checked + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Tiles Shadow Color:") + Layout.fillWidth: true + } + TextField { + id: tileShadowColorTextField + text: settings.theme_tile_shadow_color + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + onPressed: { + if(OS_VERSION !== "Android") tileShadowColorDialog.visible = true + } + } + Button { + id: oktileShadowColor + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.theme_tile_shadow_color = tileShadowColorTextField.text; toast.show("Setting saved!"); } + } + ColorDialog { + id: tileShadowColorDialog + title: "Please choose a color" + onAccepted: { + tileShadowColorTextField.text = this.color + visible = false; + } + onRejected: visible = false; + } + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Statusbar Background Color:") + Layout.fillWidth: true + } + TextField { + id: statusbarbackgroundColorTextField + text: settings.theme_status_bar_background_color + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + onPressed: { + if(OS_VERSION !== "Android") statusbarbackgroundColorDialog.visible = true + } + } + Button { + id: okStatusbarBackgroundColor + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.theme_status_bar_background_color = statusbarbackgroundColorTextField.text; toast.show("Setting saved!"); } + } + ColorDialog { + id: statusbarbackgroundColorDialog + title: "Please choose a color" + onAccepted: { + statusbarbackgroundColorTextField.text = this.color + visible = false; + } + onRejected: visible = false; + } + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("2nd line tile text size:") + Layout.fillWidth: true + } + TextField { + id: secondLineTextSizeField + text: settings.theme_tile_secondline_textsize + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onAccepted: settings.theme_tile_secondline_textsize = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.theme_tile_secondline_textsize = secondLineTextSizeField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + } + } } } @@ -3220,7 +3756,7 @@ import Qt.labs.settings 1.0 id: okPelotonUsernameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_username = pelotonUsernameTextField.text + onClicked: { settings.peloton_username = pelotonUsernameTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -3229,7 +3765,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enter the email address you use to login to Peloton (NOT your leaderboard name). Ensure there are no spaces before or after your email. Click OK.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3260,7 +3796,7 @@ import Qt.labs.settings 1.0 id: okPelotonPasswordButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_password = pelotonPasswordTextField.text + onClicked: { settings.peloton_password = pelotonPasswordTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -3268,7 +3804,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enter the password you use to login to Peloton. Click OK. If you have entered the correct login credentials and the QZ is able to access your account, you will see a when you reopen QZ. This is a secure login, not accessible by anyone but you.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3300,7 +3836,7 @@ import Qt.labs.settings 1.0 id: okPelotonDifficultyButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_difficulty = pelotonDifficultyTextField.displayText + onClicked: { settings.peloton_difficulty = pelotonDifficultyTextField.displayText; toast.show("Setting saved!"); } } } @@ -3308,7 +3844,45 @@ import Qt.labs.settings 1.0 text: qsTr("Typically, Peloton coaches call out a range for target incline, resistance and/or speed. Use this setting to choose the difficulty of the target QZ communicates. Difficulty level can be set to lower, upper or average. Click OK.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + RowLayout { + spacing: 10 + Label { + text: qsTr("Rower Level:") + Layout.fillWidth: true + } + ComboBox { + id: pelotonRowerLevelTextField + model: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ] + displayText: settings.peloton_rower_level + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActivated: { + console.log("combomodel activated" + pelotonRowerLevelTextField.currentIndex) + displayText = pelotonRowerLevelTextField.currentValue + } + + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.peloton_rower_level = parseInt(pelotonRowerLevelTextField.displayText); toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("Difficulty level for peloton rower classes. 1 is easy 6 is hard.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3337,7 +3911,7 @@ import Qt.labs.settings 1.0 id: okPZPUsernameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.pzp_username = pzpUsernameTextField.text + onClicked: { settings.pzp_username = pzpUsernameTextField.text; toast.show("Setting saved!"); } } } @@ -3345,7 +3919,7 @@ import Qt.labs.settings 1.0 text: qsTr("As of 4/1/2022, this feature is broken due to a Power Zone Pack (PZP) website change. Leave (or change back to) the default of “username” (without quotation marks, all lowercase and all one word) until further notice.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3376,7 +3950,7 @@ import Qt.labs.settings 1.0 id: okPZPPasswordButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.pzp_password = pzpPasswordTextField.text + onClicked: { settings.pzp_password = pzpPasswordTextField.text; toast.show("Setting saved!"); } } } @@ -3384,7 +3958,7 @@ import Qt.labs.settings 1.0 text: qsTr("As of 4/1/2022, this feature is broken due to a Power Zone Pack (PZP) website change. Leave this setting blank until further notice.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3414,7 +3988,7 @@ import Qt.labs.settings 1.0 id: okPelotonGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_gain = pelotonGainTextField.text + onClicked: { settings.peloton_gain = pelotonGainTextField.text; toast.show("Setting saved!"); } } } @@ -3422,7 +3996,7 @@ import Qt.labs.settings 1.0 text: qsTr("Conversion gain is a multiplier. Use this setting to align the Peloton resistance calculated by QZ with the relative effort required by your bike. In most cases the default values will be correct.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3452,7 +4026,7 @@ import Qt.labs.settings 1.0 id: okPelotonOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_offset = pelotonOffsetTextField.text + onClicked: { settings.peloton_offset = pelotonOffsetTextField.text; toast.show("Setting saved!"); } } } @@ -3460,7 +4034,7 @@ import Qt.labs.settings 1.0 text: qsTr("Increases the resistance that QZ displays in the Peloton Resistance tile. If QZ’s calculated conversion from your bike’s resistance scale to Peloton’s seems too low, the number you enter here will be added to the calculated resistance without increasing your effort or actual resistance. (Example: If QZ displays Peloton Resistance of 30 and you enter 5, QZ will display 35.)") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3481,14 +4055,14 @@ import Qt.labs.settings 1.0 checked: settings.bike_cadence_sensor Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.bike_cadence_sensor = checked + onClicked: { settings.bike_cadence_sensor = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Turn this on to send cadence to Peloton over Bluetooth. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3548,7 +4122,7 @@ import Qt.labs.settings 1.0 id: okPelotonHeartRateMetric text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_heartrate_metric = pelotonHeartRateMetricTextField.displayText; + onClicked: { settings.peloton_heartrate_metric = pelotonHeartRateMetricTextField.displayText; toast.show("Setting saved!"); } } } @@ -3556,7 +4130,7 @@ import Qt.labs.settings 1.0 text: qsTr("By default, QZ communicates heart rate to Peloton. Use this setting to change the metric that appears on the Peloton screen.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3588,7 +4162,7 @@ import Qt.labs.settings 1.0 id: okPelotonDateOnStrava text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.peloton_date = pelotonDateOnStravaTextField.displayText + onClicked: { settings.peloton_date = pelotonDateOnStravaTextField.displayText; toast.show("Setting saved!"); } } } @@ -3596,7 +4170,7 @@ import Qt.labs.settings 1.0 text: qsTr("Allows you to choose whether you would like the Peloton class air date to display before or after the class title on Strava.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3624,7 +4198,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn this on if you want QZ to capture a link to the Peloton class and display it in Strava.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3651,7 +4225,7 @@ import Qt.labs.settings 1.0 text: qsTr("By default, QZ treats Spin-UPS in Power Zone rides as an increasing ramp to warm you up. You can disable this, to leave the resistance up to you.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3672,14 +4246,41 @@ import Qt.labs.settings 1.0 checked: settings.peloton_workout_ocr Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.peloton_workout_ocr = checked + onClicked: { settings.peloton_workout_ocr = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Only for Android where QZ is running on the same Peloton device. This setting enables the AI (Artificial Intelligence) on QZ that will read the peloton workout screen and will adjust the peloton offset in order to stay in sync in realtime with your Peloton workout. A popup about screen recording will appear in order to notify this.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + SwitchDelegate { + text: qsTr("Peloton Auto Sync Companion (Exp.)") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.peloton_companion_workout_ocr + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.peloton_companion_workout_ocr = checked; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("This setting enables the AI (Artificial Intelligence) on the QZ Companion AI app that will read the peloton workout screen and will adjust the peloton offset in order to stay in sync in realtime with your Peloton workout.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3700,14 +4301,130 @@ import Qt.labs.settings 1.0 checked: settings.peloton_bike_ocr Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.peloton_bike_ocr = checked + onClicked: { settings.peloton_bike_ocr = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Only if you are on a real Peloton Bike/Bike+! This will allow QZ to collect metrics from your Bike/Bike+ and send it to Zwift. Peloton Free ride must running.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + } + } + + AccordionElement { + visible: OS_VERSION === "Other" + title: qsTr("Zwift Options") + "\uD83E\uDD47" + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Grey) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 0 + + SwitchDelegate { + text: qsTr("Zwift Treadmill Auto Inclination") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.zwift_ocr + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.zwift_ocr = checked; settings.zwift_workout_ocr = false; settings.zwift_ocr_climb_portal = false; settings.android_notification = true; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("Only for PC where QZ is running on the same Zwift device. This setting enables the AI (Artificial Intelligence) on QZ that will read the Zwift inclination from the Zwift app and will adjust the inclination on your treadmill. A popup about screen recording will appear in order to notify this.") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + SwitchDelegate { + text: qsTr("Zwift Treadmill Climb Portal") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.zwift_ocr_climb_portal + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.zwift_ocr_climb_portal = checked; settings.zwift_workout_ocr = false; settings.zwift_ocr = false; settings.android_notification = true; window.settings_restart_to_apply = true; } + } + + SwitchDelegate { + text: qsTr("Zwift Treadmill Auto Workout") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.zwift_workout_ocr + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.zwift_workout_ocr = checked; settings.zwift_ocr = false; settings.zwift_ocr_climb_portal = false; settings.android_notification = true; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("Only for PC where QZ is running on the same Zwift device. This setting enables the AI (Artificial Intelligence) on QZ that will read the Zwift inclination and speed from the Zwift app during a workout and will adjust the inclination and the speed on your treadmill. A popup about screen recording will appear in order to notify this.") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + } + } + + AccordionElement { + title: qsTr("Garmin Companion Options") + "\uD83E\uDD47" + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Grey) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 0 + + SwitchDelegate { + text: qsTr("Enable Companion App") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.garmin_companion + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.garmin_companion = checked; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("You have to install the QZ Companion App on your Garmin Watch/Computer first.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3747,7 +4464,7 @@ import Qt.labs.settings 1.0 text: qsTr("Treadmill only: enabling this if you want that QZ will stop the tape at the end of the current train program.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3781,7 +4498,7 @@ import Qt.labs.settings 1.0 id: okTreadmillPidHR text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_pid_heart_zone = treadmillPidHRTextField.displayText + onClicked: { settings.treadmill_pid_heart_zone = treadmillPidHRTextField.displayText; toast.show("Setting saved!"); } } } } @@ -3790,7 +4507,7 @@ import Qt.labs.settings 1.0 text: qsTr("QZ controls your treadmill or bike to keep you within a chosen Heart Rate Zone. Turn on, set a target heart rate (HR) zone in which to train and click OK. For example, enter 2 to train in HR zone 2 and the treadmill will auto adjust the speed (or resistance on a bike) to maintain your heart rate in zone 2. QZ gradually increases or decreases your speed (or bike resistance) in small increments every 40 seconds to reach and maintain your target HR zone. During a workout, you can display and use the ‘+’ and ‘-’ button on the PID HR Zone tile to change the target HR zone.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3817,7 +4534,7 @@ import Qt.labs.settings 1.0 Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.treadmill_pid_heart_min = treadmillPidHRminTextField.text } + onClicked: { settings.treadmill_pid_heart_min = treadmillPidHRminTextField.text ; toast.show("Setting saved!"); } } } @@ -3839,15 +4556,15 @@ import Qt.labs.settings 1.0 Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.treadmill_pid_heart_max = treadmillPidHRmaxTextField.text } + onClicked: { settings.treadmill_pid_heart_max = treadmillPidHRmaxTextField.text ; toast.show("Setting saved!"); } } } Label { - text: qsTr("Alternatevely to 'PID on Heart Zone' setting you can use this couple of settings in order to specify a HR range.") + text: qsTr("Alternatively to 'PID on Heart Zone' setting you can use this couple of settings in order to specify a HR range.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3876,7 +4593,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramPace1Mile text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.pacef_1mile = (((parseInt(trainProgramPace1mileTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPace1mileTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPace1mileTextField.text.split(":")[2]))) / 1.60934;} + onClicked: { settings.pacef_1mile = (((parseInt(trainProgramPace1mileTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPace1mileTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPace1mileTextField.text.split(":")[2]))) / 1.60934; toast.show("Setting saved!"); } } } @@ -3884,7 +4601,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enter your 1 mile time goal, click OK. This setting will be used when you’re following a training program with the speed control. These settings should also match the Zwift app settings. More info: https://github.com/cagnulein/qdomyos-zwift/issues/609.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3913,7 +4630,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramPace5km text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.pacef_5km = (((parseInt(trainProgramPace5kmTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPace5kmTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPace5kmTextField.text.split(":")[2]))) / 5;} + onClicked: { settings.pacef_5km = (((parseInt(trainProgramPace5kmTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPace5kmTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPace5kmTextField.text.split(":")[2]))) / 5; toast.show("Setting saved!"); } } } @@ -3921,7 +4638,7 @@ import Qt.labs.settings 1.0 text: qsTr("See 1 Mile Pace above; same except 5 km instead of 1 mile.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3950,7 +4667,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramPace10KM text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.pacef_10km = (((parseInt(trainProgramPace10kmTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPace10kmTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPace10kmTextField.text.split(":")[2]))) / 10;} + onClicked: { settings.pacef_10km = (((parseInt(trainProgramPace10kmTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPace10kmTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPace10kmTextField.text.split(":")[2]))) / 10; toast.show("Setting saved!"); } } } @@ -3958,7 +4675,7 @@ import Qt.labs.settings 1.0 text: qsTr("See 1 Mile Pace above; same except 10 km instead of 1 mile.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3987,7 +4704,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramPaceHalfMarathon text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.pacef_halfmarathon = (((parseInt(trainProgramPaceHalfMarathonTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPaceHalfMarathonTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPaceHalfMarathonTextField.text.split(":")[2]))) / 21;} + onClicked: { settings.pacef_halfmarathon = (((parseInt(trainProgramPaceHalfMarathonTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPaceHalfMarathonTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPaceHalfMarathonTextField.text.split(":")[2]))) / 21; toast.show("Setting saved!"); } } } @@ -3995,7 +4712,7 @@ import Qt.labs.settings 1.0 text: qsTr("See 1 Mile Pace above; same except half marathon distance instead of 1 mile.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4024,7 +4741,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramPaceMarathon text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: { settings.pacef_marathon = (((parseInt(trainProgramPaceMarathonTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPaceMarathonTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPaceMarathonTextField.text.split(":")[2]))) / 42;} + onClicked: { settings.pacef_marathon = (((parseInt(trainProgramPaceMarathonTextField.text.split(":")[0]) * 3600) + (parseInt(trainProgramPaceMarathonTextField.text.split(":")[1]) * 60) + parseInt(trainProgramPaceMarathonTextField.text.split(":")[2]))) / 42; toast.show("Setting saved!"); } } } @@ -4032,7 +4749,7 @@ import Qt.labs.settings 1.0 text: qsTr("See 1 Mile Pace above; same except marathon distance instead of 1 mile.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4064,7 +4781,7 @@ import Qt.labs.settings 1.0 id: okTreadmillPaceDefault text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.pace_default = treadmillPaceDefaultTextField.displayText + onClicked: { settings.pace_default = treadmillPaceDefaultTextField.displayText; toast.show("Setting saved!"); } } } @@ -4072,7 +4789,7 @@ import Qt.labs.settings 1.0 text: qsTr("Select the default Pace to be used when the ZWO file does not indicate a precise pace.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4109,7 +4826,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomDuration text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_total = trainProgramRandomDurationTextField.text + onClicked: { settings.trainprogram_total = trainProgramRandomDurationTextField.text; toast.show("Setting saved!"); } } } @@ -4134,7 +4851,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomPeriod text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_period_seconds = trainProgramRandomPeriodTextField.text + onClicked: { settings.trainprogram_period_seconds = trainProgramRandomPeriodTextField.text; toast.show("Setting saved!"); } } } @@ -4159,7 +4876,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomSpeedMin text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_speed_min = trainProgramRandomSpeedMinTextField.text + onClicked: { settings.trainprogram_speed_min = trainProgramRandomSpeedMinTextField.text; toast.show("Setting saved!"); } } } @@ -4184,7 +4901,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomSpeedMax text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_speed_max = trainProgramRandomSpeedMaxTextField.text + onClicked: { settings.trainprogram_speed_max = trainProgramRandomSpeedMaxTextField.text; toast.show("Setting saved!"); } } } @@ -4209,7 +4926,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomInclineMin text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_incline_min = trainProgramRandomInclineMinTextField.text + onClicked: { settings.trainprogram_incline_min = trainProgramRandomInclineMinTextField.text; toast.show("Setting saved!"); } } } @@ -4234,7 +4951,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomInclineMax text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_incline_max = trainProgramRandomInclineMaxTextField.text + onClicked: { settings.trainprogram_incline_max = trainProgramRandomInclineMaxTextField.text; toast.show("Setting saved!"); } } } @@ -4259,7 +4976,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomResistanceMin text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_resistance_min = trainProgramRandomResistanceMinTextField.text + onClicked: { settings.trainprogram_resistance_min = trainProgramRandomResistanceMinTextField.text; toast.show("Setting saved!"); } } } @@ -4284,7 +5001,7 @@ import Qt.labs.settings 1.0 id: okTrainProgramRandomResistanceMax text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.trainprogram_resistance_max = trainProgramRandomResistanceMaxTextField.text + onClicked: { settings.trainprogram_resistance_max = trainProgramRandomResistanceMaxTextField.text; toast.show("Setting saved!"); } } } } @@ -4294,7 +5011,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn on and enter your choices for workout time (in minutes and seconds) and the maximum and minimum speed, incline (treadmill), and resistance (bike) and QZ will randomly change your speed and resistance or incline accordingly for the period of time you have selected.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4325,7 +5042,7 @@ import Qt.labs.settings 1.0 checked: settings.virtual_device_force_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.virtual_device_force_bike = checked + onClicked: { settings.virtual_device_force_bike = checked; window.settings_restart_to_apply = true; } } } @@ -4333,7 +5050,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn on to convert your treadmill output to bike output when riding on Zwift. QZ sends your treadmill metrics to Zwift over Bluetooth so that you can participate as a bike rider. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4361,7 +5078,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn this on to have QZ control the speed of your treadmill during, for example, Peloton classes based on the coach’s speed callouts. Your speed will be in the low, upper or average range based on your Peloton Options > Difficulty setting. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4389,7 +5106,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn this on to have QZ go into Pause mode upon opening when using a treadmill. This is for treadmills only. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4417,7 +5134,7 @@ import Qt.labs.settings 1.0 text: qsTr("Target Speed and Target Incline tile offer a way to increase/decrease the current difficulty with the plus/minus buttons. By default, with this setting disabled, the speed and the inclination change with a 3% gain for every pressure. Switching this ON, QZ will add a 0.1 speed offset or a 0.5 incline offset instead.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4439,7 +5156,7 @@ import Qt.labs.settings 1.0 horizontalAlignment: Text.AlignRight Layout.fillHeight: false Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - inputMethodHints: Qt.ImhDigitsOnly + //inputMethodHints: Qt.ImhDigitsOnly onAccepted: settings.treadmill_step_speed = text onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length } @@ -4447,7 +5164,7 @@ import Qt.labs.settings 1.0 id: okTreadmillSpeedStepButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_step_speed = (settings.miles_unit?treadmillSpeedStepTextField.text * 1.60934:treadmillSpeedStepTextField.text) + onClicked: { settings.treadmill_step_speed = (settings.miles_unit?treadmillSpeedStepTextField.text * 1.60934:treadmillSpeedStepTextField.text); toast.show("Setting saved!"); } } } @@ -4455,7 +5172,7 @@ import Qt.labs.settings 1.0 text: qsTr("(Speed Tile) This controls the amount of the increase or decrease in the speed (in kph/mph) when you press the plus or minus button in the Speed Tile. Default is 0.5 kph.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4477,7 +5194,7 @@ import Qt.labs.settings 1.0 horizontalAlignment: Text.AlignRight Layout.fillHeight: false Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - inputMethodHints: Qt.ImhDigitsOnly + //inputMethodHints: Qt.ImhDigitsOnly onAccepted: settings.treadmill_step_incline = text onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length } @@ -4485,7 +5202,7 @@ import Qt.labs.settings 1.0 id: okTreadmillInclinationStepButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.treadmill_step_incline = treadmillInclinationStepTextField.text + onClicked: { settings.treadmill_step_incline = treadmillInclinationStepTextField.text; toast.show("Setting saved!"); } } } @@ -4493,7 +5210,7 @@ import Qt.labs.settings 1.0 text: qsTr("(Incline Tile) This controls the amount of the increase or decrease in the inclination when you press the plus or minus button in the Incline Tile. Default is 0.5.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4514,7 +5231,73 @@ import Qt.labs.settings 1.0 text: qsTr("Overrides the default inclination values sent from the treadmill") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + SwitchDelegate { + text: qsTr("Simulate Inclinatin with Speed") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.treadmill_simulate_inclination_with_speed + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.treadmill_simulate_inclination_with_speed = checked + } + + Label { + text: qsTr("For treadmills without inclination: turning this on and QZ will transform inclination requests into speed changes.") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + Label { + text: qsTr("FTMS Treadmill:") + Layout.fillWidth: true + } + RowLayout { + spacing: 10 + ComboBox { + id: ftmsTreadmillTextField + model: rootItem.bluetoothDevices + displayText: settings.ftms_treadmill + Layout.fillHeight: false + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActivated: { + console.log("combomodel activated" + ftmsTreadmillTextField.currentIndex) + displayText = ftmsTreadmillTextField.currentValue + } + + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.ftms_treadmill = ftmsTreadmillTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("If you have a generic FTMS bike and the tiles doesn't appear on the main QZ screen, select here the bluetooth name of your bike.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4527,7 +5310,7 @@ import Qt.labs.settings 1.0 text: qsTr("Expand the bars to the right to display the options under this setting. Select your specific model (if it is listed) and leave all other settings on default. If you encounter problems or have a question about settings for your specific equipment with QZ, click here to open a support ticket on GitHub or ask the QZ community on the QZ Facebook Group.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4556,7 +5339,7 @@ import Qt.labs.settings 1.0 checked: settings.norditrack_s25i_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.norditrack_s25i_treadmill = checked + onClicked: { settings.norditrack_s25i_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { text: qsTr("Nordictrack Incline Trainer x7i") @@ -4569,7 +5352,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_incline_trainer_x7i Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_incline_trainer_x7i = checked + onClicked: { settings.nordictrack_incline_trainer_x7i = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: nordictrack10Delegate @@ -4583,8 +5366,21 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_10_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_10_treadmill = checked + onClicked: {settings.nordictrack_10_treadmill = checked; window.settings_restart_to_apply = true; } } + SwitchDelegate { + text: qsTr("Proform Pro 1000") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_pro_1000_treadmill + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: {settings.proform_pro_1000_treadmill = checked; window.settings_restart_to_apply = true; } + } SwitchDelegate { id: nordictrackT65SDelegate text: qsTr("Nordictrack T6.5S v81") @@ -4597,7 +5393,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_t65s_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_t65s_treadmill = checked + onClicked: { settings.nordictrack_t65s_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -4612,7 +5408,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_t65s_83_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_t65s_83_treadmill = checked + onClicked: { settings.nordictrack_t65s_83_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -4627,7 +5423,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_t70_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_t70_treadmill = checked + onClicked: { settings.nordictrack_t70_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: nordictrackS30Delegate @@ -4641,7 +5437,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_s30_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_s30_treadmill = checked + onClicked: { settings.nordictrack_s30_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proform1800iDelegate @@ -4655,7 +5451,20 @@ import Qt.labs.settings 1.0 checked: settings.proform_treadmill_1800i Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_treadmill_1800i = checked + onClicked: { settings.proform_treadmill_1800i = checked; window.settings_restart_to_apply = true; } + } + SwitchDelegate { + text: qsTr("Proform z1300i") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_treadmill_z1300i + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_treadmill_z1300i = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proformSEDelegate @@ -4669,7 +5478,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_treadmill_se Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_treadmill_se = checked + onClicked: { settings.proform_treadmill_se = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proformCadenceLT @@ -4683,7 +5492,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_treadmill_cadence_lt Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_treadmill_cadence_lt = checked + onClicked: { settings.proform_treadmill_cadence_lt = checked; window.settings_restart_to_apply = true; } } RowLayout { spacing: 10 @@ -4706,7 +5515,7 @@ import Qt.labs.settings 1.0 id: okproformtreadmillIPButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.proformtreadmillip = proformtreadmillIPTextField.text + onClicked: { settings.proformtreadmillip = proformtreadmillIPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -4730,7 +5539,7 @@ import Qt.labs.settings 1.0 id: oknordictrack2950IPButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.nordictrack_2950_ip = nordictrack2950IPTextField.text + onClicked: { settings.nordictrack_2950_ip = nordictrack2950IPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -4745,8 +5554,23 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_ifit_adb_remote Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_ifit_adb_remote = checked + onClicked: { settings.nordictrack_ifit_adb_remote = checked; window.settings_restart_to_apply = true; } + } + + SwitchDelegate { + text: qsTr("Proform 8.0") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_treadmill_8_0 + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_treadmill_8_0 = checked; window.settings_restart_to_apply = true; } } + SwitchDelegate { id: proform90IDelegate text: qsTr("Proform 9.0") @@ -4759,7 +5583,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_treadmill_9_0 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_treadmill_9_0 = checked + onClicked: { settings.proform_treadmill_9_0 = checked; window.settings_restart_to_apply = true; } } /* SwitchDelegate { @@ -4814,7 +5638,7 @@ import Qt.labs.settings 1.0 checked: settings.pafers_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.pafers_treadmill = checked + onClicked: { settings.pafers_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: bhIboxsterPlusDelegate @@ -4828,7 +5652,53 @@ import Qt.labs.settings 1.0 checked: settings.pafers_treadmill_bh_iboxster_plus Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.pafers_treadmill_bh_iboxster_plus = checked + onClicked: { settings.pafers_treadmill_bh_iboxster_plus = checked; window.settings_restart_to_apply = true; } + } + } + } + + AccordionElement { + title: qsTr("GEM Module Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 0 + SwitchDelegate { + text: qsTr("Inclination") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.gem_module_inclination + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.gem_module_inclination = checked; window.settings_restart_to_apply = true; } + } + } + } + + AccordionElement { + title: qsTr("Echelon Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 0 + SwitchDelegate { + text: qsTr("Miles unit from the device") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.sole_treadmill_miles + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.sole_treadmill_miles = checked } } } @@ -4853,7 +5723,7 @@ import Qt.labs.settings 1.0 checked: settings.kingsmith_encrypt_v2 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v2 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v4 = false; } + onClicked: { settings.kingsmith_encrypt_v2 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v4 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -4868,7 +5738,7 @@ import Qt.labs.settings 1.0 checked: settings.kingsmith_encrypt_v3 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v3 = checked; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; } + onClicked: { settings.kingsmith_encrypt_v3 = checked; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -4883,7 +5753,21 @@ import Qt.labs.settings 1.0 checked: settings.kingsmith_encrypt_v4 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: { settings.kingsmith_encrypt_v4 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; } + onClicked: { settings.kingsmith_encrypt_v4 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v5 = false; window.settings_restart_to_apply = true; } + } + + SwitchDelegate { + text: qsTr("WalkingPad X21 v4") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.kingsmith_encrypt_v5 + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.kingsmith_encrypt_v5 = checked; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; window.settings_restart_to_apply = true; } } } } @@ -4908,7 +5792,20 @@ import Qt.labs.settings 1.0 checked: settings.fitfiu_mc_v460 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fitfiu_mc_v460 = checked + onClicked: { settings.fitfiu_mc_v460 = checked; window.settings_restart_to_apply = true; } + } + SwitchDelegate { + text: qsTr("Zero ZT-2500") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.zero_zt2500_treadmill + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.zero_zt2500_treadmill = checked; window.settings_restart_to_apply = true; } } } } @@ -4965,6 +5862,40 @@ import Qt.labs.settings 1.0 Layout.fillWidth: true onClicked: settings.domyos_treadmill_display_invert = checked } + RowLayout { + spacing: 10 + Label { + text: qsTr("Pool time (ms):") + Layout.fillWidth: true + } + TextField { + id: pollDeviceTimeTextField + text: settings.poll_device_time + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhDigitsOnly + onAccepted: settings.poll_device_time = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.poll_device_time = pollDeviceTimeTextField.text; toast.show("Setting saved!"); window.settings_restart_to_apply = true;} + } + } + Label { + text: qsTr("Default: 200. Change this only if you have random issues with speed or inclination (try to put 300)") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } } } @@ -4988,7 +5919,20 @@ import Qt.labs.settings 1.0 checked: settings.sole_treadmill_inclination Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sole_treadmill_inclination = checked + onClicked: { settings.sole_treadmill_inclination = checked; window.settings_restart_to_apply = true; } + } + SwitchDelegate { + text: qsTr("Fast Inclination (experimental)") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.sole_treadmill_inclination_fast + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.sole_treadmill_inclination_fast = checked } SwitchDelegate { id: soleMilesDelegate @@ -5030,7 +5974,7 @@ import Qt.labs.settings 1.0 checked: settings.sole_treadmill_f65 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sole_treadmill_f65 = checked + onClicked: { settings.sole_treadmill_f65 = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: soleTT8Delegate @@ -5044,7 +5988,7 @@ import Qt.labs.settings 1.0 checked: settings.sole_treadmill_tt8 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sole_treadmill_tt8 = checked + onClicked: { settings.sole_treadmill_tt8 = checked; window.settings_restart_to_apply = true; } } } } @@ -5069,7 +6013,7 @@ import Qt.labs.settings 1.0 checked: settings.technogym_myrun_treadmill_experimental Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.technogym_myrun_treadmill_experimental = checked + onClicked: { settings.technogym_myrun_treadmill_experimental = checked; window.settings_restart_to_apply = true; } } } } @@ -5093,7 +6037,7 @@ import Qt.labs.settings 1.0 checked: settings.fitshow_anyrun Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fitshow_anyrun = checked + onClicked: { settings.fitshow_anyrun = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: fitshowTruetimerDelegate @@ -5144,7 +6088,7 @@ import Qt.labs.settings 1.0 id: okfitshowTreadmillUserIdButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitshow_user_id = fitshowTreadmillUserIdTextField.text + onClicked: { settings.fitshow_user_id = fitshowTreadmillUserIdTextField.text; toast.show("Setting saved!"); } } } } @@ -5169,7 +6113,7 @@ import Qt.labs.settings 1.0 checked: settings.eslinker_cadenza Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.eslinker_cadenza = checked + onClicked: { settings.eslinker_cadenza = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: eslinkerTreadmillYpooDelegate @@ -5183,8 +6127,21 @@ import Qt.labs.settings 1.0 checked: settings.eslinker_ypoo Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.eslinker_ypoo = checked + onClicked: { settings.eslinker_ypoo = checked; window.settings_restart_to_apply = true; } } + SwitchDelegate { + text: qsTr("Costaway Folding") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.eslinker_costaway + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.eslinker_costaway = checked; window.settings_restart_to_apply = true; } + } } } @@ -5208,7 +6165,7 @@ import Qt.labs.settings 1.0 checked: settings.horizon_paragon_x Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.horizon_paragon_x = checked + onClicked: { settings.horizon_paragon_x = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: horizonFTMSTreadmillCadenzaDelegate @@ -5222,7 +6179,7 @@ import Qt.labs.settings 1.0 checked: settings.horizon_treadmill_force_ftms Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.horizon_treadmill_force_ftms = checked + onClicked: { settings.horizon_treadmill_force_ftms = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: horizon78TreadmillDelegate @@ -5236,7 +6193,7 @@ import Qt.labs.settings 1.0 checked: settings.horizon_treadmill_7_8 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.horizon_treadmill_7_8 = checked + onClicked: { settings.horizon_treadmill_7_8 = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5289,7 +6246,7 @@ import Qt.labs.settings 1.0 id: okhorizonTreadmillProfile1Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.horizon_treadmill_profile_user1 = horizonTreadmillProfile1TextField.text + onClicked: { settings.horizon_treadmill_profile_user1 = horizonTreadmillProfile1TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -5312,7 +6269,7 @@ import Qt.labs.settings 1.0 id: okhorizonTreadmillProfile2Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.horizon_treadmill_profile_user2 = horizonTreadmillProfile2TextField.text + onClicked: { settings.horizon_treadmill_profile_user2 = horizonTreadmillProfile2TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -5335,7 +6292,7 @@ import Qt.labs.settings 1.0 id: okhorizonTreadmillProfile3Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.horizon_treadmill_profile_user3 = horizonTreadmillProfile3TextField.text + onClicked: { settings.horizon_treadmill_profile_user3 = horizonTreadmillProfile3TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -5358,7 +6315,7 @@ import Qt.labs.settings 1.0 id: okhorizonTreadmillProfile4Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.horizon_treadmill_profile_user4 = horizonTreadmillProfile4TextField.text + onClicked: { settings.horizon_treadmill_profile_user4 = horizonTreadmillProfile4TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -5381,12 +6338,34 @@ import Qt.labs.settings 1.0 id: okhorizonTreadmillProfile5Button text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.horizon_treadmill_profile_user5 = horizonTreadmillProfile5TextField.text + onClicked: { settings.horizon_treadmill_profile_user5 = horizonTreadmillProfile5TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } } } - + + AccordionElement { + title: qsTr("Bodytone Treadmill Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 0 + SwitchDelegate { + text: qsTr("Force Using FTMS") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.horizon_treadmill_force_ftms + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.horizon_treadmill_force_ftms = checked; window.settings_restart_to_apply = true; } + } + } + } } AccordionElement { @@ -5409,7 +6388,7 @@ import Qt.labs.settings 1.0 checked: settings.trx_route_key Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.trx_route_key = checked + onClicked: { settings.trx_route_key = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: trxsevoDelegate @@ -5423,7 +6402,7 @@ import Qt.labs.settings 1.0 checked: settings.toorx_65s_evo Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.toorx_65s_evo = checked + onClicked: { settings.toorx_65s_evo = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5438,7 +6417,21 @@ import Qt.labs.settings 1.0 checked: settings.bh_spada_2 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.bh_spada_2 = checked + onClicked: { settings.bh_spada_2 = checked; window.settings_restart_to_apply = true; } + } + + SwitchDelegate { + text: qsTr("BH SPADA Wattage") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.bh_spada_2_watt + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.bh_spada_2_watt = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5453,7 +6446,7 @@ import Qt.labs.settings 1.0 checked: settings.jtx_fitness_sprint_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.jtx_fitness_sprint_treadmill = checked + onClicked: { settings.jtx_fitness_sprint_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5468,7 +6461,7 @@ import Qt.labs.settings 1.0 checked: settings.reebok_fr30_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.reebok_fr30_treadmill = checked + onClicked: { settings.reebok_fr30_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5483,7 +6476,7 @@ import Qt.labs.settings 1.0 checked: settings.dkn_endurun_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.dkn_endurun_treadmill = checked + onClicked: { settings.dkn_endurun_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5498,7 +6491,7 @@ import Qt.labs.settings 1.0 checked: settings.toorx_3_0 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.toorx_3_0 = checked + onClicked: { settings.toorx_3_0 = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5513,7 +6506,7 @@ import Qt.labs.settings 1.0 checked: settings.toorx_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.toorx_bike = checked + onClicked: { settings.toorx_bike = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5528,7 +6521,7 @@ import Qt.labs.settings 1.0 checked: settings.toorx_ftms_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.toorx_ftms_treadmill = checked + onClicked: { settings.toorx_ftms_treadmill = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5543,7 +6536,7 @@ import Qt.labs.settings 1.0 checked: settings.toorx_ftms Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.toorx_ftms = checked + onClicked: { settings.toorx_ftms = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { @@ -5558,7 +6551,7 @@ import Qt.labs.settings 1.0 checked: settings.jll_IC400_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.jll_IC400_bike = checked + onClicked: { settings.jll_IC400_bike = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: toorxBikeFytterRI08Delegate @@ -5572,7 +6565,7 @@ import Qt.labs.settings 1.0 checked: settings.fytter_ri08_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fytter_ri08_bike = checked + onClicked: { settings.fytter_ri08_bike = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: toorxBikeASVIVADelegate @@ -5586,7 +6579,7 @@ import Qt.labs.settings 1.0 checked: settings.asviva_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.asviva_bike = checked + onClicked: { settings.asviva_bike = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: toorxBikeHertzXR770Delegate @@ -5600,7 +6593,111 @@ import Qt.labs.settings 1.0 checked: settings.hertz_xr_770 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.hertz_xr_770 = checked + onClicked: { settings.hertz_xr_770 = checked; window.settings_restart_to_apply = true; } + } + } + } + + AccordionElement { + title: qsTr("Rower Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Grey) + color: Material.backgroundColor + accordionContent: ColumnLayout { + spacing: 0 + AccordionElement { + title: qsTr("PM3, PM4 Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + RowLayout { + spacing: 10 + Label { + text: qsTr("Serial Port:") + Layout.fillWidth: true + } + TextField { + id: csaferowerSerialPortTextField + text: settings.csafe_rower + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + //inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.csafe_rower = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.csafe_rower = csaferowerSerialPortTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + } + + Label { + text: qsTr("FTMS Rower:") + Layout.fillWidth: true + } + RowLayout { + spacing: 10 + ComboBox { + id: ftmsRowerTextField + model: rootItem.bluetoothDevices + displayText: settings.ftms_rower + Layout.fillHeight: false + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActivated: { + console.log("combomodel activated" + ftmsRowerTextField.currentIndex) + displayText = ftmsRowerTextField.currentValue + } + + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.ftms_rower = ftmsRowerTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + + Button { + text: "Refresh Devices List" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: refresh_bluetooth_devices_clicked(); + } + + Label { + text: qsTr("Allows you to force QZ to connect to your FTMS Rower. If you are in doubt, leave this Disabled and send an email to the QZ support. Default is “Disabled.”") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + AccordionElement { + title: qsTr("Proform/Nordictrack Rower Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: + SwitchDelegate { + text: qsTr("Proform Sport RL") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.proform_rower_sport_rl + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.proform_rower_sport_rl = checked; window.settings_restart_to_apply = true; } + } } } } @@ -5641,7 +6738,7 @@ import Qt.labs.settings 1.0 id: okDomyosEllipticalRatioButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.domyos_elliptical_speed_ratio = domyosEllipticalSpeedRatioTextField.text + onClicked: { settings.domyos_elliptical_speed_ratio = domyosEllipticalSpeedRatioTextField.text; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -5678,7 +6775,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_hybrid_trainer_xt Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_hybrid_trainer_xt = checked + onClicked: { settings.proform_hybrid_trainer_xt = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: proformHybridPFEL03815Delegate @@ -5692,7 +6789,7 @@ import Qt.labs.settings 1.0 checked: settings.proform_hybrid_trainer_PFEL03815 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.proform_hybrid_trainer_PFEL03815 = checked + onClicked: { settings.proform_hybrid_trainer_PFEL03815 = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { text: qsTr("Nordictrack C7.5") @@ -5705,7 +6802,7 @@ import Qt.labs.settings 1.0 checked: settings.nordictrack_elliptical_c7_5 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.nordictrack_elliptical_c7_5 = checked + onClicked: { settings.nordictrack_elliptical_c7_5 = checked; window.settings_restart_to_apply = true; } } } @@ -5728,7 +6825,7 @@ import Qt.labs.settings 1.0 checked: settings.sole_elliptical_inclination Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sole_elliptical_inclination = checked + onClicked: { settings.sole_elliptical_inclination = checked; window.settings_restart_to_apply = true; } } SwitchDelegate { id: soleEllipticalE55Delegate @@ -5742,7 +6839,28 @@ import Qt.labs.settings 1.0 checked: settings.sole_elliptical_e55 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.sole_elliptical_e55 = checked + onClicked: { settings.sole_elliptical_e55 = checked; window.settings_restart_to_apply = true; } + } + } + + AccordionElement { + title: qsTr("iConcept Elliptical Options") + indicatRectColor: Material.color(Material.Grey) + textColor: Material.color(Material.Yellow) + color: Material.backgroundColor + accordionContent: + SwitchDelegate { + text: qsTr("iConcept elliptical") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.iconcept_elliptical + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.iconcept_elliptical = checked; window.settings_restart_to_apply = true; } } } } @@ -5783,7 +6901,7 @@ import Qt.labs.settings 1.0 id: okFilterDeviceButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.filter_device = filterDeviceTextField.displayText + onClicked: { settings.filter_device = filterDeviceTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -5798,7 +6916,7 @@ import Qt.labs.settings 1.0 text: qsTr("Allows you to force QZ to connect to your equipment (see “Bluetooth Troubleshooting” below). Default is “Disabled.”") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5811,7 +6929,7 @@ import Qt.labs.settings 1.0 spacing: 10 Label { id: labelwattOffset - text: qsTr("Watt Offset (only <0):") + text: qsTr("Watt Offset:") Layout.fillWidth: true } TextField { @@ -5828,7 +6946,7 @@ import Qt.labs.settings 1.0 id: okwattOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.watt_offset = wattOffsetTextField.text + onClicked: { settings.watt_offset = wattOffsetTextField.text; toast.show("Setting saved!"); } } } @@ -5836,7 +6954,7 @@ import Qt.labs.settings 1.0 text: qsTr("You can increase/decrease your watt output for moving your avatar faster/slower in Zwift or other similar apps as a way of calibrating your equipment. The number you enter as an Offset adds that amount to your watts.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5866,7 +6984,7 @@ import Qt.labs.settings 1.0 id: okWattGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.watt_gain = wattGainTextField.text + onClicked: { settings.watt_gain = wattGainTextField.text; toast.show("Setting saved!"); } } } @@ -5874,7 +6992,7 @@ import Qt.labs.settings 1.0 text: qsTr("You can increase/decrease your watt output for moving your avatar faster/slower in Zwift or other similar apps as a way of calibrating your equipment. For example, to use a rower to cycle in Zwift, you could double your watt output to better match your cycling speed by entering 2. The number you enter is a multiplier applied to your actual watts.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5904,7 +7022,7 @@ import Qt.labs.settings 1.0 id: okspeedOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.speed_offset = speedOffsetTextField.text + onClicked: { settings.speed_offset = speedOffsetTextField.text; toast.show("Setting saved!"); } } } @@ -5912,7 +7030,7 @@ import Qt.labs.settings 1.0 text: qsTr("You can increase/decrease your speed for moving your avatar faster/slower in Zwift if your equipment outputs speed but not watts. The number you enter as an Offset adds that amount to your speed.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5943,7 +7061,7 @@ import Qt.labs.settings 1.0 id: okSpeedGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.speed_gain = speedGainTextField.text + onClicked: { settings.speed_gain = speedGainTextField.text; toast.show("Setting saved!"); } } } @@ -5951,7 +7069,7 @@ import Qt.labs.settings 1.0 text: qsTr("You can increase/decrease your speed output for moving your avatar faster/slower in Zwift or other apps as a way of calibrating your equipment if your equipment outputs speed but not watts. For example, to use a rower to cycle in Zwift, you could double your speed output to better match your cycling speed. The number you enter is a multiplier applied to your actual speed.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5981,7 +7099,7 @@ import Qt.labs.settings 1.0 id: okcadenceOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.cadence_offset = cadenceOffsetTextField.text + onClicked: { settings.cadence_offset = cadenceOffsetTextField.text; toast.show("Setting saved!"); } } } @@ -5989,7 +7107,7 @@ import Qt.labs.settings 1.0 text: qsTr("You can increase/decrease your cadence output. The number you enter as an Offset adds that amount to your cadence.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6019,7 +7137,7 @@ import Qt.labs.settings 1.0 id: okCadenceGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.cadence_gain = speedGainTextField.text + onClicked: { settings.cadence_gain = speedGainTextField.text; toast.show("Setting saved!"); } } } @@ -6027,7 +7145,7 @@ import Qt.labs.settings 1.0 text: qsTr("You can increase/decrease your cadence output as a way of calibrating your equipment if your equipment outputs cadence but not watts. The number you enter is a multiplier applied to your actual cadence.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6064,7 +7182,7 @@ import Qt.labs.settings 1.0 id: okStravaSuffixButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.strava_suffix = stravaSuffixTextField.text + onClicked: { settings.strava_suffix = stravaSuffixTextField.text; toast.show("Setting saved!"); } } } @@ -6072,7 +7190,7 @@ import Qt.labs.settings 1.0 text: qsTr("Default is “QZ.” Please leave this set to default so that other Strava users will see the QZ; a tiny bit of advertising that helps promote the app and support its development. If you choose to remove it, please consider contributing to the developer’s Patreon or Buy Me a Coffee accounts or just subscribe to the Swag bag in the left side bar to allow me to continue developing and supporting the app.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6092,14 +7210,14 @@ import Qt.labs.settings 1.0 checked: settings.strava_auth_external_webbrowser Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.strava_auth_external_webbrowser = checked + onClicked: { settings.strava_auth_external_webbrowser = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("QZ can open a external browser in order to auth strava to QZ. Default: disabled.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6127,7 +7245,34 @@ import Qt.labs.settings 1.0 text: qsTr("Append the Virtual Tag to the Strava Activity") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + SwitchDelegate { + text: qsTr("Date Prefix on Strava Workout") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.strava_date_prefix + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.strava_date_prefix = checked + } + + Label { + text: qsTr("Append the Date to the Strava Activity as a prefix only for non-peloton workout") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6148,14 +7293,14 @@ import Qt.labs.settings 1.0 checked: settings.volume_change_gears Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.volume_change_gears = checked + onClicked: { settings.volume_change_gears = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Allows you to change resistance during auto-follow mode using the volume buttons of the device running QZ, Bluetooth headphones or a Bluetooth remote. Changes made using these external controls will be visible in the Gears tile. This is a VERY USEFUL feature! Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6183,7 +7328,7 @@ import Qt.labs.settings 1.0 text: qsTr("If the power output/watts your equipment sends to QZ is quite variable, this setting will result in smoother Power Zone graphs. This is also helpful for use with Power Meter Pedals. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6211,7 +7356,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enables the calculation of watts, even while in Pause mode. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6239,7 +7384,7 @@ import Qt.labs.settings 1.0 text: qsTr("Turn this on if you have a bike with inclination capabilities to fix Zwift’s bug that sends half-negative downhill inclination") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6269,7 +7414,7 @@ import Qt.labs.settings 1.0 id: okTreadmillInclinationOffsetButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.zwift_inclination_offset = treadmillInclinationOffsetTextField.text + onClicked: { settings.zwift_inclination_offset = treadmillInclinationOffsetTextField.text; toast.show("Setting saved!"); } } } @@ -6277,7 +7422,7 @@ import Qt.labs.settings 1.0 text: qsTr("Inclination Offset and Gain are used to adjust the incline set by Zwift instead of, or in addition to, using the QZ Zwift Gain setting. For example, when Zwift changes the incline to 1%, you can have your treadmill change to 2%. The number you enter as an offset adds to the inclination sent from Zwift or any other 3rd party app. Default is 0.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6307,7 +7452,7 @@ import Qt.labs.settings 1.0 id: okTreadmillInclinationGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.zwift_inclination_gain = treadmillInclinationGainTextField.text + onClicked: { settings.zwift_inclination_gain = treadmillInclinationGainTextField.text; toast.show("Setting saved!"); } } } @@ -6315,7 +7460,34 @@ import Qt.labs.settings 1.0 text: qsTr("The number you enter as a Gain is a multiplier applied to the inclination sent from Zwift or any other 3rd party app. Default is 1.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + SwitchDelegate { + text: qsTr("Disable Wattage from Machinery") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.watt_ignore_builtin + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: settings.watt_ignore_builtin = checked + } + + Label { + text: qsTr("This prevents your fitness device from sending its wattage calculation to QZ and defaults to QZ’s more accurate calculation.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6368,14 +7540,14 @@ import Qt.labs.settings 1.0 checked: settings.cadence_sensor_as_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.cadence_sensor_as_bike = checked + onClicked: { settings.cadence_sensor_as_bike = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("If your bike doesn’t have Bluetooth, this setting allows you to use a cadence sensor so your bike will work with QZ. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6408,7 +7580,7 @@ import Qt.labs.settings 1.0 id: okCadenceSensorNameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.cadence_sensor_name = cadenceSensorNameTextField.displayText; + onClicked: { settings.cadence_sensor_name = cadenceSensorNameTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -6423,7 +7595,7 @@ import Qt.labs.settings 1.0 text: qsTr("Use this setting to connect QZ to your cadence sensor. Default is Disabled.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6453,7 +7625,7 @@ import Qt.labs.settings 1.0 id: okCadenceSpeedRatio text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.cadence_sensor_speed_ratio = cadenceSpeedRatioTextField.text + onClicked: { settings.cadence_sensor_speed_ratio = cadenceSpeedRatioTextField.text; toast.show("Setting saved!"); } } } @@ -6461,7 +7633,7 @@ import Qt.labs.settings 1.0 text: qsTr("Wheel ratio is the multiplier used by QZ to calculate your speed based on your cadence. For example, if you enter 1 for your wheel ratio and you are riding at a cadence of 30, QZ will display your speed as 30 km/h. The default of 0.33 is correct for most bikes.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6492,14 +7664,14 @@ import Qt.labs.settings 1.0 checked: settings.power_sensor_as_bike Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.power_sensor_as_bike = checked + onClicked: { settings.power_sensor_as_bike = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("If your bike doesn’t have Bluetooth, this setting allows you to use a power meter pedal sensor so your bike will work with QZ. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6520,14 +7692,14 @@ import Qt.labs.settings 1.0 checked: settings.power_sensor_as_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.power_sensor_as_treadmill = checked + onClicked: { settings.power_sensor_as_treadmill = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("If your treadmill doesn’t have Bluetooth, this setting allows you to use a Stryde sensor (or similar) so your treadmill will work with QZ. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6555,7 +7727,7 @@ import Qt.labs.settings 1.0 text: qsTr("Some power sensors send cadence divided by 2. This setting will fix this behavior.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6583,7 +7755,7 @@ import Qt.labs.settings 1.0 text: qsTr("Divide the cadence sent to Strava by 2.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6616,7 +7788,7 @@ import Qt.labs.settings 1.0 id: okPowerSensorNameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.power_sensor_name = powerSensorNameTextField.displayText; + onClicked: { settings.power_sensor_name = powerSensorNameTextField.displayText;; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -6631,7 +7803,7 @@ import Qt.labs.settings 1.0 text: qsTr("Leave on Disabled or select from list of found Bluetooth devices.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -6682,7 +7854,7 @@ import Qt.labs.settings 1.0 id: okEliteRizerNameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.elite_rizer_name = eliteRizerNameTextField.displayText; + onClicked: { settings.elite_rizer_name = eliteRizerNameTextField.displayText;; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -6714,7 +7886,7 @@ import Qt.labs.settings 1.0 id: okEliteRizerGainButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.elite_rizer_gain = eliteRizerGainTextField.text + onClicked: { settings.elite_rizer_gain = eliteRizerGainTextField.text; toast.show("Setting saved!"); } } } } @@ -6750,7 +7922,7 @@ import Qt.labs.settings 1.0 id: okEliteSterzoSmartNameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.elite_sterzo_smart_name = eliteSterzoSmartNameTextField.displayText; + onClicked: { settings.elite_sterzo_smart_name = eliteSterzoSmartNameTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -6797,7 +7969,7 @@ import Qt.labs.settings 1.0 id: okFTMSAccessoryNameButton text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ftms_accessory_name = ftmsAccessoryNameTextField.displayText; + onClicked: { settings.ftms_accessory_name = ftmsAccessoryNameTextField.displayText; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } @@ -6820,7 +7992,7 @@ import Qt.labs.settings 1.0 checked: settings.ss2k_peloton Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.ss2k_peloton = checked + onClicked: { settings.ss2k_peloton = checked; window.settings_restart_to_apply = true; } } RowLayout { @@ -6844,7 +8016,7 @@ import Qt.labs.settings 1.0 id: okSS2kShiftStep text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_shift_step = ss2kShiftStepTextField.text + onClicked: { settings.ss2k_shift_step = ss2kShiftStepTextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -6868,7 +8040,7 @@ import Qt.labs.settings 1.0 id: okSS2kMaxResistance text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_max_resistance = ss2kMaxResistanceTextField.text + onClicked: { settings.ss2k_max_resistance = ss2kMaxResistanceTextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -6892,7 +8064,7 @@ import Qt.labs.settings 1.0 id: okSS2kMinResistance text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_min_resistance = ss2kMinResistanceTextField.text + onClicked: { settings.ss2k_min_resistance = ss2kMinResistanceTextField.text; toast.show("Setting saved!"); } } } @@ -6925,7 +8097,7 @@ import Qt.labs.settings 1.0 id: okSS2kResistanceSample1 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_resistance_sample_1 = ss2kResistanceSample1TextField.text + onClicked: { settings.ss2k_resistance_sample_1 = ss2kResistanceSample1TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -6948,7 +8120,7 @@ import Qt.labs.settings 1.0 id: okSS2kShiftStepSample1 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_shift_step_sample_1 = ss2kShiftStepSample1TextField.text + onClicked: { settings.ss2k_shift_step_sample_1 = ss2kShiftStepSample1TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -6972,7 +8144,7 @@ import Qt.labs.settings 1.0 id: okSS2kResistanceSample2 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_resistance_sample_2 = ss2kResistanceSample2TextField.text + onClicked: { settings.ss2k_resistance_sample_2 = ss2kResistanceSample2TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -6995,7 +8167,7 @@ import Qt.labs.settings 1.0 id: okSS2kShiftStepSample2 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_shift_step_sample_2 = ss2kShiftStepSample2TextField.text + onClicked: { settings.ss2k_shift_step_sample_2 = ss2kShiftStepSample2TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -7019,7 +8191,7 @@ import Qt.labs.settings 1.0 id: okSS2kResistanceSample3 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_resistance_sample_3 = ss2kResistanceSample3TextField.text + onClicked: { settings.ss2k_resistance_sample_3 = ss2kResistanceSample3TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -7042,7 +8214,7 @@ import Qt.labs.settings 1.0 id: okSS2kShiftStepSample3 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_shift_step_sample_3 = ss2kShiftStepSample3TextField.text + onClicked: { settings.ss2k_shift_step_sample_3 = ss2kShiftStepSample3TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -7066,7 +8238,7 @@ import Qt.labs.settings 1.0 id: okSS2kResistanceSample4 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_resistance_sample_4 = ss2kResistanceSample4TextField.text + onClicked: { settings.ss2k_resistance_sample_4 = ss2kResistanceSample4TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } RowLayout { @@ -7089,7 +8261,7 @@ import Qt.labs.settings 1.0 id: okSS2kShiftStepSample4 text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.ss2k_shift_step_sample_4 = ss2kShiftStepSample4TextField.text + onClicked: { settings.ss2k_shift_step_sample_4 = ss2kShiftStepSample4TextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } } } } @@ -7118,7 +8290,7 @@ import Qt.labs.settings 1.0 checked: settings.fitmetria_fanfit_enable Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fitmetria_fanfit_enable = checked + onClicked: { settings.fitmetria_fanfit_enable = checked; window.settings_restart_to_apply = true; } } RowLayout { @@ -7144,7 +8316,7 @@ import Qt.labs.settings 1.0 id: okFitmetriaFanFitModeTextField text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitmetria_fanfit_mode = fitmetriaFanFitModeTextField.displayText + onClicked: { settings.fitmetria_fanfit_mode = fitmetriaFanFitModeTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -7168,7 +8340,7 @@ import Qt.labs.settings 1.0 id: okFitmetriaFanFitMin text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitmetria_fanfit_min = fitmetriaFanFitMinTextField.text + onClicked: { settings.fitmetria_fanfit_min = fitmetriaFanFitMinTextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -7192,7 +8364,7 @@ import Qt.labs.settings 1.0 id: okFitmetriaFanFitMax text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitmetria_fanfit_max = fitmetriaFanFitMaxTextField.text + onClicked: { settings.fitmetria_fanfit_max = fitmetriaFanFitMaxTextField.text; toast.show("Setting saved!"); } } } } @@ -7217,7 +8389,7 @@ import Qt.labs.settings 1.0 checked: settings.fitmetria_fanfit_enable Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fitmetria_fanfit_enable = checked + onClicked: { settings.fitmetria_fanfit_enable = checked; window.settings_restart_to_apply = true; } } RowLayout { @@ -7241,7 +8413,7 @@ import Qt.labs.settings 1.0 Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitmetria_fanfit_mode = headWindModeTextField.displayText + onClicked: { settings.fitmetria_fanfit_mode = headWindModeTextField.displayText; toast.show("Setting saved!"); } } } RowLayout { @@ -7263,7 +8435,7 @@ import Qt.labs.settings 1.0 Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitmetria_fanfit_min = headWindMinTextField.text + onClicked: { settings.fitmetria_fanfit_min = headWindMinTextField.text; toast.show("Setting saved!"); } } } RowLayout { @@ -7285,7 +8457,7 @@ import Qt.labs.settings 1.0 Button { text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.fitmetria_fanfit_max = headWindMaxTextField.text + onClicked: { settings.fitmetria_fanfit_max = headWindMaxTextField.text; toast.show("Setting saved!"); } } } } @@ -7336,7 +8508,7 @@ import Qt.labs.settings 1.0 id: okMapsType text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.maps_type = mapsTypeTextField.displayText + onClicked: { settings.maps_type = mapsTypeTextField.displayText; toast.show("Setting saved!"); } } } SwitchDelegate { @@ -7418,14 +8590,14 @@ import Qt.labs.settings 1.0 checked: settings.bluetooth_relaxed Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.bluetooth_relaxed = checked + onClicked: { settings.bluetooth_relaxed = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Leave this setting off unless the Support staff asks you to turn it on during troubleshooting. Can improve the Android Bluetooth connection to Zwift. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7446,14 +8618,14 @@ import Qt.labs.settings 1.0 checked: settings.bluetooth_30m_hangs Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.bluetooth_30m_hangs = checked + onClicked: { settings.bluetooth_30m_hangs = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Same as “Relaxed Bluetooth for mad devices”. Leave off unless the Support staff asks you to turn it on. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7474,14 +8646,14 @@ import Qt.labs.settings 1.0 checked: settings.battery_service Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.battery_service = checked + onClicked: { settings.battery_service = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Leave this off unless the Support staff asks you to turn it on. Enables a new Bluetooth service, indicating the battery level of your device. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7529,14 +8701,14 @@ import Qt.labs.settings 1.0 checked: settings.virtual_device_onlyheart Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.virtual_device_onlyheart = checked + onClicked: { settings.virtual_device_onlyheart = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Forces QZ to communicate ONLY the Heart Rate metric to third-party apps. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7557,14 +8729,14 @@ import Qt.labs.settings 1.0 checked: settings.virtual_device_echelon Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.virtual_device_echelon = checked + onClicked: { settings.virtual_device_echelon = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Enables QZ to communicate with the Echelon app. This setting can only be used with iOS running QZ and iOS running the Echelon app. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7585,14 +8757,14 @@ import Qt.labs.settings 1.0 checked: settings.virtual_device_rower Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.virtual_device_rower = checked + onClicked: { settings.virtual_device_rower = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Enables QZ to send a rower Bluetooth profile instead of a bike profile to third party apps that support rowing (examples: Kinomap and BitGym). This should be off for Zwift. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7620,7 +8792,7 @@ import Qt.labs.settings 1.0 text: qsTr("Enables third-party apps to change the resistance of your equipment. Default is on.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7642,14 +8814,14 @@ import Qt.labs.settings 1.0 checked: settings.bike_power_sensor Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.bike_power_sensor = checked + onClicked: { settings.bike_power_sensor = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("This changes the virtual Bluetooth bridge from the standard FMTS to the Power Sensor interface. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7670,14 +8842,14 @@ import Qt.labs.settings 1.0 checked: settings.virtual_device_ifit Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.virtual_device_ifit = checked + onClicked: { settings.virtual_device_ifit = checked; window.settings_restart_to_apply = true; } } Label { - text: qsTr("Enables a virtual bluetooth bridge to the iFit App. This setting requires that at least one device be Android. For example, this setting does NOT work with QZ on iOS and iFit to iOS, but DOES work with QZ on iOS and iFit to Android.") + text: qsTr("Enables a virtual bluetooth bridge to the iFit App. This setting requires that at least one device be Android. For example, this setting does NOT work with QZ on iOS and iFit to iOS, but DOES work with QZ on iOS and iFit to Android. On Android remember to rename your device into I_EL into the android settings and reboot your device.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7706,14 +8878,14 @@ import Qt.labs.settings 1.0 checked: settings.wahoo_rgt_dircon Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.wahoo_rgt_dircon = checked + onClicked: { settings.wahoo_rgt_dircon = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Enables the compatibility of the Wahoo KICKR protocol to Wahoo RGT app. Leave the RGT compatibility disabled in order to use Zwift.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7742,7 +8914,7 @@ import Qt.labs.settings 1.0 id: okDirconServerPort text: "OK" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - onClicked: settings.dircon_server_base_port = dirconServerPortTextField.text + onClicked: { settings.dircon_server_base_port = dirconServerPortTextField.text; toast.show("Setting saved!"); } } } } @@ -7750,6 +8922,33 @@ import Qt.labs.settings 1.0 } } + SwitchDelegate { + text: qsTr("Race Mode") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.race_mode + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.race_mode = checked; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("By default QZ sends the info to Zwift or any other 3rd party apps with a 1000ms interval rate. Enabling the Race Mode setting will cause QZ to send them to 100ms (10hz). Of course the bottleneck will be always your bike/treadmill.") + font.bold: true + font.italic: true + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + SwitchDelegate { id: runCadenceSensorDelegate text: qsTr("Run Cadence Sensor") @@ -7762,14 +8961,14 @@ import Qt.labs.settings 1.0 checked: settings.run_cadence_sensor Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.run_cadence_sensor = checked + onClicked: { settings.run_cadence_sensor = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Forces the virtual Bluetooth bridge to send only the cadence information instead of the full FTMS metrics. Default is off.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7835,14 +9034,14 @@ import Qt.labs.settings 1.0 checked: settings.android_wakelock Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.android_wakelock = checked + onClicked: { settings.android_wakelock = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Forces Android devices to remain awake while QZ is running. Default is on.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7863,14 +9062,14 @@ import Qt.labs.settings 1.0 checked: settings.ios_peloton_workaround Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.ios_peloton_workaround = checked + onClicked: { settings.ios_peloton_workaround = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("This MUST be always ON on an iOS device. Turning it OFF will lead to unexpected crashes of QZ. Default is on.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7891,14 +9090,14 @@ import Qt.labs.settings 1.0 checked: settings.applewatch_fakedevice Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.applewatch_fakedevice = checked + onClicked: { settings.applewatch_fakedevice = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Simulates QZ being connected to a bike. When this is turned on QZ will calculate KCal based on your heart rate. Examples of when to use this setting: ○ To capture Peloton class data for classes without connected equipment (e.g., a strength or yoga workout).. ○ To arrange tiles on the QZ dashboard without connecting to your equipment. ○ To use the QZ Apple Watch app without connecting to your equipment.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7919,14 +9118,14 @@ import Qt.labs.settings 1.0 checked: settings.fakedevice_treadmill Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fakedevice_treadmill = checked + onClicked: { settings.fakedevice_treadmill = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Same as Fake Device but instead of simulating a bike it simulates a treadmill.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7947,14 +9146,41 @@ import Qt.labs.settings 1.0 checked: settings.fakedevice_elliptical Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.fakedevice_elliptical = checked + onClicked: { settings.fakedevice_elliptical = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Same as Fake Device but instead of simulating a bike it simulates an elliptical.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + SwitchDelegate { + text: qsTr("Fake Rower") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.fakedevice_rower + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.fakedevice_rower = checked; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("Same as Fake Device but instead of simulating a bike it simulates a rower.") + font.bold: true + font.italic: true + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7975,14 +9201,14 @@ import Qt.labs.settings 1.0 checked: settings.ios_cache_heart_device Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.ios_cache_heart_device = checked + onClicked: { settings.ios_cache_heart_device = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Leave this on unless you have issues connecting your Bluetooth HRM to QZ. If turning this off does not solve the connection issue, open a support ticket on GitHub. Default is on.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8003,14 +9229,14 @@ import Qt.labs.settings 1.0 checked: settings.android_notification Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.android_notification = checked + onClicked: { settings.android_notification = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Android Only: enable this to force Android to don't kill QZ when it's running on background") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8031,14 +9257,14 @@ import Qt.labs.settings 1.0 checked: settings.log_debug Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.log_debug = checked + onClicked: { settings.log_debug = checked; window.settings_restart_to_apply = true; } } Label { text: qsTr("Turn this on to save a debug log to your device for use when requesting help with a bug.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8058,7 +9284,7 @@ import Qt.labs.settings 1.0 text: qsTr("Clears all the QZ logs, QZ .fit files and QZ images (these files are saved by QZ for every session) from your device while maintaining your saved Profiles and Settings.") font.bold: true font.italic: true - font.pixelSize: 8 + font.pixelSize: 9 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter diff --git a/src/shuaa5treadmill.cpp b/src/shuaa5treadmill.cpp index ef4047eac..bd7665ac5 100644 --- a/src/shuaa5treadmill.cpp +++ b/src/shuaa5treadmill.cpp @@ -1,6 +1,5 @@ #include "shuaa5treadmill.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualtreadmill.h" #include #include @@ -11,9 +10,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -43,10 +43,15 @@ void shuaa5treadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, QStri timeout.singleShot(2000, &loop, SLOT(quit())); } - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); if (!disable_log) - qDebug() << " >> " << QByteArray((const char *)data, data_len).toHex(' ') << " // " << info; + qDebug() << " >> " << writeBuffer->toHex(' ') << " // " << info; loop.exec(); } @@ -333,19 +338,8 @@ void shuaa5treadmill::characteristicChanged(const QLowEnergyCharacteristic &char if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { if (heart == 0.0 || settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { - Heart = heart; } } @@ -440,7 +434,7 @@ void shuaa5treadmill::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -454,10 +448,11 @@ void shuaa5treadmill::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &shuaa5treadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &shuaa5treadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -567,10 +562,6 @@ bool shuaa5treadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *shuaa5treadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *shuaa5treadmill::VirtualDevice() { return VirtualTreadmill(); } - void shuaa5treadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/shuaa5treadmill.h b/src/shuaa5treadmill.h index 640e0ef5d..63db3b241 100644 --- a/src/shuaa5treadmill.h +++ b/src/shuaa5treadmill.h @@ -29,7 +29,6 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,13 +38,10 @@ class shuaa5treadmill : public treadmill { Q_OBJECT public: shuaa5treadmill(bool noWriteResistance, bool noHeartService); - bool connected(); + bool connected() override; void forceSpeed(double requestSpeed); void forceIncline(double requestIncline); - double minStepInclination(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + double minStepInclination() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, @@ -55,7 +51,6 @@ class shuaa5treadmill : public treadmill { void btinit(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; diff --git a/src/skandikawiribike.cpp b/src/skandikawiribike.cpp index 949a0f7af..633c4c549 100644 --- a/src/skandikawiribike.cpp +++ b/src/skandikawiribike.cpp @@ -1,5 +1,7 @@ #include "skandikawiribike.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -27,12 +29,7 @@ skandikawiribike::skandikawiribike(bool noWriteResistance, bool noHeartService, refresh->start(300ms); } -skandikawiribike::~skandikawiribike() { - qDebug() << QStringLiteral("~skandikawiribike()") << virtualBike; - if (virtualBike) { - delete virtualBike; - } -} +skandikawiribike::~skandikawiribike() { qDebug() << QStringLiteral("~skandikawiribike()"); } void skandikawiribike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -47,12 +44,15 @@ void skandikawiribike::writeCharacteristic(uint8_t *data, uint8_t data_len, cons timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -192,7 +192,13 @@ void skandikawiribike::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); lastPacket = newValue; - if (newValue.length() == 5) { + if (newValue.length() == 5 && X2000 == false) { + if (newValue.at(2) < 33) { + Resistance = newValue.at(2); + emit debug(QStringLiteral("Current resistance: ") + QString::number(Resistance.value())); + } + return; + } else if (newValue.length() == 6 && X2000 == true) { if (newValue.at(2) < 33) { Resistance = newValue.at(2); emit debug(QStringLiteral("Current resistance: ") + QString::number(Resistance.value())); @@ -202,15 +208,17 @@ void skandikawiribike::characteristicChanged(const QLowEnergyCharacteristic &cha return; } - if (newValue.at(1) == 0x00) { + if ((newValue.at(1) == 0x00 && X2000 == false) || (newValue.at(1) == 0x20 && X2000 == true)) { double speed = GetSpeedFromPacket(newValue); emit debug(QStringLiteral("Current speed: ") + QString::number(speed)); if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = speed; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } - } else if (newValue.at(1) == 0x10) { + } else if ((newValue.at(1) == 0x10 && X2000 == false) || (newValue.at(1) == 0x30 && X2000 == true)) { if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() .startsWith(QStringLiteral("Disabled"))) { @@ -220,7 +228,11 @@ void skandikawiribike::characteristicChanged(const QLowEnergyCharacteristic &cha double kcal = GetKcalFromPacket(newValue); - m_watts = GetWattFromPacket(newValue); + if (X2000) { + m_watts = wattFromHR(true); + } else { + m_watts = GetWattFromPacket(newValue); + } if (Resistance.value() < 1) { emit debug(QStringLiteral("invalid resistance value ") + QString::number(Resistance.value()) + QStringLiteral(" putting to default")); @@ -235,10 +247,17 @@ void skandikawiribike::characteristicChanged(const QLowEnergyCharacteristic &cha #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { - Heart = 0; + if (X2000) { + Heart = newValue.at(8); + } else { + Heart = 0; + } } } + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + if (Cadence.value() > 0) { CrankRevs++; LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); @@ -257,8 +276,6 @@ void skandikawiribike::characteristicChanged(const QLowEnergyCharacteristic &cha } KCal = kcal; - Distance += ((Speed.value() / 3600000.0) * - ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); } double skandikawiribike::GetSpeedFromPacket(const QByteArray &packet) { @@ -285,11 +302,16 @@ double skandikawiribike::GetKcalFromPacket(const QByteArray &packet) { } void skandikawiribike::btinit() { - uint8_t initData1[] = {0x40, 0x00, 0x9a, 0x24, 0xfe}; + if (X2000) { + uint8_t initData1[] = {0x40, 0x00, 0x9a, 0x38, 0x12}; - // in the snoof log it repeats this frame 4 times, i will have to analyze the response to understand if 4 times are - // enough - writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + } else { + uint8_t initData1[] = {0x40, 0x00, 0x9a, 0x24, 0xfe}; + + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + } initDone = true; } @@ -321,7 +343,7 @@ void skandikawiribike::stateChanged(QLowEnergyService::ServiceState state) { &skandikawiribike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -329,11 +351,14 @@ void skandikawiribike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -343,10 +368,11 @@ void skandikawiribike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&skandikawiribike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &skandikawiribike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -400,6 +426,11 @@ void skandikawiribike::deviceDiscovered(const QBluetoothDeviceInfo &device) { { bluetoothDevice = device; + if (device.name().toUpper().startsWith(QLatin1String("HT"))) { + X2000 = true; + qDebug() << "X-2000 WORKAROUND!"; + } + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &skandikawiribike::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &skandikawiribike::serviceScanDone); @@ -440,10 +471,6 @@ bool skandikawiribike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *skandikawiribike::VirtualBike() { return virtualBike; } - -void *skandikawiribike::VirtualDevice() { return VirtualBike(); } - uint16_t skandikawiribike::watts() { QSettings settings; // double v = 0; // NOTE: unused variable v diff --git a/src/skandikawiribike.h b/src/skandikawiribike.h index 69a630d51..4a0dedd15 100644 --- a/src/skandikawiribike.h +++ b/src/skandikawiribike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,10 +38,7 @@ class skandikawiribike : public bike { skandikawiribike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); ~skandikawiribike(); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -57,10 +53,9 @@ class skandikawiribike : public bike { void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattNotify1Characteristic; @@ -79,6 +74,8 @@ class skandikawiribike : public bike { bool noWriteResistance = false; bool noHeartService = false; + bool X2000 = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/smartrowrower.cpp b/src/smartrowrower.cpp index e6d0166a0..301ae87ca 100644 --- a/src/smartrowrower.cpp +++ b/src/smartrowrower.cpp @@ -1,6 +1,8 @@ #include "smartrowrower.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -59,12 +61,20 @@ void smartrowrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const Q return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); + } if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info; + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } loop.exec(); @@ -189,7 +199,9 @@ void smartrowrower::characteristicChanged(const QLowEnergyCharacteristic &charac double distance = GetDistanceFromPacket(newValue); QTime localTime; + int pace_inst; + // https://github.com/inonoob/pirowflo/blob/6ea5f3a9d224ed594b23c25c186737bc0cae7ac3/src/adapters/smartrow/smartrowtobleant.py switch (newValue.at(0)) { case 'a': // elapsed time @@ -216,6 +228,13 @@ void smartrowrower::characteristicChanged(const QLowEnergyCharacteristic &charac break; case 'e': // actual split time + // pace_inst = int(event[6])*60 + int(event[7:9]) + // 3243 = 180 + 243 = 713 + // speed = int(500 * 100 / pace_inst) # speed in cm/s + pace_inst = (atoi(newValue.mid(6, 1)) * 60) + atoi(newValue.mid(7, 2)); + qDebug() << QStringLiteral("pace_inst") << pace_inst; + Speed = (500.0 * 100.0 / pace_inst) * 0.036; + // average split time break; case 'f': @@ -232,10 +251,10 @@ void smartrowrower::characteristicChanged(const QLowEnergyCharacteristic &charac break; } - Speed = (0.37497622 * ((double)Cadence.value())) / 2.0; if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -258,23 +277,15 @@ void smartrowrower::characteristicChanged(const QLowEnergyCharacteristic &charac #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -342,7 +353,7 @@ void smartrowrower::stateChanged(QLowEnergyService::ServiceState state) { &smartrowrower::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -350,11 +361,14 @@ void smartrowrower::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -364,9 +378,10 @@ void smartrowrower::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&smartrowrower::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -459,10 +474,6 @@ bool smartrowrower::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *smartrowrower::VirtualBike() { return virtualBike; } - -void *smartrowrower::VirtualDevice() { return VirtualBike(); } - uint16_t smartrowrower::watts() { if (currentCadence().value() == 0) return 0; diff --git a/src/smartrowrower.h b/src/smartrowrower.h index eee5f037f..f208966bf 100644 --- a/src/smartrowrower.h +++ b/src/smartrowrower.h @@ -27,7 +27,6 @@ #include #include "rower.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,13 +36,10 @@ class smartrowrower : public rower { Q_OBJECT public: smartrowrower(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t resistanceFromPowerRequest(uint16_t power); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t resistanceFromPowerRequest(uint16_t power) override; + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: const resistance_t max_resistance = 32; @@ -57,10 +53,9 @@ class smartrowrower : public rower { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/smartspin2k.cpp b/src/smartspin2k.cpp index 33e0b5782..9850d4a05 100644 --- a/src/smartspin2k.cpp +++ b/src/smartspin2k.cpp @@ -1,6 +1,5 @@ #include "smartspin2k.h" -#include "ios/lockscreen.h" -#include "virtualbike.h" +#include "ftmsbike.h" #include #include #include @@ -11,9 +10,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -112,12 +112,15 @@ void smartspin2k::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt timeout.singleShot(300, &loop, SLOT(quit())); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -143,12 +146,15 @@ void smartspin2k::writeCharacteristicFTMS(uint8_t *data, uint8_t data_len, const timeout.singleShot(300, &loop, SLOT(quit())); } - gattCommunicationChannelServiceFTMS->writeCharacteristic(gattWriteCharControlPointId, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelServiceFTMS->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -196,11 +202,15 @@ void smartspin2k::forceResistance(resistance_t requestResistance) { QSettings settings; - double ss2k_min_resistance = settings.value(QZSettings::ss2k_min_resistance, QZSettings::default_ss2k_min_resistance).toDouble(); - double ss2k_max_resistance = settings.value(QZSettings::ss2k_max_resistance, QZSettings::default_ss2k_max_resistance).toDouble(); + double ss2k_min_resistance = + settings.value(QZSettings::ss2k_min_resistance, QZSettings::default_ss2k_min_resistance).toDouble(); + double ss2k_max_resistance = + settings.value(QZSettings::ss2k_max_resistance, QZSettings::default_ss2k_max_resistance).toDouble(); - if(requestResistance > ss2k_max_resistance) requestResistance = ss2k_max_resistance; - if(requestResistance < ss2k_min_resistance) requestResistance = ss2k_min_resistance; + if (requestResistance > ss2k_max_resistance) + requestResistance = ss2k_max_resistance; + if (requestResistance < ss2k_min_resistance) + requestResistance = ss2k_min_resistance; // if not calibrated, slope=0 and intercept is the configured shift step uint16_t steps = slope * requestResistance + intercept; @@ -512,10 +522,6 @@ bool smartspin2k::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *smartspin2k::VirtualBike() { return virtualBike; } - -void *smartspin2k::VirtualDevice() { return VirtualBike(); } - uint16_t smartspin2k::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/smartspin2k.h b/src/smartspin2k.h index cabaa0119..e16b909a4 100644 --- a/src/smartspin2k.h +++ b/src/smartspin2k.h @@ -30,8 +30,6 @@ #include #include "bike.h" -#include "ftmsbike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -42,10 +40,7 @@ class smartspin2k : public bike { Q_OBJECT public: smartspin2k(bool noWriteResistance, bool noHeartService, resistance_t max_resistance, bike *parentDevice); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: #define max_calibration_samples 4 @@ -54,13 +49,12 @@ class smartspin2k : public bike { void writeCharacteristicFTMS(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; void forceResistance(resistance_t requestResistance); void setShiftStep(uint16_t); void lowInit(resistance_t resistance); QTimer *refresh; - virtualbike *virtualBike = nullptr; QUdpSocket *udpSocket = new QUdpSocket(); diff --git a/src/snodebike.cpp b/src/snodebike.cpp index a69889326..43d88dbca 100644 --- a/src/snodebike.cpp +++ b/src/snodebike.cpp @@ -2,7 +2,6 @@ #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -12,9 +11,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -162,7 +162,9 @@ void snodebike::characteristicChanged(const QLowEnergyCharacteristic &characteri (uint16_t)((uint8_t)newValue.at(index)))) / 100.0; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } index += 2; emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); @@ -251,8 +253,8 @@ void snodebike::characteristicChanged(const QLowEnergyCharacteristic &characteri { if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * - 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in @@ -312,16 +314,7 @@ void snodebike::characteristicChanged(const QLowEnergyCharacteristic &characteri if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { if (heart == 0.0) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { Heart = heart; } @@ -330,7 +323,8 @@ void snodebike::characteristicChanged(const QLowEnergyCharacteristic &characteri #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -378,7 +372,7 @@ void snodebike::stateChanged(QLowEnergyService::ServiceState state) { emit connectedAndDiscovered(); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -386,11 +380,14 @@ void snodebike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -400,10 +397,11 @@ void snodebike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&snodebike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &snodebike::changeInclination); connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &snodebike::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -424,7 +422,12 @@ void snodebike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charac } } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharControlPointId, b); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray(b); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); } } @@ -514,10 +517,6 @@ bool snodebike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *snodebike::VirtualBike() { return virtualBike; } - -void *snodebike::VirtualDevice() { return VirtualBike(); } - uint16_t snodebike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/snodebike.h b/src/snodebike.h index 4ebece888..81d93e64c 100644 --- a/src/snodebike.h +++ b/src/snodebike.h @@ -29,7 +29,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,19 +38,15 @@ class snodebike : public bike { Q_OBJECT public: snodebike(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, bool wait_for_response = false); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService; QLowEnergyCharacteristic gattNotify1Characteristic; diff --git a/src/solebike.cpp b/src/solebike.cpp index 9e6e9b8ec..f42eb06eb 100644 --- a/src/solebike.cpp +++ b/src/solebike.cpp @@ -1,6 +1,7 @@ #include "solebike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -59,12 +60,15 @@ void solebike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info; + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } loop.exec(); @@ -254,14 +258,17 @@ void solebike::characteristicChanged(const QLowEnergyCharacteristic &characteris if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = GetSpeedFromPacket(newValue); } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } m_watt = GetWattFromPacket(newValue); if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -283,23 +290,15 @@ void solebike::characteristicChanged(const QLowEnergyCharacteristic &characteris #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -435,7 +434,7 @@ void solebike::stateChanged(QLowEnergyService::ServiceState state) { &solebike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -443,11 +442,14 @@ void solebike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -457,10 +459,11 @@ void solebike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&solebike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &solebike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -559,10 +562,6 @@ bool solebike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *solebike::VirtualBike() { return virtualBike; } - -void *solebike::VirtualDevice() { return VirtualBike(); } - uint16_t solebike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/solebike.h b/src/solebike.h index 5dcca31fb..e9355592a 100644 --- a/src/solebike.h +++ b/src/solebike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,12 +36,9 @@ class solebike : public bike { Q_OBJECT public: solebike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: bool r92 = false; @@ -58,10 +54,9 @@ class solebike : public bike { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/soleelliptical.cpp b/src/soleelliptical.cpp index 030cadb15..23f6866e7 100644 --- a/src/soleelliptical.cpp +++ b/src/soleelliptical.cpp @@ -1,6 +1,9 @@ #include "soleelliptical.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -29,13 +32,7 @@ soleelliptical::soleelliptical(bool noWriteResistance, bool noHeartService, bool refresh->start(300ms); } -soleelliptical::~soleelliptical() { - qDebug() << QStringLiteral("~soleelliptical()") << virtualTreadmill; - if (virtualTreadmill) { - - delete virtualTreadmill; - } -} +soleelliptical::~soleelliptical() { qDebug() << QStringLiteral("~soleelliptical()"); } void soleelliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response) { @@ -50,12 +47,15 @@ void soleelliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -133,15 +133,18 @@ void soleelliptical::update() { gattCommunicationChannelService && gattWriteCharacteristic.isValid() && gattNotifyCharacteristic.isValid() && initDone) { - update_metrics(true, watts()); - QSettings settings; + bool watt_ignore_builtin = + settings.value(QZSettings::watt_ignore_builtin, QZSettings::default_watt_ignore_builtin).toBool(); + + update_metrics(watt_ignore_builtin, watts()); + bool sole_elliptical_inclination = settings.value(QZSettings::sole_elliptical_inclination, QZSettings::default_sole_elliptical_inclination) .toBool(); // ******************************************* virtual treadmill init ************************************* - if (!firstVirtual && searchStopped && !virtualTreadmill && !virtualBike) { + if (!firstVirtual && searchStopped && !this->hasVirtualDevice()) { bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); bool virtual_device_force_bike = @@ -150,15 +153,17 @@ void soleelliptical::update() { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &soleelliptical::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &soleelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &soleelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstVirtual = 1; } @@ -169,10 +174,11 @@ void soleelliptical::update() { if (sec1Update++ == (1000 / refresh->interval())) { sec1Update = 0; - } else { - bool sole_elliptical_e55 = settings.value(QZSettings::sole_elliptical_e55, QZSettings::default_sole_elliptical_e55).toBool(); + } else { + bool sole_elliptical_e55 = + settings.value(QZSettings::sole_elliptical_e55, QZSettings::default_sole_elliptical_e55).toBool(); - if(!sole_elliptical_e55) { + if (!sole_elliptical_e55) { uint8_t noOpData[] = {0x5b, 0x04, 0x00, 0x10, 0x4f, 0x4b, 0x5d}; uint8_t noOpData1[] = {0x5b, 0x04, 0x00, 0x06, 0x4f, 0x4b, 0x5d}; @@ -336,16 +342,19 @@ void soleelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara // double distance = GetDistanceFromPacket(newValue) * // settings.value(QZSettings::domyos_elliptical_speed_ratio, // QZSettings::default_domyos_elliptical_speed_ratio).toDouble(); - uint16_t watt = (newValue.at(13) << 8) | newValue.at(14); + uint16_t watt = ((uint16_t)((uint8_t)newValue.at(13)) << 8) | (uint16_t)((uint8_t)newValue.at(14)); bool disable_hr_frommachinery = settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + bool watt_ignore_builtin = + settings.value(QZSettings::watt_ignore_builtin, QZSettings::default_watt_ignore_builtin).toBool(); if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) .toString() .startsWith(QStringLiteral("Disabled"))) { Cadence = ((uint8_t)newValue.at(10)); } - // m_watt = watt; + if(!watt_ignore_builtin) + m_watt = watt; if (Resistance.value() < 1) { emit debug(QStringLiteral("invalid resistance value ") + QString::number(Resistance.value()) + @@ -361,19 +370,9 @@ void soleelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara { if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) { Heart = ((uint8_t)newValue.at(18)); + } else if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); } -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - else { - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); - } -#endif -#endif } Distance += ((Speed.value() / 3600000.0) * @@ -425,9 +424,10 @@ void soleelliptical::btinit(bool startTape) { QSettings settings; Q_UNUSED(startTape) - bool sole_elliptical_e55 = settings.value(QZSettings::sole_elliptical_e55, QZSettings::default_sole_elliptical_e55).toBool(); + bool sole_elliptical_e55 = + settings.value(QZSettings::sole_elliptical_e55, QZSettings::default_sole_elliptical_e55).toBool(); - if(!sole_elliptical_e55) { + if (!sole_elliptical_e55) { // set speed and incline to 0 uint8_t initData1[] = {0x5b, 0x01, 0xf0, 0x5d}; uint8_t initData2[] = {0x5b, 0x02, 0x03, 0x01, 0x5d}; @@ -610,10 +610,6 @@ bool soleelliptical::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *soleelliptical::VirtualTreadmill() { return virtualTreadmill; } - -void *soleelliptical::VirtualDevice() { return VirtualTreadmill(); } - uint16_t soleelliptical::watts() { QSettings settings; diff --git a/src/soleelliptical.h b/src/soleelliptical.h index 44dd03340..b4af0f50e 100644 --- a/src/soleelliptical.h +++ b/src/soleelliptical.h @@ -27,8 +27,6 @@ #include #include "elliptical.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" class soleelliptical : public elliptical { Q_OBJECT @@ -36,11 +34,8 @@ class soleelliptical : public elliptical { soleelliptical(bool noWriteResistance = false, bool noHeartService = false, bool testResistance = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); ~soleelliptical(); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); - double minStepInclination() { return 1; } + bool connected() override; + double minStepInclination() override { return 1; } private: double GetSpeedFromPacket(const QByteArray &packet); @@ -56,8 +51,6 @@ class soleelliptical : public elliptical { uint16_t watts(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = 0; uint8_t firstVirtual = 0; uint8_t counterPoll = 0; diff --git a/src/solef80treadmill.cpp b/src/solef80treadmill.cpp index bade51dd1..a87a829c2 100644 --- a/src/solef80treadmill.cpp +++ b/src/solef80treadmill.cpp @@ -1,6 +1,4 @@ #include "solef80treadmill.h" - -#include "ios/lockscreen.h" #include "virtualtreadmill.h" #include #include @@ -13,7 +11,9 @@ #ifdef Q_OS_ANDROID #include #endif +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include using namespace std::chrono_literals; @@ -68,10 +68,20 @@ void solef80treadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, QStr timeout.singleShot(2000, &loop, SLOT(quit())); } - gattCustomService->writeCharacteristic(gattWriteCharCustomService, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (gattWriteCharCustomService.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + gattCustomService->writeCharacteristic(gattWriteCharCustomService, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + gattCustomService->writeCharacteristic(gattWriteCharCustomService, *writeBuffer); + } if (!disable_log) - qDebug() << " >> " << QByteArray((const char *)data, data_len).toHex(' ') << " // " << info; + qDebug() << " >> " << writeBuffer->toHex(' ') << " // " << info; loop.exec(); } @@ -280,6 +290,10 @@ void solef80treadmill::update() { /*initDone*/) { QSettings settings; + bool sole_treadmill_inclination_fast = + settings + .value(QZSettings::sole_treadmill_inclination_fast, QZSettings::default_sole_treadmill_inclination_fast) + .toBool(); update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())); // updating the treadmill console every second @@ -298,30 +312,45 @@ void solef80treadmill::update() { writeCharacteristic(noop2, sizeof(noop2), QStringLiteral("noop2"), false, true); } - if (requestSpeed != -1) { - if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { - emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); - forceSpeed(requestSpeed); + int max_speed_loop = 0; + if(requestSpeed != -1) { + max_speed_loop = (fabs(requestSpeed - currentSpeed().value()) * 10.0) - 1; + } + + do { + if (requestSpeed != -1) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed) + " " + QString::number(max_speed_loop)); + forceSpeed(requestSpeed); + } + // i have to do the reset on when the speed is equal to the current + // requestSpeed = -1; } - // i have to do the reset on when the speed is equal to the current - // requestSpeed = -1; - } - if (requestInclination != -100) { - if (requestInclination < 0) - requestInclination = 0; - // this treadmill has only 1% step inclination - if ((int)requestInclination != (int)currentInclination().value() && requestInclination >= 0 && - requestInclination <= 15) { - emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); - forceIncline(requestInclination); - } else if ((int)requestInclination == (int)currentInclination().value()) { - qDebug() << "int inclination match the current one" << requestInclination - << currentInclination().value(); - requestInclination = -100; + } while (requestSpeed != -1 && sole_treadmill_inclination_fast && max_speed_loop); + + int max_inclination_loop = 0; + if(requestInclination != -100) { + max_inclination_loop = abs(requestInclination - (int)currentInclination().value()); + } + do { + if (requestInclination != -100) { + if (requestInclination < 0) + requestInclination = 0; + // this treadmill has only 1% step inclination + if ((int)requestInclination != (int)currentInclination().value() && requestInclination >= 0 && + requestInclination <= 15) { + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination) + " " + QString::number(max_inclination_loop)); + forceIncline(requestInclination); + } else if ((int)requestInclination == (int)currentInclination().value()) { + qDebug() << "int inclination match the current one" << requestInclination + << currentInclination().value(); + requestInclination = -100; + } + // i have to do the reset on when the inclination is equal to the current + // requestInclination = -100; } - // i have to do the reset on when the inclination is equal to the current - // requestInclination = -100; - } + } while (requestInclination != -100 && sole_treadmill_inclination_fast && max_inclination_loop); + if (requestStart != -1) { emit debug(QStringLiteral("starting...")); if (lastSpeed == 0.0) { @@ -344,18 +373,32 @@ void solef80treadmill::update() { if (requestStop != -1) { emit debug(QStringLiteral("stopping...")); - uint8_t stop[] = {0x5b, 0x02, 0x03, 0x06, 0x5d}; - uint8_t stop1[] = {0x5b, 0x02, 0x03, 0x07, 0x5d}; - uint8_t stop2[] = {0x5b, 0x04, 0x00, 0x32, 0x4f, 0x4b, 0x5d}; - - if (gattCustomService) { - writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); - writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); - writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); - writeCharacteristic(stop2, sizeof(stop2), QStringLiteral("stop"), false, true); - writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); - writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); - writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + if(treadmill_type == F63) { + uint8_t stop[] = {0x5b, 0x02, 0xf1, 0x06, 0x5d}; + uint8_t stop1[] = {0x5b, 0x02, 0x03, 0x06, 0x5d}; + + if (gattCustomService) { + writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); + writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + } + } else { + uint8_t stop[] = {0x5b, 0x02, 0x03, 0x06, 0x5d}; + uint8_t stop1[] = {0x5b, 0x02, 0x03, 0x07, 0x5d}; + uint8_t stop2[] = {0x5b, 0x04, 0x00, 0x32, 0x4f, 0x4b, 0x5d}; + + if (gattCustomService) { + writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); + writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); + writeCharacteristic(stop, sizeof(stop), QStringLiteral("stop"), false, true); + writeCharacteristic(stop2, sizeof(stop2), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + writeCharacteristic(stop1, sizeof(stop1), QStringLiteral("stop"), false, true); + } } requestStop = -1; @@ -661,19 +704,8 @@ void solef80treadmill::characteristicChanged(const QLowEnergyCharacteristic &cha if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { if (heart == 0.0 || settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { - Heart = heart; } } @@ -770,7 +802,7 @@ void solef80treadmill::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -784,10 +816,11 @@ void solef80treadmill::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &solef80treadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &solef80treadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -900,10 +933,6 @@ bool solef80treadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *solef80treadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *solef80treadmill::VirtualDevice() { return VirtualTreadmill(); } - void solef80treadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/solef80treadmill.h b/src/solef80treadmill.h index d31af6d73..7c069f96d 100644 --- a/src/solef80treadmill.h +++ b/src/solef80treadmill.h @@ -29,7 +29,6 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,13 +38,10 @@ class solef80treadmill : public treadmill { Q_OBJECT public: solef80treadmill(bool noWriteResistance, bool noHeartService); - bool connected(); + bool connected() override; void forceSpeed(double requestSpeed); void forceIncline(double requestIncline); - double minStepInclination(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + double minStepInclination() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, @@ -55,7 +51,6 @@ class solef80treadmill : public treadmill { void btinit(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; diff --git a/src/spirittreadmill.cpp b/src/spirittreadmill.cpp index 6c96d245f..2387a63e2 100644 --- a/src/spirittreadmill.cpp +++ b/src/spirittreadmill.cpp @@ -1,5 +1,8 @@ #include "spirittreadmill.h" +#include "virtualbike.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include @@ -34,12 +37,15 @@ void spirittreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -445,7 +451,7 @@ void spirittreadmill::stateChanged(QLowEnergyService::ServiceState state) { &spirittreadmill::descriptorWritten); // ******************************************* virtual treadmill init ************************************* - if (!firstVirtualTreadmill && !virtualTreadMill && !virtualBike) { + if (!firstVirtualTreadmill && !this->hasVirtualDevice()) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); @@ -455,15 +461,17 @@ void spirittreadmill::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, false); + auto virtualTreadMill = new virtualtreadmill(this, false); connect(virtualTreadMill, &virtualtreadmill::debug, this, &spirittreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &spirittreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &spirittreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } } } @@ -579,10 +587,6 @@ bool spirittreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *spirittreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *spirittreadmill::VirtualDevice() { return VirtualTreadMill(); } - void spirittreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/spirittreadmill.h b/src/spirittreadmill.h index e9d472125..4d73dbf75 100644 --- a/src/spirittreadmill.h +++ b/src/spirittreadmill.h @@ -26,17 +26,13 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" class spirittreadmill : public treadmill { Q_OBJECT public: spirittreadmill(); - bool connected(); + bool connected() override; - void *VirtualTreadMill(); - void *VirtualDevice(); private: double GetSpeedFromPacket(const QByteArray &packet); @@ -53,8 +49,6 @@ class spirittreadmill : public treadmill { void startDiscover(); QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = nullptr; uint8_t firstVirtualTreadmill = 0; bool firstCharChanged = true; diff --git a/src/sportsplusbike.cpp b/src/sportsplusbike.cpp index 8696f8264..c8a8c6497 100644 --- a/src/sportsplusbike.cpp +++ b/src/sportsplusbike.cpp @@ -1,5 +1,7 @@ #include "sportsplusbike.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -36,11 +38,15 @@ void sportsplusbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + " // " + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + " // " + info); } loop.exec(); @@ -143,7 +149,9 @@ void sportsplusbike::characteristicChanged(const QLowEnergyCharacteristic &chara if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = speed; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } lastTimeCharChanged = QDateTime::currentDateTime(); } else if (newValue.at(1) == 0x30) { @@ -160,7 +168,7 @@ void sportsplusbike::characteristicChanged(const QLowEnergyCharacteristic &chara cadence = (uint8_t)newValue.at(8); // double resistance = GetResistanceFromPacket(newValue); kcal = GetKcalFromPacket(newValue); - } else if(carefitness_bike) { + } else if (carefitness_bike) { if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) .toString() .startsWith(QStringLiteral("Disabled"))) { @@ -183,12 +191,15 @@ void sportsplusbike::characteristicChanged(const QLowEnergyCharacteristic &chara if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = speed; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value())); if (!firstCharChanged) { - Distance += ((Speed.value() / 3600.0) / (1000.0 / (lastTimeCharChanged.msecsTo(QDateTime::currentDateTime())))); + Distance += + ((Speed.value() / 3600.0) / (1000.0 / (lastTimeCharChanged.msecsTo(QDateTime::currentDateTime())))); } lastTimeCharChanged = QDateTime::currentDateTime(); @@ -220,7 +231,9 @@ void sportsplusbike::characteristicChanged(const QLowEnergyCharacteristic &chara if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = speed; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } lastTimeCharChanged = QDateTime::currentDateTime(); kcal = GetKcalFromPacket(newValue); @@ -335,7 +348,7 @@ void sportsplusbike::stateChanged(QLowEnergyService::ServiceState state) { QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0000fff2-0000-1000-8000-00805f9b34fb")); QBluetoothUuid _gattNotify3CharacteristicId(QStringLiteral("0000fff3-0000-1000-8000-00805f9b34fb")); - if(!carefitness_bike) + if (!carefitness_bike) gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId); else gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId); @@ -357,14 +370,16 @@ void sportsplusbike::stateChanged(QLowEnergyService::ServiceState state) { &sportsplusbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstVirtualBike && !virtualBike) { + if (!firstVirtualBike && !this->hasVirtualDevice()) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&sportsplusbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &sportsplusbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstVirtualBike = 1; @@ -440,8 +455,8 @@ void sportsplusbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { device.address().toString() + ')'); { bluetoothDevice = device; - if((bluetoothDevice.name().toUpper().contains(QStringLiteral("CARE")) && - bluetoothDevice.name().length() == 11)) // CARE9040177 - Carefitness CV-351) + if ((bluetoothDevice.name().toUpper().contains(QStringLiteral("CARE")) && + bluetoothDevice.name().length() == 11)) // CARE9040177 - Carefitness CV-351) { carefitness_bike = true; } @@ -494,10 +509,6 @@ bool sportsplusbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *sportsplusbike::VirtualBike() { return virtualBike; } - -void *sportsplusbike::VirtualDevice() { return VirtualBike(); } - void sportsplusbike::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { @@ -511,30 +522,14 @@ uint16_t sportsplusbike::wattsFromResistance(double resistance) { const int wattTableFirstDimension = 24; const int wattTableSecondDimension = 6; double wattTable[wattTableFirstDimension][wattTableSecondDimension] = { - {13,17,22,27,33,39}, - {14,19,24,30,36,42}, - {15,21,26,33,39,45}, - {16,22,28,34,42,49}, - {18,24,30,37,45,53}, - {20,27,34,42,51,60}, - {22,30,38,47,57,67}, - {24,33,42,53,63,75}, - {27,36,46,58,70,83}, - {31,41,53,67,81,96}, - {35,47,60,76,92,110}, - {39,53,67,85,104,124}, - {33,59,75,95,116,138}, - {47,63,81,103,126,151}, - {50,68,88,111,136,164}, - {54,74,95,120,147,178}, - {58,80,103,129,158,192}, - {63,86,111,139,169,206}, - {68,92,119,149,181,220}, - {73,98,127,159,193,234}, - {77,104,134,168,205,248}, - {81,110,141,177,217,262}, - {86,116,149,187,229,276}, - {91,122,157,197,240,290}}; + {13, 17, 22, 27, 33, 39}, {14, 19, 24, 30, 36, 42}, {15, 21, 26, 33, 39, 45}, + {16, 22, 28, 34, 42, 49}, {18, 24, 30, 37, 45, 53}, {20, 27, 34, 42, 51, 60}, + {22, 30, 38, 47, 57, 67}, {24, 33, 42, 53, 63, 75}, {27, 36, 46, 58, 70, 83}, + {31, 41, 53, 67, 81, 96}, {35, 47, 60, 76, 92, 110}, {39, 53, 67, 85, 104, 124}, + {33, 59, 75, 95, 116, 138}, {47, 63, 81, 103, 126, 151}, {50, 68, 88, 111, 136, 164}, + {54, 74, 95, 120, 147, 178}, {58, 80, 103, 129, 158, 192}, {63, 86, 111, 139, 169, 206}, + {68, 92, 119, 149, 181, 220}, {73, 98, 127, 159, 193, 234}, {77, 104, 134, 168, 205, 248}, + {81, 110, 141, 177, 217, 262}, {86, 116, 149, 187, 229, 276}, {91, 122, 157, 197, 240, 290}}; int level = resistance; if (level < 0) { diff --git a/src/sportsplusbike.h b/src/sportsplusbike.h index 0ed7dec56..a35a68c47 100644 --- a/src/sportsplusbike.h +++ b/src/sportsplusbike.h @@ -32,11 +32,8 @@ class sportsplusbike : public bike { Q_OBJECT public: sportsplusbike(bool noWriteResistance, bool noHeartService); - resistance_t pelotonToBikeResistance(int pelotonResistance); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -50,11 +47,10 @@ class sportsplusbike : public bike { bool wait_for_response); uint16_t wattsFromResistance(double resistance); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; double GetWattFromPacket(const QByteArray &packet); QTimer *refresh; - virtualbike *virtualBike = nullptr; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); diff --git a/src/sportstechbike.cpp b/src/sportstechbike.cpp index 74ca0f79c..330d7beb2 100644 --- a/src/sportstechbike.cpp +++ b/src/sportstechbike.cpp @@ -1,5 +1,7 @@ #include "sportstechbike.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -36,11 +38,15 @@ void sportstechbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + " // " + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + " // " + info); } loop.exec(); @@ -163,7 +169,9 @@ void sportstechbike::characteristicChanged(const QLowEnergyCharacteristic &chara if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = speed; } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } Resistance = requestResistance; emit resistanceRead(Resistance.value()); @@ -275,14 +283,16 @@ void sportstechbike::stateChanged(QLowEnergyService::ServiceState state) { &sportstechbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstVirtualBike && !virtualBike) { + if (!firstVirtualBike && !this->hasVirtualDevice()) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&sportstechbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &sportstechbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstVirtualBike = 1; @@ -390,10 +400,6 @@ bool sportstechbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *sportstechbike::VirtualBike() { return virtualBike; } - -void *sportstechbike::VirtualDevice() { return VirtualBike(); } - void sportstechbike::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/sportstechbike.h b/src/sportstechbike.h index 953a7367e..4e3a0eecd 100644 --- a/src/sportstechbike.h +++ b/src/sportstechbike.h @@ -26,16 +26,12 @@ #include #include "bike.h" -#include "virtualbike.h" class sportstechbike : public bike { Q_OBJECT public: sportstechbike(bool noWriteResistance, bool noHeartService); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -49,12 +45,11 @@ class sportstechbike : public bike { void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; double GetWattFromPacket(const QByteArray &packet); double GetCadenceFromPacket(const QByteArray &packet); QTimer *refresh; - virtualbike *virtualBike = nullptr; bool noWriteResistance = false; bool noHeartService = false; diff --git a/src/stagesbike.cpp b/src/stagesbike.cpp index f58b9bdb7..c6a187d94 100644 --- a/src/stagesbike.cpp +++ b/src/stagesbike.cpp @@ -1,5 +1,4 @@ #include "stagesbike.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -11,7 +10,9 @@ #ifdef Q_OS_ANDROID #include #endif +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include using namespace std::chrono_literals; @@ -177,6 +178,7 @@ void stagesbike::characteristicChanged(const QLowEnergyCharacteristic &character uint16_t flags = (((uint16_t)((uint8_t)newValue.at(1)) << 8) | (uint16_t)((uint8_t)newValue.at(0))); bool cadence_present = false; bool wheel_revs = false; + bool crank_rev_present = false; uint16_t time_division = 1024; uint8_t index = 4; @@ -206,26 +208,42 @@ void stagesbike::characteristicChanged(const QLowEnergyCharacteristic &character { cadence_present = true; wheel_revs = true; - time_division = 2048; - } else if ((flags & 0x20) == 0x20) // Crank Revolution Data Present + } + + if ((flags & 0x20) == 0x20) // Crank Revolution Data Present { cadence_present = true; + crank_rev_present = true; } if (cadence_present) { - if (!wheel_revs) { - CrankRevs = - (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); - index += 2; - } else { + if (wheel_revs && !crank_rev_present) { + time_division = 2048; CrankRevs = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) | ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index))); index += 4; + + LastCrankEventTime = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + + index += 2; // wheel event time + + } else if (wheel_revs && crank_rev_present) { + index += 4; // wheel revs + index += 2; // wheel event time + } + + if (crank_rev_present) { + CrankRevs = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + index += 2; + + LastCrankEventTime = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + index += 2; } - LastCrankEventTime = - (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime; if (deltaT < 0) { @@ -237,6 +255,10 @@ void stagesbike::characteristicChanged(const QLowEnergyCharacteristic &character .startsWith(QStringLiteral("Disabled"))) { if (CrankRevs != oldCrankRevs && deltaT) { double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * time_division * 60; + if (!crank_rev_present) + cadence = + cadence / + 2; // I really don't like this, there is no relationship between wheel rev and crank rev if (cadence >= 0) { Cadence = cadence; } @@ -246,7 +268,8 @@ void stagesbike::characteristicChanged(const QLowEnergyCharacteristic &character } } - emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); + qDebug() << QStringLiteral("Current Cadence: ") << Cadence.value() << CrankRevs << oldCrankRevs << deltaT + << time_division << LastCrankEventTime << oldLastCrankEventTime; oldLastCrankEventTime = LastCrankEventTime; oldCrankRevs = CrankRevs; @@ -330,24 +353,10 @@ void stagesbike::characteristicChanged(const QLowEnergyCharacteristic &character } else #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } - if (Cadence.value() > 0) { - CrankRevs++; - LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); - } - lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); if (!noVirtualDevice) { @@ -447,7 +456,7 @@ void stagesbike::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike && !noVirtualDevice + if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -472,9 +481,10 @@ void stagesbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&stagesbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &stagesbike::inclinationChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -579,9 +589,6 @@ bool stagesbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *stagesbike::VirtualBike() { return virtualBike; } - -void *stagesbike::VirtualDevice() { return VirtualBike(); } uint16_t stagesbike::watts() { if (currentCadence().value() == 0) { diff --git a/src/stagesbike.h b/src/stagesbike.h index e7d14b67e..6fb469bd6 100644 --- a/src/stagesbike.h +++ b/src/stagesbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,13 +36,10 @@ class stagesbike : public bike { Q_OBJECT public: stagesbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); - resistance_t pelotonToBikeResistance(int pelotonResistance); - bool connected(); - resistance_t maxResistance() { return 100; } - bool ergManagedBySS2K() { return true; } - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + bool connected() override; + resistance_t maxResistance() override { return 100; } + bool ergManagedBySS2K() override { return true; } private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, @@ -52,10 +48,9 @@ class stagesbike : public bike { metric ResistanceFromFTMSAccessory; uint64_t ResistanceFromFTMSAccessoryLastTime = 0; void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; // QLowEnergyCharacteristic gattNotify1Characteristic; @@ -86,7 +81,7 @@ class stagesbike : public bike { public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); - void resistanceFromFTMSAccessory(resistance_t res); + void resistanceFromFTMSAccessory(resistance_t res) override; private slots: diff --git a/src/strydrunpowersensor.cpp b/src/strydrunpowersensor.cpp index d75e403c6..9e03eb660 100644 --- a/src/strydrunpowersensor.cpp +++ b/src/strydrunpowersensor.cpp @@ -1,5 +1,6 @@ #include "strydrunpowersensor.h" -#include "ios/lockscreen.h" + +#include "virtualtreadmill.h" #include "virtualbike.h" #include #include @@ -9,9 +10,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -79,8 +81,8 @@ void strydrunpowersensor::update() { update_metrics(true, watts()); if (requestInclination != -100) { - Inclination = requestInclination; - emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + Inclination = treadmillInclinationOverrideReverse(requestInclination); + qDebug() << QStringLiteral("writing incline ") << requestInclination; requestInclination = -100; } @@ -98,7 +100,7 @@ void strydrunpowersensor::serviceDiscovered(const QBluetoothUuid &gatt) { void strydrunpowersensor::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { - qDebug() << "characteristicChanged" << characteristic.uuid() << newValue.toHex(' ') << newValue.length(); + qDebug() << "<<" << characteristic.uuid() << newValue.toHex(' ') << newValue.length(); Q_UNUSED(characteristic); QSettings settings; bool power_as_treadmill = @@ -116,6 +118,7 @@ void strydrunpowersensor::characteristicChanged(const QLowEnergyCharacteristic & uint8_t index = 4; if (newValue.length() > 3) { + powerReceived = true; m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2))); } @@ -228,9 +231,12 @@ void strydrunpowersensor::characteristicChanged(const QLowEnergyCharacteristic & emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); if (Flags.inclination) { - Inclination = treadmillInclinationOverride(((double)(((int16_t)((int8_t)newValue.at(index + 1)) << 8) | - (int16_t)((uint8_t)newValue.at(index)))) / - 10.0); + double inc = + ((double)(((int16_t)((int8_t)newValue.at(index + 1)) << 8) | (int16_t)((uint8_t)newValue.at(index)))) / + 10.0; + // steps of 0.5 only to send to the Inclination override function + inc = qRound(inc * 2.0) / 2.0; + Inclination = treadmillInclinationOverride(inc); index += 4; // the ramo value is useless emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value())); } @@ -346,6 +352,11 @@ void strydrunpowersensor::characteristicChanged(const QLowEnergyCharacteristic & ((double)lastRefreshCadenceChanged.msecsTo(QDateTime::currentDateTime()))); emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); emit debug(QStringLiteral("Current Speed: ") + QString::number(speed)); + if (powerReceived == false) { + m_watt = wattsCalc(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(), + Speed.value(), Inclination.value()); + emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value())); + } } emit debug(QStringLiteral("Current Cadence: ") + QString::number(cadence)); lastRefreshCadenceChanged = QDateTime::currentDateTime(); @@ -421,16 +432,7 @@ void strydrunpowersensor::characteristicChanged(const QLowEnergyCharacteristic & } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && Heart.value() == 0) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } @@ -527,8 +529,8 @@ void strydrunpowersensor::stateChanged(QLowEnergyService::ServiceState state) { } } - // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualTreadmill && !noVirtualDevice + // ******************************************* virtual treadmill/bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -538,18 +540,41 @@ void strydrunpowersensor::stateChanged(QLowEnergyService::ServiceState state) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { - emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadmill = new virtualtreadmill(this, noHeartService); - // connect(virtualBike,&virtualbike::debug ,this,&strydrunpowersensor::debug); - connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, - &strydrunpowersensor::inclinationChanged); + if (!virtual_device_force_bike) { + debug("creating virtual treadmill interface..."); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); + connect(virtualTreadmill, &virtualtreadmill::debug, this, &strydrunpowersensor::debug); + connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, + &strydrunpowersensor::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + debug("creating virtual bike interface..."); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, + settings.value(QZSettings::bike_resistance_offset, QZSettings::default_bike_resistance_offset).toInt(), + settings.value(QZSettings::bike_resistance_gain_f, QZSettings::default_bike_resistance_gain_f).toDouble()); + connect(virtualBike, &virtualbike::changeInclination, this, + &strydrunpowersensor::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); + } + } } firstStateChanged = 1; // ******************************************************************************************************** } +void strydrunpowersensor::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + if (grade < 0) + grade = 0; + changeInclination(grade, percentage); +} + void strydrunpowersensor::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' ')); @@ -642,10 +667,6 @@ bool strydrunpowersensor::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *strydrunpowersensor::VirtualTreadmill() { return virtualTreadmill; } - -void *strydrunpowersensor::VirtualDevice() { return VirtualTreadmill(); } - uint16_t strydrunpowersensor::watts() { return m_watt.value(); } void strydrunpowersensor::controllerStateChanged(QLowEnergyController::ControllerState state) { diff --git a/src/strydrunpowersensor.h b/src/strydrunpowersensor.h index 177c5c451..fdff1592d 100644 --- a/src/strydrunpowersensor.h +++ b/src/strydrunpowersensor.h @@ -27,7 +27,7 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" + #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,10 +37,7 @@ class strydrunpowersensor : public treadmill { Q_OBJECT public: strydrunpowersensor(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); - bool connected(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool connected() override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false, @@ -49,7 +46,6 @@ class strydrunpowersensor : public treadmill { uint16_t watts(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; QList gattCommunicationChannelService; // QLowEnergyCharacteristic gattNotify1Characteristic; @@ -73,6 +69,8 @@ class strydrunpowersensor : public treadmill { uint16_t LastCrankEventTime = 0; double CrankRevs = 0; + bool powerReceived = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif @@ -100,5 +98,7 @@ class strydrunpowersensor : public treadmill { void update(); void error(QLowEnergyController::Error err); void errorService(QLowEnergyService::ServiceError); + + void changeInclinationRequested(double grade, double percentage); }; #endif // STRYDRUNPOWERSENSOR_H diff --git a/src/tacxneo2.cpp b/src/tacxneo2.cpp index 77a4685d8..9baa336c6 100644 --- a/src/tacxneo2.cpp +++ b/src/tacxneo2.cpp @@ -1,5 +1,4 @@ #include "tacxneo2.h" -#include "ios/lockscreen.h" #include "virtualbike.h" #include #include @@ -9,9 +8,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -38,11 +38,15 @@ void tacxneo2::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCustomService->writeCharacteristic(gattWriteCharCustomId, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -68,7 +72,13 @@ void tacxneo2::forceInclination(double inclination) { // Inclination = inclination; // this bike doesn't provide resistance, so i will put at the same value of the inclination #659 - Resistance = inclination; + QSettings settings; + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + if (tacx_neo2_peloton) + Resistance = inclination * 10; + else + Resistance = inclination; inclination += 200; inclination = inclination * 100; @@ -91,6 +101,11 @@ void tacxneo2::update() { if (initRequest) { initRequest = false; + QSettings settings; + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + if (tacx_neo2_peloton) + requestInclination = 0; } else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState //&& // gattCommunicationChannelService && @@ -106,14 +121,16 @@ void tacxneo2::update() { } if (requestResistance != -1) { - if (requestResistance != currentResistance().value()) { + if (requestResistance != currentResistance().value() || lastGearValue != gears()) { emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); + auto virtualBike = this->VirtualBike(); if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && (requestPower == 0 || requestPower == -1)) { requestInclination = requestResistance / 10.0; } // forceResistance(requestResistance);; } + lastGearValue = gears(); requestResistance = -1; } if (requestInclination != -100) { @@ -163,6 +180,8 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris QSettings settings; QString heartRateBeltName = settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); qDebug() << QStringLiteral(" << char ") << characteristic.uuid(); emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); @@ -204,12 +223,12 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris } else { index += 2; } - LastCrankEventTime = + uint16_t LastCrankEventTimeRead = (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); - int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime; + int16_t deltaT = LastCrankEventTimeRead - oldLastCrankEventTime; if (deltaT < 0) { - deltaT = LastCrankEventTime + 65535 - oldLastCrankEventTime; + deltaT = LastCrankEventTimeRead + 65535 - oldLastCrankEventTime; } if (CrankRevsRead != oldCrankRevs && deltaT) { @@ -222,10 +241,12 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris Cadence = 0; } - oldLastCrankEventTime = LastCrankEventTime; + oldLastCrankEventTime = LastCrankEventTimeRead; oldCrankRevs = CrankRevsRead; - Speed = Cadence.value() * settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio).toDouble(); + Speed = Cadence.value() * + settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio) + .toDouble(); Distance += ((Speed.value() / 3600000.0) * ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); @@ -234,29 +255,33 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris // (uint16_t)((uint8_t)newValue.at(index)))); debug("Current Resistance: " + // QString::number(Resistance.value())); - double ac = 0.01243107769; - double bc = 1.145964912; - double cc = -23.50977444; - - double ar = 0.1469553975; - double br = -5.841344538; - double cr = 97.62165482; - - m_pelotonResistance = - (((sqrt(pow(br, 2.0) - 4.0 * ar * - (cr - (m_watt.value() * 132.0 / - (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - - br) / - (2.0 * ar)) * - settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + - settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); - Resistance = m_pelotonResistance; + if (tacx_neo2_peloton) { + m_pelotonResistance = bikeResistanceToPeloton(Resistance.value()); + } else { + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + m_pelotonResistance = + (((sqrt(pow(br, 2.0) - 4.0 * ar * + (cr - (m_watt.value() * 132.0 / + (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + Resistance = m_pelotonResistance; + } emit resistanceRead(Resistance.value()); if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * - 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in @@ -290,14 +315,7 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris } #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && Heart.value() == 0) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } if (Cadence.value() > 0) { @@ -308,7 +326,8 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)currentHeart().value()); @@ -408,7 +427,7 @@ void tacxneo2::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -416,11 +435,14 @@ void tacxneo2::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -430,10 +452,11 @@ void tacxneo2::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService, 4, 1); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, 4, 1); connect(virtualBike, &virtualbike::changeInclination, this, &tacxneo2::changeInclination); // connect(virtualBike, &virtualbike::powerPacketReceived, this, &tacxneo2::powerPacketReceived); // connect(virtualBike, &virtualbike::debug, this, &tacxneo2::debug); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -536,10 +559,6 @@ bool tacxneo2::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *tacxneo2::VirtualBike() { return virtualBike; } - -void *tacxneo2::VirtualDevice() { return VirtualBike(); } - uint16_t tacxneo2::watts() { if (currentCadence().value() == 0) { return 0; @@ -556,3 +575,28 @@ void tacxneo2::controllerStateChanged(QLowEnergyController::ControllerState stat m_control->connectToDevice(); } } + +resistance_t tacxneo2::pelotonToBikeResistance(int pelotonResistance) { + for (resistance_t i = 0; i < max_resistance; i++) { + if (bikeResistanceToPeloton(i) <= pelotonResistance && bikeResistanceToPeloton(i + 1) > pelotonResistance) { + return i; + } + } + if (pelotonResistance < bikeResistanceToPeloton(1)) + return 0; + else + return max_resistance; +} + +double tacxneo2::bikeResistanceToPeloton(double resistance) { + QSettings settings; + bool tacx_neo2_peloton = + settings.value(QZSettings::tacx_neo2_peloton, QZSettings::default_tacx_neo2_peloton).toBool(); + + if (tacx_neo2_peloton) { + return (resistance * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + } else { + return resistance; + } +} diff --git a/src/tacxneo2.h b/src/tacxneo2.h index 4cb0fb385..458c12f45 100644 --- a/src/tacxneo2.h +++ b/src/tacxneo2.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -39,9 +38,7 @@ class tacxneo2 : public bike { tacxneo2(bool noWriteResistance, bool noHeartService); void changePower(int32_t power) override; bool connected() override; - - void *VirtualBike(); - void *VirtualDevice() override; + resistance_t pelotonToBikeResistance(int pelotonResistance) override; private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, @@ -49,9 +46,11 @@ class tacxneo2 : public bike { void startDiscover(); void forceInclination(double inclination); uint16_t watts() override; + double bikeResistanceToPeloton(double resistance); QTimer *refresh; - virtualbike *virtualBike = 0; + + const int max_resistance = 100; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; @@ -76,6 +75,8 @@ class tacxneo2 : public bike { uint16_t oldCrankRevs = 0; uint16_t CrankRevsRead = 0; + double lastGearValue = -1; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/technogymmyruntreadmill.cpp b/src/technogymmyruntreadmill.cpp index c6de2775c..c3523cbb3 100644 --- a/src/technogymmyruntreadmill.cpp +++ b/src/technogymmyruntreadmill.cpp @@ -1,7 +1,6 @@ #include "technogymmyruntreadmill.h" #include "ftmsbike.h" -#include "ios/lockscreen.h" #include "virtualtreadmill.h" #include #include @@ -12,9 +11,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -44,10 +44,15 @@ void technogymmyruntreadmill::writeCharacteristic(QLowEnergyService *service, QL timeout.singleShot(3000, &loop, SLOT(quit())); } - service->writeCharacteristic(characteristic, QByteArray((const char *)data, data_len), writeMode); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + service->writeCharacteristic(characteristic, *writeBuffer, writeMode); if (!disable_log) - qDebug() << " >> " << QByteArray((const char *)data, data_len).toHex(' ') << " // " << info; + qDebug() << " >> " << writeBuffer->toHex(' ') << " // " << info; loop.exec(); } @@ -171,7 +176,7 @@ void technogymmyruntreadmill::update() { requestSpeed = -1; } if (requestInclination != -100) { - if(requestInclination < 0) + if (requestInclination < 0) requestInclination = 0; if (requestInclination != currentInclination().value() && requestInclination >= 0 && requestInclination <= 15) { @@ -367,13 +372,16 @@ void technogymmyruntreadmill::characteristicChanged(const QLowEnergyCharacterist index += 1; } else { if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) - KCal += ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) * - settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / - 200.0) / - (60000.0 / - ((double)lastRefreshCharacteristicChanged.msecsTo( - QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in - // kg * 3.5) / 200 ) / 60 + KCal += + ((((0.048 * + ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / + ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 } emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); @@ -419,7 +427,10 @@ void technogymmyruntreadmill::characteristicChanged(const QLowEnergyCharacterist bool InstantaneousStrideLengthPresent = (flags & 0x01); bool TotalDistancePresent = (flags & 0x02) ? true : false; bool WalkingorRunningStatusbits = (flags & 0x04) ? true : false; - bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool(); + bool double_cadence = settings + .value(QZSettings::powr_sensor_running_cadence_double, + QZSettings::default_powr_sensor_running_cadence_double) + .toBool(); double cadence_multiplier = 1.0; if (double_cadence) cadence_multiplier = 2.0; @@ -435,20 +446,10 @@ void technogymmyruntreadmill::characteristicChanged(const QLowEnergyCharacterist } if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { - if (heart == 0.0 || settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + if (heart == 0.0 || + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { + update_hr_from_external(); } else { - Heart = heart; } } @@ -558,7 +559,7 @@ void technogymmyruntreadmill::stateChanged(QLowEnergyService::ServiceState state emit connectedAndDiscovered(); // ******************************************* virtual treadmill init ************************************* - if (!firstStateChanged && !virtualTreadmill && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -567,20 +568,25 @@ void technogymmyruntreadmill::stateChanged(QLowEnergyService::ServiceState state ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadmill = new virtualtreadmill(this, noHeartService); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); connect(virtualTreadmill, &virtualtreadmill::debug, this, &technogymmyruntreadmill::debug); connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &technogymmyruntreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &technogymmyruntreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } } firstStateChanged = 1; @@ -694,10 +700,6 @@ bool technogymmyruntreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *technogymmyruntreadmill::VirtualTreadmill() { return virtualTreadmill; } - -void *technogymmyruntreadmill::VirtualDevice() { return VirtualTreadmill(); } - void technogymmyruntreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/technogymmyruntreadmill.h b/src/technogymmyruntreadmill.h index 3aaeada27..ab5267aba 100644 --- a/src/technogymmyruntreadmill.h +++ b/src/technogymmyruntreadmill.h @@ -29,8 +29,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -40,14 +38,11 @@ class technogymmyruntreadmill : public treadmill { Q_OBJECT public: technogymmyruntreadmill(bool noWriteResistance, bool noHeartService); - bool connected(); + bool connected() override; void forceSpeed(double requestSpeed); void forceIncline(double requestIncline); - bool autoPauseWhenSpeedIsZero(); - bool autoStartWhenSpeedIsGreaterThenZero(); - - void *VirtualTreadmill(); - void *VirtualDevice(); + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; private: void writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic characteristic, uint8_t *data, @@ -58,8 +53,6 @@ class technogymmyruntreadmill : public treadmill { void btinit(); QTimer *refresh; - virtualtreadmill *virtualTreadmill = nullptr; - virtualbike *virtualBike = nullptr; QList gattCommunicationChannelService; QLowEnergyCharacteristic gattWriteCharControlPointId; diff --git a/src/technogymmyruntreadmillrfcomm.cpp b/src/technogymmyruntreadmillrfcomm.cpp index 4f65c61f6..8ae138883 100644 --- a/src/technogymmyruntreadmillrfcomm.cpp +++ b/src/technogymmyruntreadmillrfcomm.cpp @@ -1,4 +1,5 @@ #include "technogymmyruntreadmillrfcomm.h" +#include "virtualtreadmill.h" #include #include #include @@ -103,15 +104,16 @@ void technogymmyruntreadmillrfcomm::update() { if (initDone) { // ******************************************* virtual treadmill init ************************************* - if (!virtualTreadMill) { + if (!this->hasVirtualDevice()) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, true); + auto virtualTreadMill = new virtualtreadmill(this, true); connect(virtualTreadMill, &virtualtreadmill::debug, this, &technogymmyruntreadmillrfcomm::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &technogymmyruntreadmillrfcomm::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } } // ******************************************************************************************************** @@ -226,7 +228,3 @@ void technogymmyruntreadmillrfcomm::readSocket() { void technogymmyruntreadmillrfcomm::onSocketErrorOccurred(QBluetoothSocket::SocketError error) { emit debug(QStringLiteral("onSocketErrorOccurred ") + QString::number(error)); } - -void *technogymmyruntreadmillrfcomm::VirtualTreadMill() { return virtualTreadMill; } - -void *technogymmyruntreadmillrfcomm::VirtualDevice() { return VirtualTreadMill(); } diff --git a/src/technogymmyruntreadmillrfcomm.h b/src/technogymmyruntreadmillrfcomm.h index aaf07b6b5..366814b62 100644 --- a/src/technogymmyruntreadmillrfcomm.h +++ b/src/technogymmyruntreadmillrfcomm.h @@ -28,14 +28,11 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class technogymmyruntreadmillrfcomm : public treadmill { Q_OBJECT public: explicit technogymmyruntreadmillrfcomm(); - void *VirtualTreadMill(); - void *VirtualDevice(); public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); @@ -55,7 +52,6 @@ class technogymmyruntreadmillrfcomm : public treadmill { QBluetoothServiceInfo serialPortService; QBluetoothSocket *socket = nullptr; - virtualtreadmill *virtualTreadMill = nullptr; QTimer *refresh; bool initDone = false; diff --git a/src/templateinfosenderbuilder.cpp b/src/templateinfosenderbuilder.cpp index 0524686a0..6707472f1 100644 --- a/src/templateinfosenderbuilder.cpp +++ b/src/templateinfosenderbuilder.cpp @@ -19,6 +19,40 @@ using namespace std::chrono_literals; +#define TRAINPROGRAM_FIELD_TO_STRING() \ + item[QStringLiteral("duration")] = row.duration.toString(); \ + item[QStringLiteral("duration_s")] = QTime(0,0,0).secsTo(row.duration); \ + item[QStringLiteral("distance")] = row.distance; \ + item[QStringLiteral("speed")] = row.speed; \ + item[QStringLiteral("minspeed")] = row.minSpeed; \ + item[QStringLiteral("maxspeed")] = row.maxSpeed; \ + item[QStringLiteral("fanspeed")] = row.fanspeed; \ + item[QStringLiteral("inclination")] = row.inclination; \ + item[QStringLiteral("resistance")] = row.resistance; \ + item[QStringLiteral("maxresistance")] = row.maxResistance; \ + item[QStringLiteral("mets")] = row.mets; \ + item[QStringLiteral("pace_intensity")] = row.pace_intensity; \ + item[QStringLiteral("lower_resistance")] = row.lower_resistance; \ + item[QStringLiteral("upper_resistance")] = row.upper_resistance; \ + item[QStringLiteral("requested_peloton_resistance")] = row.requested_peloton_resistance; \ + item[QStringLiteral("lower_requested_peloton_resistance")] = row.lower_requested_peloton_resistance; \ + item[QStringLiteral("upper_requested_peloton_resistance")] = row.upper_requested_peloton_resistance; \ + item[QStringLiteral("power")] = row.power; \ + item[QStringLiteral("cadence")] = row.cadence; \ + item[QStringLiteral("lower_cadence")] = row.lower_cadence; \ + item[QStringLiteral("upper_cadence")] = row.upper_cadence; \ + item[QStringLiteral("forcespeed")] = row.forcespeed; \ + item[QStringLiteral("loopTimeHR")] = row.loopTimeHR; \ + item[QStringLiteral("zoneHR")] = row.zoneHR; \ + item[QStringLiteral("HRmin")] = row.HRmin; \ + item[QStringLiteral("HRmax")] = row.HRmax; \ + item[QStringLiteral("maxSpeed")] = row.maxSpeed; \ + item[QStringLiteral("latitude")] = row.latitude; \ + item[QStringLiteral("longitude")] = row.longitude; \ + item[QStringLiteral("altitude")] = row.altitude; \ + item[QStringLiteral("azimuth")] = row.azimuth; + + QHash TemplateInfoSenderBuilder::instanceMap; TemplateInfoSenderBuilder::TemplateInfoSenderBuilder(QObject *parent) : QObject(parent) { engine = new QJSEngine(this); @@ -428,21 +462,7 @@ void TemplateInfoSenderBuilder::onLoadTrainingPrograms(const QJsonValue &msgCont fileXml + QStringLiteral(".xml")); for (auto &row : lst) { QJsonObject item; - item[QStringLiteral("duration")] = row.duration.toString(); - item[QStringLiteral("speed")] = row.speed; - item[QStringLiteral("fanspeed")] = row.fanspeed; - item[QStringLiteral("inclination")] = row.inclination; - item[QStringLiteral("resistance")] = row.resistance; - item[QStringLiteral("requested_peloton_resistance")] = row.requested_peloton_resistance; - item[QStringLiteral("cadence")] = row.cadence; - item[QStringLiteral("forcespeed")] = row.forcespeed; - item[QStringLiteral("loopTimeHR")] = row.loopTimeHR; - item[QStringLiteral("zoneHR")] = row.zoneHR; - item[QStringLiteral("HRmin")] = row.HRmin; - item[QStringLiteral("HRmax")] = row.HRmax; - item[QStringLiteral("maxSpeed")] = row.maxSpeed; - item[QStringLiteral("latitude")] = row.latitude; - item[QStringLiteral("longitude")] = row.longitude; + TRAINPROGRAM_FIELD_TO_STRING(); outArr.append(item); } } @@ -454,6 +474,27 @@ void TemplateInfoSenderBuilder::onLoadTrainingPrograms(const QJsonValue &msgCont tempSender->send(out.toJson()); } +void TemplateInfoSenderBuilder::onGetTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender) { + QJsonObject main; + QJsonArray outArr; + QJsonObject outObj; + QString fileXml; + if (homeform::singleton() && homeform::singleton()->trainingProgram()) { + QList lst = homeform::singleton()->trainingProgram()->loadedRows; + for (auto &row : lst) { + QJsonObject item; + TRAINPROGRAM_FIELD_TO_STRING(); + outArr.append(item); + } + } + outObj[QStringLiteral("list")] = outArr; + outObj[QStringLiteral("name")] = fileXml; + main[QStringLiteral("content")] = outObj; + main[QStringLiteral("msg")] = QStringLiteral("R_gettrainingprogram"); + QJsonDocument out(main); + tempSender->send(out.toJson()); +} + void TemplateInfoSenderBuilder::onAppendActivityDescription(const QJsonValue &msgContent, TemplateInfoSender *tempSender) { QJsonObject content; @@ -734,6 +775,17 @@ void TemplateInfoSenderBuilder::onSaveChart(const QJsonValue &msgContent, Templa tempSender->send(out.toJson()); } +void TemplateInfoSenderBuilder::onGetPelotonImage(const QJsonValue &msgContent, TemplateInfoSender *tempSender) { + QJsonObject main; + QString base64 = ""; + if (homeform::singleton() && !homeform::singleton()->currentPelotonImage().isEmpty()) + base64 = homeform::singleton()->currentPelotonImage().toBase64(); + main[QStringLiteral("content")] = base64; + main[QStringLiteral("msg")] = QStringLiteral("R_getpelotonimage"); + QJsonDocument out(main); + tempSender->send(out.toJson()); +} + void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) { TemplateInfoSender *sender = qobject_cast(this->sender()); if (!sender) { @@ -782,6 +834,9 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) { } else if (msg == QStringLiteral("loadtrainingprograms")) { onLoadTrainingPrograms(jsonObject[QStringLiteral("content")], sender); return; + } else if (msg == QStringLiteral("gettrainingprogram")) { + onGetTrainingProgram(jsonObject[QStringLiteral("content")], sender); + return; } else if (msg == QStringLiteral("appendactivitydescription")) { onAppendActivityDescription(jsonObject[QStringLiteral("content")], sender); return; @@ -791,6 +846,9 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) { } else if (msg == QStringLiteral("savechart")) { onSaveChart(jsonObject[QStringLiteral("content")], sender); return; + } else if (msg == QStringLiteral("getpelotonimage")) { + onGetPelotonImage(jsonObject[QStringLiteral("content")], sender); + return; } else if (msg == QStringLiteral("lap")) { onLap(jsonObject[QStringLiteral("content")], sender); return; @@ -803,7 +861,7 @@ void TemplateInfoSenderBuilder::onDataReceived(const QByteArray &data) { } else if (msg == QStringLiteral("gears_plus")) { onGearsPlus(jsonObject[QStringLiteral("content")], sender); return; - } else if (msg == QStringLiteral("pelotonoffset_minus")) { + } else if (msg == QStringLiteral("gears_minus")) { onGearsMinus(jsonObject[QStringLiteral("content")], sender); return; } else if (msg == QStringLiteral("peloton_start_workout")) { @@ -923,6 +981,14 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("pace_s"), el.second()); obj.setProperty(QStringLiteral("pace_m"), el.minute()); obj.setProperty(QStringLiteral("pace_h"), el.hour()); + el = device->averagePace(); + obj.setProperty(QStringLiteral("avgpace_s"), el.second()); + obj.setProperty(QStringLiteral("avgpace_m"), el.minute()); + obj.setProperty(QStringLiteral("avgpace_h"), el.hour()); + el = device->maxPace(); + obj.setProperty(QStringLiteral("maxpace_s"), el.second()); + obj.setProperty(QStringLiteral("maxpace_m"), el.minute()); + obj.setProperty(QStringLiteral("maxpace_h"), el.hour()); el = device->movingTime(); obj.setProperty(QStringLiteral("moving_s"), el.second()); obj.setProperty(QStringLiteral("moving_m"), el.minute()); @@ -1012,6 +1078,10 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("req_resistance"), (dep = ((bike *)device)->lastRequestedResistance()).value()); } else if (tp == bluetoothdevice::ROWING) { + el = ((rower *)device)->lastRequestedPace(); + obj.setProperty(QStringLiteral("target_pace_s"), el.second()); + obj.setProperty(QStringLiteral("target_pace_m"), el.minute()); + obj.setProperty(QStringLiteral("target_pace_h"), el.hour()); obj.setProperty(QStringLiteral("peloton_resistance"), (dep = ((rower *)device)->pelotonResistance()).value()); obj.setProperty(QStringLiteral("peloton_resistance_avg"), dep.average()); @@ -1020,6 +1090,7 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("cadence_avg"), dep.average()); obj.setProperty(QStringLiteral("cadence_lapavg"), dep.lapAverage()); obj.setProperty(QStringLiteral("cadence_lapmax"), dep.lapMax()); + obj.setProperty(QStringLiteral("req_cadence"), (dep = ((rower *)device)->lastRequestedCadence()).value()); obj.setProperty(QStringLiteral("resistance"), (dep = ((rower *)device)->currentResistance()).value()); obj.setProperty(QStringLiteral("resistance_avg"), dep.average()); obj.setProperty(QStringLiteral("cranks"), ((rower *)device)->currentCrankRevolutions()); @@ -1028,6 +1099,10 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("strokeslength"), ((rower *)device)->currentStrokesLength().value()); } else if (tp == bluetoothdevice::TREADMILL) { obj.setProperty(QStringLiteral("target_speed"), ((treadmill *)device)->lastRequestedSpeed().value()); + el = ((treadmill *)device)->lastRequestedPace(); + obj.setProperty(QStringLiteral("target_pace_s"), el.second()); + obj.setProperty(QStringLiteral("target_pace_m"), el.minute()); + obj.setProperty(QStringLiteral("target_pace_h"), el.hour()); obj.setProperty(QStringLiteral("target_inclination"), ((treadmill *)device)->lastRequestedInclination().value()); obj.setProperty(QStringLiteral("cadence"), (dep = ((treadmill *)device)->currentCadence()).value()); diff --git a/src/templateinfosenderbuilder.h b/src/templateinfosenderbuilder.h index d7f705d02..4b3917038 100644 --- a/src/templateinfosenderbuilder.h +++ b/src/templateinfosenderbuilder.h @@ -67,6 +67,7 @@ class TemplateInfoSenderBuilder : public QObject { void onSetSpeed(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onSetDifficult(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onSaveChart(const QJsonValue &msgContent, TemplateInfoSender *tempSender); + void onGetPelotonImage(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onLap(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onPelotonOffsetPlus(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onPelotonOffsetMinus(const QJsonValue &msgContent, TemplateInfoSender *tempSender); @@ -78,6 +79,7 @@ class TemplateInfoSenderBuilder : public QObject { void onAutoresistance(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onSaveTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onLoadTrainingPrograms(const QJsonValue &msgContent, TemplateInfoSender *tempSender); + void onGetTrainingProgram(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onAppendActivityDescription(const QJsonValue &msgContent, TemplateInfoSender *tempSender); void onGetSessionArray(TemplateInfoSender *tempSender); void onGetLatLon(TemplateInfoSender *tempSender); diff --git a/src/toorxtreadmill.cpp b/src/toorxtreadmill.cpp index 5bad23b87..a73b687f7 100644 --- a/src/toorxtreadmill.cpp +++ b/src/toorxtreadmill.cpp @@ -1,4 +1,5 @@ #include "toorxtreadmill.h" +#include "virtualtreadmill.h" #include #include #include @@ -79,13 +80,14 @@ void toorxtreadmill::update() { if (initDone) { // ******************************************* virtual treadmill init ************************************* - if (!virtualTreadMill) { + if (!this->hasVirtualDevice()) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, true); + auto virtualTreadMill = new virtualtreadmill(this, true); connect(virtualTreadMill, &virtualtreadmill::debug, this, &toorxtreadmill::debug); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } } // ******************************************************************************************************** @@ -335,7 +337,3 @@ uint16_t toorxtreadmill::GetElapsedTimeFromPacket(const QByteArray &packet) { void toorxtreadmill::onSocketErrorOccurred(QBluetoothSocket::SocketError error) { emit debug(QStringLiteral("onSocketErrorOccurred ") + QString::number(error)); } - -void *toorxtreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *toorxtreadmill::VirtualDevice() { return VirtualTreadMill(); } diff --git a/src/toorxtreadmill.h b/src/toorxtreadmill.h index 031dd172e..206638e33 100644 --- a/src/toorxtreadmill.h +++ b/src/toorxtreadmill.h @@ -28,14 +28,11 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class toorxtreadmill : public treadmill { Q_OBJECT public: explicit toorxtreadmill(); - void *VirtualTreadMill(); - void *VirtualDevice(); public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); @@ -52,8 +49,6 @@ class toorxtreadmill : public treadmill { QBluetoothServiceInfo serialPortService; QBluetoothSocket *socket = nullptr; - virtualtreadmill *virtualTreadMill = nullptr; - QTimer *refresh; bool initDone = false; diff --git a/src/trainprogram.cpp b/src/trainprogram.cpp index ecaa743ff..71d9202b6 100644 --- a/src/trainprogram.cpp +++ b/src/trainprogram.cpp @@ -3,12 +3,17 @@ #include #include #include +#include #include #ifdef Q_OS_ANDROID #include "androidactivityresultreceiver.h" #include "keepawakehelper.h" #include +#elif defined(Q_OS_WINDOWS) +#include "windows_zwift_incline_paddleocr_thread.h" +#include "windows_zwift_workout_paddleocr_thread.h" #endif +#include "localipaddress.h" using namespace std::chrono_literals; @@ -49,6 +54,8 @@ trainprogram::trainprogram(const QList &rows, bluetooth *b, QString *d applySpeedFilter(); } + this->videoAvailable = videoAvailable; + connect(&timer, SIGNAL(timeout()), this, SLOT(scheduler())); timer.setInterval(1s); timer.start(); @@ -76,6 +83,7 @@ QString trainrow::toString() const { rv += QStringLiteral(" average_requested_peloton_resistance = %1") .arg(average_requested_peloton_resistance); // used for peloton rv += QStringLiteral(" upper_requested_peloton_resistance = %1").arg(upper_requested_peloton_resistance); + rv += QStringLiteral(" pace_intensity = %1").arg(pace_intensity); rv += QStringLiteral(" cadence = %1").arg(cadence); rv += QStringLiteral(" lower_cadence = %1").arg(lower_cadence); rv += QStringLiteral(" average_cadence = %1").arg(average_cadence); // used for peloton @@ -275,7 +283,7 @@ int trainprogram::TotalGPXSecs() { return QTime(0, 0, 0).secsTo(rows.at(rows.length() - 1).gpxElapsed); } -double trainprogram::TimeRateFromGPX(double gpxsecs, double videosecs, double currentspeed) { +double trainprogram::TimeRateFromGPX(double gpxsecs, double videosecs, double currentspeed, int recordingFactor) { // no rows available, return 1 if (rows.length() <= 0) { qDebug() << "TimeRateFromGPX no Rows"; @@ -316,7 +324,9 @@ double trainprogram::TimeRateFromGPX(double gpxsecs, double videosecs, double cu double avgSpeedForLimit = avgSpeedFromGpxStep(currentStep + 1, 5); if (avgSpeedForLimit > 0.0) { bike *dev = (bike *)bluetoothManager->device(); - dev->setSpeedLimit(avgSpeedForLimit * 3); + // bepo70: Replay allows Factor 2 max, so set the speed Limit to 2 * Video recording Factor speed to + // avoid any jumps in Video + dev->setSpeedLimit(avgSpeedForLimit * (double)recordingFactor * 2.0 / 3.0); } } if (gpxsecs == lastGpxRateSetAt) { @@ -347,6 +357,73 @@ double trainprogram::TimeRateFromGPX(double gpxsecs, double videosecs, double cu return rate; } +// Calculate the Median Inclination for a given Step. Median is built from the given Step -2 Steps and +2 Steps (5 Steps +// in total) +double trainprogram::medianInclination(int step) { + QList inclinations; + inclinations.reserve(5); + if (rows.length() == 0) + return 0; + if ((step > 1) && (rows.length() > step - 2)) + inclinations.append(rows.at(step - 2).inclination); + else + inclinations.append(0); + if ((step > 0) && (rows.length() > step - 1)) + inclinations.append(rows.at(step - 1).inclination); + else + inclinations.append(0); + if (rows.length() > step) + inclinations.append(rows.at(step).inclination); + else + inclinations.append(0); + if (rows.length() > step + 1) + inclinations.append(rows.at(step + 1).inclination); + else + inclinations.append(0); + if (rows.length() > step + 2) + inclinations.append(rows.at(step + 2).inclination); + else + inclinations.append(0); + std::sort(inclinations.begin(), inclinations.end()); + return (inclinations.at(2)); +} + +// Calculates a weighted Inclination for a given Step. Inclination is calculated for the given Step + windowsize Steps +// (7) The inclination for each Point needed goes through a Median Filter first to eliminate/minimize Errors in the +// recorded elevation Data +double trainprogram::weightedInclination(int step) { + int windowsize = 7; + int firststep = step; + double inc = 0; + double sumweights = 0; + double pointweight = 0; + if (rows.length() == 0) + return 0; + // Determine first and last possible Steps + if (firststep < 0) + firststep = 0; + int laststep = step + windowsize; + if (laststep >= rows.length()) { + firststep = rows.length() - 1 - (windowsize * 2); + if (firststep < 0) + firststep = 0; + } + // Loop through the determined Steps + for (int s = firststep; s <= laststep; s++) { + // Calculate the Weight used for the inclination + pointweight = ((((double)windowsize * 2.0) - 1.0) - ((s - firststep) * 2.0)); + // Calculate the sum of weights + sumweights = (sumweights + pointweight); + // Calculate the sum of weighted median inclinations + inc = (inc + (medianInclination(s)) * pointweight); + } + // avoid a Division by 0 + if (sumweights == 0) + return 0; + // Return the sum of weighted median inclinations / sum of all weights + return (inc / sumweights); +} + double trainprogram::avgInclinationNext100Meters(int step) { int c = step; double km = 0; @@ -430,16 +507,180 @@ void trainprogram::clearRows() { rows.clear(); } +void trainprogram::pelotonOCRprocessPendingDatagrams() { + qDebug() << "in !"; + QHostAddress sender; + QSettings settings; + uint16_t port; + while (pelotonOCRsocket->hasPendingDatagrams()) { + QByteArray datagram; + datagram.resize(pelotonOCRsocket->pendingDatagramSize()); + pelotonOCRsocket->readDatagram(datagram.data(), datagram.size(), &sender, &port); + qDebug() << "PelotonOCR Message From :: " << sender.toString(); + qDebug() << "PelotonOCR Port From :: " << port; + qDebug() << "PelotonOCR Message :: " << datagram; + + QString s = datagram; + pelotonOCRcomputeTime(s); + + QString url = "http://" + localipaddress::getIP(sender).toString() + ":" + + QString::number(settings.value("template_inner_QZWS_port", 6666).toInt()) + + "/floating/floating.htm"; + int r = pelotonOCRsocket->writeDatagram(QByteArray(url.toLatin1()), sender, 8003); + qDebug() << "url floating" << url << r; + } +} + +void trainprogram::pelotonOCRcomputeTime(QString t) { + static bool pelotonOCRcomputeTime_intro = false; + static bool pelotonOCRcomputeTime_syncing = false; + QRegularExpression re("\\d\\d:\\d\\d"); + QRegularExpressionMatch match = re.match(t.left(5)); + if (t.contains(QStringLiteral("INTRO")) || t.contains(QStringLiteral("UNTIL START"))) { + qDebug() << QStringLiteral("PELOTON OCR: SKIPPING INTRO, restarting training program"); + if (!pelotonOCRcomputeTime_intro) { + pelotonOCRcomputeTime_intro = true; + emit toastRequest("Peloton Syncing! Skipping intro..."); + } + restart(); + } else if (match.hasMatch()) { + int minutes = t.left(2).toInt(); + int seconds = t.left(5).right(2).toInt(); + seconds -= 1; //(due to the OCR delay) + seconds += minutes * 60; + QTime ocrRemaining = QTime(0, 0, 0, 0).addSecs(seconds); + QTime currentRemaining = remainingTime(); + qDebug() << QStringLiteral("PELOTON OCR USING: ocrRemaining") << ocrRemaining + << QStringLiteral("currentRemaining") << currentRemaining; + uint32_t abs = qAbs(ocrRemaining.secsTo(currentRemaining)); + if (abs < 120) { + qDebug() << QStringLiteral("PELOTON OCR SYNCING!"); + if (!pelotonOCRcomputeTime_syncing) { + pelotonOCRcomputeTime_syncing = true; + emit toastRequest("Peloton Syncing!"); + } + // applying the differences + if (ocrRemaining > currentRemaining) + decreaseElapsedTime(abs); + else + increaseElapsedTime(abs); + } + } +} + void trainprogram::scheduler() { QMutexLocker(&this->schedulerMutex); QSettings settings; + // outside the if case about a valid train program because the information for the floating window url should be + // sent anyway + if (settings.value(QZSettings::peloton_companion_workout_ocr, QZSettings::default_companion_peloton_workout_ocr) + .toBool()) { + if (!pelotonOCRsocket) { + pelotonOCRsocket = new QUdpSocket(this); + bool result = pelotonOCRsocket->bind(QHostAddress::AnyIPv4, 8003); + qDebug() << result; + pelotonOCRprocessPendingDatagrams(); + connect(pelotonOCRsocket, SIGNAL(readyRead()), this, SLOT(pelotonOCRprocessPendingDatagrams())); + } + } + if (rows.count() == 0 || started == false || enabled == false || bluetoothManager->device() == nullptr || (bluetoothManager->device()->currentSpeed().value() <= 0 && !settings.value(QZSettings::continuous_moving, QZSettings::default_continuous_moving).toBool()) || bluetoothManager->device()->isPaused()) { + // in case no workout has been selected + // Zwift OCR + if ((settings.value(QZSettings::zwift_ocr, QZSettings::default_zwift_ocr).toBool() || + settings.value(QZSettings::zwift_ocr_climb_portal, QZSettings::default_zwift_ocr_climb_portal).toBool()) && + bluetoothManager && bluetoothManager->device() && + (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL || + bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL)) { + +#ifdef Q_OS_ANDROID + { + QAndroidJniObject text = QAndroidJniObject::callStaticObjectMethod( + "org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastText"); + QString t = text.toString(); + QAndroidJniObject textExtended = QAndroidJniObject::callStaticObjectMethod( + "org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastTextExtended"); + // 2272 1027 + jint w = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/ScreenCaptureService", + "getImageWidth", "()I"); + jint h = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/ScreenCaptureService", + "getImageHeight", "()I"); + QString tExtended = textExtended.toString(); + QAndroidJniObject packageNameJava = QAndroidJniObject::callStaticObjectMethod( + "org/cagnulen/qdomyoszwift/MediaProjection", "getPackageName"); + QString packageName = packageNameJava.toString(); + if (packageName.contains("com.zwift.zwiftgame")) { + qDebug() << QStringLiteral("ZWIFT OCR ACCEPTED") << packageName << w << h << t << tExtended; + foreach (QString s, tExtended.split("§§")) { + // qDebug() << s; + QStringList ss = s.split("$$"); + if (ss.length() > 1) { + // (2195, 75 - 2254, 106)" + qDebug() << ss[0] << ss[1]; + QString inc = ss[1].replace("Rect(", "").replace(")", ""); + if (inc.split(",").length() > 2) { + int w_minbound = w * 0.93; + int h_minbound = h * 0.08; + int h_maxbound = h * 0.15; + int x = inc.split(",").at(0).toInt(); + int y = inc.split(",").at(2).toInt(); + qDebug() << x << w_minbound << h_maxbound << y << h_minbound; + if (x > w_minbound && y < h_maxbound && y > h_minbound) { + ss[0] = ss[0].replace("%", ""); + ss[0] = ss[0].replace("O", "0"); + ss[0] = ss[0].replace("l", "1"); + ss[0] = ss[0].replace(" ", ""); + if (ss[0].toInt() < 15 && ss[0].toInt() > -15) { + bluetoothManager->device()->changeInclination(ss[0].toInt(), ss[0].toInt()); + } else { + qDebug() << "filtering" << ss[0].toInt(); + } + } + } + } + } + + } else { + qDebug() << QStringLiteral("ZWIFT OCR IGNORING") << packageName << t; + } + } +#elif defined(Q_OS_WINDOWS) + static windows_zwift_incline_paddleocr_thread *windows_zwift_ocr_thread = nullptr; + if (!windows_zwift_ocr_thread) { + windows_zwift_ocr_thread = new windows_zwift_incline_paddleocr_thread(bluetoothManager->device()); + connect(windows_zwift_ocr_thread, &windows_zwift_incline_paddleocr_thread::debug, bluetoothManager, + &bluetooth::debug); + connect(windows_zwift_ocr_thread, &windows_zwift_incline_paddleocr_thread::onInclination, this, + &trainprogram::changeInclination); + windows_zwift_ocr_thread->start(); + } +#endif + } else if (settings.value(QZSettings::zwift_workout_ocr, QZSettings::default_zwift_workout_ocr).toBool() && + bluetoothManager && bluetoothManager->device() && + (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL || + bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL)) { +#ifdef Q_OS_WINDOWS + static windows_zwift_workout_paddleocr_thread *windows_zwift_workout_ocr_thread = nullptr; + if (!windows_zwift_workout_ocr_thread) { + windows_zwift_workout_ocr_thread = + new windows_zwift_workout_paddleocr_thread(bluetoothManager->device()); + connect(windows_zwift_workout_ocr_thread, &windows_zwift_workout_paddleocr_thread::debug, + bluetoothManager, &bluetooth::debug); + connect(windows_zwift_workout_ocr_thread, &windows_zwift_workout_paddleocr_thread::onInclination, this, + &trainprogram::changeInclination); + connect(windows_zwift_workout_ocr_thread, &windows_zwift_workout_paddleocr_thread::onSpeed, this, + &trainprogram::changeSpeed); + windows_zwift_workout_ocr_thread->start(); + } +#endif + } + return; } @@ -453,30 +694,7 @@ void trainprogram::scheduler() { QString packageName = packageNameJava.toString(); if (packageName.contains("com.onepeloton.callisto")) { qDebug() << QStringLiteral("PELOTON OCR ACCEPTED") << packageName << t; - QRegularExpression re("\\d\\d:\\d\\d"); - QRegularExpressionMatch match = re.match(t.left(5)); - if (t.contains(QStringLiteral("INTRO"))) { - qDebug() << QStringLiteral("PELOTON OCR: SKIPPING INTRO, restarting training program"); - restart(); - } else if (match.hasMatch()) { - int minutes = t.left(2).toInt(); - int seconds = t.left(5).right(2).toInt(); - seconds -= 1; //(due to the OCR delay) - seconds += minutes * 60; - QTime ocrRemaining = QTime(0, 0, 0, 0).addSecs(seconds); - QTime currentRemaining = remainingTime(); - qDebug() << QStringLiteral("PELOTON OCR USING: ocrRemaining") << ocrRemaining - << QStringLiteral("currentRemaining") << currentRemaining; - uint32_t abs = qAbs(ocrRemaining.secsTo(currentRemaining)); - if (abs < 120) { - qDebug() << QStringLiteral("PELOTON OCR SYNCING!"); - // applying the differences - if (ocrRemaining > currentRemaining) - decreaseElapsedTime(abs); - else - increaseElapsedTime(abs); - } - } + pelotonOCRcomputeTime(t); } else { qDebug() << QStringLiteral("PELOTON OCR IGNORING") << packageName << t; } @@ -507,6 +725,23 @@ void trainprogram::scheduler() { emit changeInclination(inc, inc); emit changeNextInclination300Meters(avgInclinationNext300Meters()); } + } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { + if (rows.at(0).forcespeed && rows.at(0).speed) { + qDebug() << QStringLiteral("trainprogram change speed") + QString::number(rows.at(0).speed); + emit changeSpeed(rows.at(0).speed); + } + if (rows.at(0).cadence != -1) { + qDebug() << QStringLiteral("trainprogram change cadence") + QString::number(rows.at(0).cadence); + emit changeCadence(rows.at(0).cadence); + } + if (rows.at(0).power != -1) { + qDebug() << QStringLiteral("trainprogram change power") + QString::number(rows.at(0).power); + emit changePower(rows.at(0).power); + } + if (rows.at(0).resistance != -1) { + qDebug() << QStringLiteral("trainprogram change resistance") + QString::number(rows.at(0).resistance); + emit changeResistance(rows.at(0).resistance); + } } else { if (rows.at(0).resistance != -1) { qDebug() << QStringLiteral("trainprogram change resistance") + QString::number(rows.at(0).resistance); @@ -628,6 +863,33 @@ void trainprogram::scheduler() { emit changeInclination(inc, inc); emit changeNextInclination300Meters(avgInclinationNext300Meters()); } + } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { + if (rows.at(currentStep).forcespeed && rows.at(currentStep).speed) { + qDebug() << QStringLiteral("trainprogram change speed ") + + QString::number(rows.at(currentStep).speed); + double speed; + if (!isnan(rows.at(currentStep).latitude) && !isnan(rows.at(currentStep).longitude)) { + speed = avgSpeedFromGpxStep(currentStep, 60); + } else { + speed = rows.at(currentStep).speed; + } + emit changeSpeed(speed); + } + if (rows.at(currentStep).cadence != -1) { + qDebug() << QStringLiteral("trainprogram change cadence ") + + QString::number(rows.at(currentStep).cadence); + emit changeCadence(rows.at(currentStep).cadence); + } + if (rows.at(currentStep).power != -1) { + qDebug() << QStringLiteral("trainprogram change power ") + + QString::number(rows.at(currentStep).power); + emit changePower(rows.at(currentStep).power); + } + if (rows.at(currentStep).resistance != -1) { + qDebug() << QStringLiteral("trainprogram change resistance ") + + QString::number(rows.at(currentStep).resistance); + emit changeResistance(rows.at(currentStep).resistance); + } } else { if (rows.at(currentStep).resistance != -1) { qDebug() << QStringLiteral("trainprogram change resistance ") + @@ -743,6 +1005,10 @@ void trainprogram::scheduler() { if (rows.at(currentStep).inclination != -200 && (!isnan(rows.at(currentStep).latitude) && !isnan(rows.at(currentStep).longitude))) { double inc = avgInclinationNext100Meters(currentStep); + // if Bike used and it is a gpx with Video use the new weightedInclination + if ((videoAvailable) && (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE)) { + inc = weightedInclination(currentStep); + } double bikeResistanceOffset = settings.value(QZSettings::bike_resistance_offset, QZSettings::default_bike_resistance_offset) .toInt(); @@ -885,6 +1151,9 @@ bool trainprogram::saveXML(const QString &filename, const QList &rows) stream.writeAttribute(QStringLiteral("upper_requested_peloton_resistance"), QString::number(row.upper_requested_peloton_resistance)); } + if (row.pace_intensity >= 0) { + stream.writeAttribute(QStringLiteral("pace_intensity"), QString::number(row.pace_intensity)); + } if (row.cadence >= 0) { stream.writeAttribute(QStringLiteral("cadence"), QString::number(row.cadence)); } @@ -1008,6 +1277,9 @@ QList trainprogram::loadXML(const QString &filename) { row.upper_requested_peloton_resistance = atts.value(QStringLiteral("upper_requested_peloton_resistance")).toInt(); } + if (atts.hasAttribute(QStringLiteral("pace_intensity"))) { + row.pace_intensity = atts.value(QStringLiteral("pace_intensity")).toInt(); + } if (atts.hasAttribute(QStringLiteral("cadence"))) { row.cadence = atts.value(QStringLiteral("cadence")).toInt(); } diff --git a/src/trainprogram.h b/src/trainprogram.h index 8d8fe4d66..a6d1a4410 100644 --- a/src/trainprogram.h +++ b/src/trainprogram.h @@ -29,6 +29,7 @@ class trainrow { int8_t lower_requested_peloton_resistance = -1; int8_t average_requested_peloton_resistance = -1; // used for peloton int8_t upper_requested_peloton_resistance = -1; + int8_t pace_intensity = -1; // used for peloton int16_t cadence = -1; int16_t lower_cadence = -1; int16_t average_cadence = -1; // used for peloton @@ -78,15 +79,25 @@ class trainprogram : public QObject { int32_t offsetElapsedTime() { return offset; } void clearRows(); double avgSpeedFromGpxStep(int gpxStep, int seconds); - double TimeRateFromGPX(double gpxsecs, double videosecs, double currentspeed); + double TimeRateFromGPX(double gpxsecs, double videosecs, double currentspeed, int recordingFactor); int TotalGPXSecs(); + double weightedInclination(int step); + double medianInclination(int step); bool overridePowerForCurrentRow(double power); + bool powerzoneWorkout() { + foreach(trainrow r, rows) { + if(r.power != -1) return true; + } + return false; + } QList rows; QList loadedRows; // rows as loaded QString description = ""; QString tags = ""; bool enabled = true; + bool videoAvailable = false; + void setVideoAvailable(bool v) {videoAvailable = v;} void restart(); bool isStarted() { return started; } @@ -98,6 +109,9 @@ class trainprogram : public QObject { void onTapeStarted(); void scheduler(); +private slots: + void pelotonOCRprocessPendingDatagrams(); + signals: void start(); void stop(bool paused); @@ -113,6 +127,7 @@ class trainprogram : public QObject { void changeSpeedAndInclination(double speed, double inclination); void changeGeoPosition(QGeoCoordinate p, double azimuth, double avgAzimuthNext300Meters); void changeTimestamp(QTime source, QTime actual); + void toastRequest(QString message); private: mutable QRecursiveMutex schedulerMutex; @@ -137,6 +152,9 @@ class trainprogram : public QObject { int lastStepTimestampChanged = 0; double lastCurrentStepDistance = 0.0; QTime lastCurrentStepTime = QTime(0, 0, 0); + + QUdpSocket* pelotonOCRsocket = nullptr; + void pelotonOCRcomputeTime(QString t); }; #endif // TRAINPROGRAM_H diff --git a/src/treadmill.cpp b/src/treadmill.cpp index 5a04fce11..ab30f8a36 100644 --- a/src/treadmill.cpp +++ b/src/treadmill.cpp @@ -1,5 +1,10 @@ #include "treadmill.h" +#ifdef Q_OS_ANDROID +#include +#endif +#ifdef Q_OS_IOS #include "ios/lockscreen.h" +#endif #include treadmill::treadmill() {} @@ -40,6 +45,8 @@ void treadmill::update_metrics(bool watt_calc, const double watts) { bool power_as_treadmill = settings.value(QZSettings::power_sensor_as_treadmill, QZSettings::default_power_sensor_as_treadmill).toBool(); + simulateInclinationWithSpeed(); + if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) .toString() .startsWith(QStringLiteral("Disabled")) == false && @@ -79,21 +86,29 @@ void treadmill::update_metrics(bool watt_calc, const double watts) { _firstUpdate = false; } -uint16_t treadmill::watts(double weight) { - - // calc Watts ref. https://alancouzens.com/blog/Run_Power.html - +uint16_t treadmill::wattsCalc(double weight, double speed, double inclination) { uint16_t watts = 0; - if (currentSpeed().value() > 0) { - - double pace = 60 / currentSpeed().value(); + if (speed > 0) { + // calc Watts ref. https://alancouzens.com/blog/Run_Power.html + double pace = 60 / speed; double VO2R = 210.0 / pace; double VO2A = (VO2R * weight) / 1000.0; double hwatts = 75 * VO2A; - double vwatts = ((9.8 * weight) * (currentInclination().value() / 100.0)); + double vwatts = ((9.8 * weight) * (inclination / 100.0)); watts = hwatts + vwatts; } - m_watt.setValue(watts); + return watts; +} + +uint16_t treadmill::watts(double weight) { + QSettings settings; + bool power_sensor = !(settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))); + if(!power_sensor) { + uint16_t watts = wattsCalc(weight, currentSpeed().value(), currentInclination().value()); + m_watt.setValue(watts); + } return m_watt.value(); } @@ -169,8 +184,37 @@ void treadmill::verticalOscillationSensor(double verticalOscillation) { VerticalOscillationMM.setValue(verticalOscillation); } +double treadmill::treadmillInclinationOverrideReverse(double Inclination) { + for (int i = 0; i <= 15 * 2; i++) { + if (treadmillInclinationOverride(((double)(i)) / 2.0) <= Inclination && + treadmillInclinationOverride(((double)(i + 1)) / 2.0) > Inclination) { + qDebug() << QStringLiteral("treadmillInclinationOverrideReverse") + << treadmillInclinationOverride(((double)(i)) / 2.0) + << treadmillInclinationOverride(((double)(i + 1)) / 2.0) << Inclination << i; + return ((double)i) / 2.0; + } + } + if (Inclination < treadmillInclinationOverride(0)) + return 0; + else + return 15; +} + double treadmill::treadmillInclinationOverride(double Inclination) { QSettings settings; + + double treadmill_inclination_ovveride_gain = settings + .value(QZSettings::treadmill_inclination_ovveride_gain, + QZSettings::default_treadmill_inclination_ovveride_gain) + .toDouble(); + double treadmill_inclination_ovveride_offset = settings + .value(QZSettings::treadmill_inclination_ovveride_offset, + QZSettings::default_treadmill_inclination_ovveride_offset) + .toDouble(); + + Inclination = Inclination * treadmill_inclination_ovveride_gain; + Inclination = Inclination + treadmill_inclination_ovveride_offset; + int inc = Inclination * 10; qDebug() << "treadmillInclinationOverride" << Inclination << inc; switch (inc) { @@ -314,12 +358,16 @@ double treadmill::treadmillInclinationOverride(double Inclination) { } void treadmill::cadenceFromAppleWatch() { + QSettings settings; #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - QSettings settings; - if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) - .toString() - .startsWith(QStringLiteral("Disabled"))) { + if (settings.value(QZSettings::garmin_companion, QZSettings::default_garmin_companion).toBool()) { + lockscreen h; + Cadence = h.getFootCad(); + qDebug() << QStringLiteral("Current Garmin Cadence: ") << QString::number(Cadence.value()); + } else if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) { lockscreen h; long appleWatchCadence = h.stepCadence(); Cadence = appleWatchCadence; @@ -327,4 +375,53 @@ void treadmill::cadenceFromAppleWatch() { } #endif #endif + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::garmin_companion, QZSettings::default_garmin_companion).toBool()) { + Cadence = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Garmin", "getFootCad", "()I"); + qDebug() << QStringLiteral("Current Garmin Cadence: ") << QString::number(Cadence.value()); + } +#endif +} + +bool treadmill::simulateInclinationWithSpeed() { + QSettings settings; + bool treadmill_simulate_inclination_with_speed = + settings + .value(QZSettings::treadmill_simulate_inclination_with_speed, + QZSettings::default_treadmill_simulate_inclination_with_speed) + .toBool(); + double w = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + if (treadmill_simulate_inclination_with_speed) { + if (requestInclination != -100) { + qDebug() << QStringLiteral("treadmill_simulate_inclination_with_speed enabled!") << requestInclination + << requestSpeed << m_lastRawSpeedRequested; + if (requestSpeed != -1) { + requestSpeed = + wattsCalc(w, requestSpeed, requestInclination) * requestSpeed / wattsCalc(w, requestSpeed, 0); + } else if (m_lastRawSpeedRequested != -1) { + requestSpeed = wattsCalc(w, m_lastRawSpeedRequested, requestInclination) * m_lastRawSpeedRequested / + wattsCalc(w, m_lastRawSpeedRequested, 0); + } + } + requestInclination = -100; + return true; + } + return false; +} + +QTime treadmill::lastRequestedPace() { + QSettings settings; + bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool(); + double unit_conversion = 1.0; + if (miles) { + unit_conversion = 0.621371; + } + if (lastRequestedSpeed().value() == 0) { + return QTime(0, 0, 0, 0); + } else { + double speed = lastRequestedSpeed().value() * unit_conversion; + return QTime(0, (int)(1.0 / (speed / 60.0)), + (((double)(1.0 / (speed / 60.0)) - ((double)((int)(1.0 / (speed / 60.0))))) * 60.0), 0); + } } diff --git a/src/treadmill.h b/src/treadmill.h index 13c06b001..8a4b775c2 100644 --- a/src/treadmill.h +++ b/src/treadmill.h @@ -10,9 +10,10 @@ class treadmill : public bluetoothdevice { treadmill(); void update_metrics(bool watt_calc, const double watts); metric lastRequestedSpeed() { return RequestedSpeed; } + QTime lastRequestedPace(); metric lastRequestedInclination() { return RequestedInclination; } - virtual bool connected(); - virtual metric currentInclination(); + bool connected() override; + metric currentInclination() override; virtual double requestedSpeed(); virtual double currentTargetSpeed(); virtual double requestedInclination(); @@ -24,10 +25,11 @@ class treadmill : public bluetoothdevice { metric currentVerticalOscillation() { return VerticalOscillationMM; } metric currentStepCount() { return StepCount; } uint16_t watts(double weight); - bluetoothdevice::BLUETOOTH_TYPE deviceType(); - void clearStats(); - void setLap(); - void setPaused(bool p); + static uint16_t wattsCalc(double weight, double speed, double inclination); + bluetoothdevice::BLUETOOTH_TYPE deviceType() override; + void clearStats() override; + void setLap() override; + void setPaused(bool p) override; double lastRawSpeedRequested() { return (m_lastRawSpeedRequested != -1 ? m_lastRawSpeedRequested : currentSpeed().value()); } @@ -39,18 +41,19 @@ class treadmill : public bluetoothdevice { virtual bool autoPauseWhenSpeedIsZero(); virtual bool autoStartWhenSpeedIsGreaterThenZero(); static double treadmillInclinationOverride(double Inclination); + static double treadmillInclinationOverrideReverse(double Inclination); void cadenceFromAppleWatch(); public slots: virtual void changeSpeed(double speed); - virtual void changeInclination(double grade, double percentage); + void changeInclination(double grade, double percentage) override; virtual void changeSpeedAndInclination(double speed, double inclination); - virtual void cadenceSensor(uint8_t cadence); - virtual void powerSensor(uint16_t power); - virtual void speedSensor(double speed); - virtual void instantaneousStrideLengthSensor(double length); - virtual void groundContactSensor(double groundContact); - virtual void verticalOscillationSensor(double verticalOscillation); + void cadenceSensor(uint8_t cadence) override; + void powerSensor(uint16_t power) override; + void speedSensor(double speed) override; + void instantaneousStrideLengthSensor(double length) override; + void groundContactSensor(double groundContact) override; + void verticalOscillationSensor(double verticalOscillation) override; signals: void tapeStarted(); @@ -70,6 +73,9 @@ class treadmill : public bluetoothdevice { double m_lastRawSpeedRequested = -1; double m_lastRawInclinationRequested = -100; bool instantaneousStrideLengthCMAvailableFromDevice = false; + + private: + bool simulateInclinationWithSpeed(); }; #endif // TREADMILL_H diff --git a/src/truetreadmill.cpp b/src/truetreadmill.cpp index 27ff164ba..edc6fc829 100644 --- a/src/truetreadmill.cpp +++ b/src/truetreadmill.cpp @@ -1,6 +1,8 @@ #include "truetreadmill.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif +#include "virtualbike.h" #include "virtualtreadmill.h" #include #include @@ -55,24 +57,23 @@ void truetreadmill::update() { QSettings settings; // ******************************************* virtual treadmill init ************************************* - if (!firstInit && !virtualTreadMill && !virtualBike) { - bool virtual_device_enabled = - settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); - bool virtual_device_force_bike = - settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) - .toBool(); + if (!firstInit && !this->hasVirtualDevice()) { + bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool(); if (virtual_device_enabled) { if (!virtual_device_force_bike) { debug("creating virtual treadmill interface..."); - virtualTreadMill = new virtualtreadmill(this, noHeartService); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); connect(virtualTreadMill, &virtualtreadmill::debug, this, &truetreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &truetreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } else { debug("creating virtual bike interface..."); - virtualBike = new virtualbike(this); + auto virtualBike = new virtualbike(this); connect(virtualBike, &virtualbike::changeInclination, this, &truetreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); } firstInit = 1; } @@ -147,7 +148,7 @@ void truetreadmill::characteristicChanged(const QLowEnergyCharacteristic &charac Q_UNUSED(characteristic); QByteArray avalue = newValue; - emit debug(QStringLiteral(" << ") + QString::number(avalue.length()) + QStringLiteral(" ") + avalue.toHex(' ')); + emit debug(QStringLiteral(" << ") + QString::number(newValue.length()) + QStringLiteral(" ") + newValue.toHex(' ')); #ifdef Q_OS_ANDROID if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) @@ -159,20 +160,10 @@ void truetreadmill::characteristicChanged(const QLowEnergyCharacteristic &charac uint8_t heart = 0; if (heart == 0 || disable_hr_frommachinery) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif - } else - + update_hr_from_external(); + } else { Heart = heart; + } } } @@ -180,30 +171,39 @@ void truetreadmill::characteristicChanged(const QLowEnergyCharacteristic &charac double speed = 0; - if (avalue.length() == 16) { - uint16_t convertedData = (avalue.at(7) << 8) | ((uint8_t)avalue.at(6)); - speed = ((double)convertedData) / 100.0; - double incline = ((double)(avalue.at(14))) / 10.0; + if (assault_treadmill) { + if (avalue.length() == 16) { + uint16_t convertedData = (avalue.at(6) << 8) | ((uint8_t)avalue.at(5)); + speed = ((double)convertedData) / 100.0; + } else { + return; + } + } else { + if (avalue.length() == 16) { + uint16_t convertedData = (avalue.at(7) << 8) | ((uint8_t)avalue.at(6)); + speed = ((double)convertedData) / 100.0; + double incline = ((double)(avalue.at(14))) / 10.0; - if (Inclination.value() != incline) { + if (Inclination.value() != incline) { - emit inclinationChanged(0, incline); - } - Inclination = incline; - emit debug(QStringLiteral("Current incline: ") + QString::number(incline)); + emit inclinationChanged(0, incline); + } + Inclination = incline; + emit debug(QStringLiteral("Current incline: ") + QString::number(incline)); - } else if (avalue.length() == 19) { - uint16_t convertedData = (avalue.at(8) << 8) | ((uint8_t)avalue.at(7)); - speed = ((double)convertedData) / 100.0; - } else if (avalue.length() == 4) { - double incline = ((double)(avalue.at(2))) / 10.0; - if (Inclination.value() != incline) { + } else if (avalue.length() == 19) { + uint16_t convertedData = (avalue.at(8) << 8) | ((uint8_t)avalue.at(7)); + speed = ((double)convertedData) / 100.0; + } else if (avalue.length() == 4) { + double incline = ((double)(avalue.at(2))) / 10.0; + if (Inclination.value() != incline) { - emit inclinationChanged(0, incline); + emit inclinationChanged(0, incline); + } + Inclination = incline; + emit debug(QStringLiteral("Current incline: ") + QString::number(incline)); + return; } - Inclination = incline; - emit debug(QStringLiteral("Current incline: ") + QString::number(incline)); - return; } if (!firstCharacteristicChanged) { @@ -315,6 +315,10 @@ void truetreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { { bluetoothDevice = device; + if (device.name().toUpper().startsWith(QStringLiteral("ASSAULT TREADMILL "))) { + assault_treadmill = true; + qDebug() << QStringLiteral("ASSAULT TREADMILL enabled!"); + } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &truetreadmill::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &truetreadmill::serviceScanDone); @@ -366,6 +370,3 @@ bool truetreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *truetreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *truetreadmill::VirtualDevice() { return VirtualTreadMill(); } diff --git a/src/truetreadmill.h b/src/truetreadmill.h index ed965cef8..edb8d3dcf 100644 --- a/src/truetreadmill.h +++ b/src/truetreadmill.h @@ -28,8 +28,6 @@ #include #include "treadmill.h" -#include "virtualbike.h" -#include "virtualtreadmill.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -41,11 +39,8 @@ class truetreadmill : public treadmill { public: truetreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, double forceInitSpeed = 0.0, double forceInitInclination = 0.0); - bool connected(); - - void *VirtualTreadMill(); - void *VirtualDevice(); - virtual bool canStartStop() { return false; } + bool connected() override; + bool canStartStop() override { return false; } private: void startDiscover(); @@ -59,8 +54,6 @@ class truetreadmill : public treadmill { bool firstCharacteristicChanged = true; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; - virtualbike *virtualBike = 0; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattNotifyCharacteristic; @@ -68,6 +61,8 @@ class truetreadmill : public treadmill { bool initDone = false; bool initRequest = false; + bool assault_treadmill = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/trxappgateusbbike.cpp b/src/trxappgateusbbike.cpp index 8630d8b7e..ce0bcec40 100644 --- a/src/trxappgateusbbike.cpp +++ b/src/trxappgateusbbike.cpp @@ -1,5 +1,7 @@ #include "trxappgateusbbike.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include @@ -41,16 +43,19 @@ void trxappgateusbbike::writeCharacteristic(uint8_t *data, uint8_t data_len, con timeout.singleShot(300ms, &loop, &QEventLoop::quit); } + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) - gattCommunicationChannelService->writeCharacteristic( - gattWriteCharacteristic, QByteArray((const char *)data, data_len), QLowEnergyService::WriteWithoutResponse); + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); else - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -253,22 +258,7 @@ void trxappgateusbbike::characteristicChanged(const QLowEnergyCharacteristic &ch speed = cadence * 0.37407407407407407407407407407407; if (Heart.value() > 0) { - int avgP = ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() * - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()) - - (settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble() * - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble())) / - (settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble() - - settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble()) + - (Heart.value() * - ((settings.value(QZSettings::power_hr_pwr1, QZSettings::default_power_hr_pwr1).toDouble() - - settings.value(QZSettings::power_hr_pwr2, QZSettings::default_power_hr_pwr2).toDouble()) / - (settings.value(QZSettings::power_hr_hr1, QZSettings::default_power_hr_hr1).toDouble() - - settings.value(QZSettings::power_hr_hr2, QZSettings::default_power_hr_hr2).toDouble()))); - if (Speed.value() > 0) { - watt = avgP; - } else { - watt = 0; - } + watt = wattFromHR(true); if (watt) kcal = @@ -298,19 +288,8 @@ void trxappgateusbbike::characteristicChanged(const QLowEnergyCharacteristic &ch } if (heart == 0.0 || settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool()) { - -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } else { - Heart = heart; } } @@ -509,7 +488,7 @@ void trxappgateusbbike::btinit(bool startTape) { writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true); writeCharacteristic((uint8_t *)initData5, sizeof(initData5), QStringLiteral("init"), false, true); writeCharacteristic((uint8_t *)initData6, sizeof(initData6), QStringLiteral("init"), false, true); - } else if (bike_type == TYPE::HERTZ_XR_770 || bike_type == TYPE::HERTZ_XR_770_2) { + } else if (bike_type == TYPE::HERTZ_XR_770 || bike_type == TYPE::HERTZ_XR_770_2 || bike_type == TYPE::FITHIWAY) { const uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x01, 0x92}; const uint8_t initData2[] = {0xf0, 0xa0, 0x02, 0x01, 0x93}; const uint8_t initData3[] = {0xf0, 0xa3, 0x01, 0x01, 0x01, 0x96}; @@ -749,7 +728,8 @@ void trxappgateusbbike::stateChanged(QLowEnergyService::ServiceState state) { if (bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE || bike_type == TYPE::JLL_IC400 || bike_type == TYPE::DKN_MOTION_2 || bike_type == TYPE::FYTTER_RI08 || - bike_type == TYPE::HERTZ_XR_770_2 || bike_type == TYPE::VIRTUFIT_2 || bike_type == TYPE::TUNTURI) { + bike_type == TYPE::HERTZ_XR_770_2 || bike_type == TYPE::VIRTUFIT_2 || bike_type == TYPE::TUNTURI || + bike_type == TYPE::FITHIWAY) { uuidWrite = QStringLiteral("49535343-8841-43f4-a8d4-ecbe34729bb3"); uuidNotify1 = QStringLiteral("49535343-1E4D-4BD9-BA61-23C647249616"); uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e"); @@ -786,7 +766,7 @@ void trxappgateusbbike::stateChanged(QLowEnergyService::ServiceState state) { &trxappgateusbbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstVirtualBike && !virtualBike) { + if (!firstVirtualBike && !this->hasVirtualDevice()) { QSettings settings; bool virtual_device_enabled = @@ -794,10 +774,11 @@ void trxappgateusbbike::stateChanged(QLowEnergyService::ServiceState state) { if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&trxappgateusbbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &trxappgateusbbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstVirtualBike = 1; @@ -835,7 +816,8 @@ void trxappgateusbbike::serviceScanDone(void) { QString uuid2 = QStringLiteral("49535343-FE7D-4AE5-8FA9-9FAFD205E455"); QString uuid3 = QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb"); if (bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW || bike_type == TYPE::ICONSOLE || - bike_type == TYPE::JLL_IC400 || bike_type == TYPE::FYTTER_RI08 || bike_type == TYPE::TUNTURI) { + bike_type == TYPE::JLL_IC400 || bike_type == TYPE::FYTTER_RI08 || bike_type == TYPE::TUNTURI || + bike_type == TYPE::FITHIWAY) { uuid = uuid2; } @@ -1002,6 +984,9 @@ void trxappgateusbbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { bike_type = TYPE::DKN_MOTION; qDebug() << QStringLiteral("DKN MOTION bike found"); + } else if (device.name().toUpper().startsWith(QStringLiteral("FITHIWAY"))) { + bike_type = TYPE::FITHIWAY; + qDebug() << QStringLiteral("FITHIWAY bike found"); } bluetoothDevice = device; @@ -1054,10 +1039,6 @@ bool trxappgateusbbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *trxappgateusbbike::VirtualBike() { return virtualBike; } - -void *trxappgateusbbike::VirtualDevice() { return VirtualBike(); } - void trxappgateusbbike::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/trxappgateusbbike.h b/src/trxappgateusbbike.h index 333d73dd9..1bf1dfa27 100644 --- a/src/trxappgateusbbike.h +++ b/src/trxappgateusbbike.h @@ -26,7 +26,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -37,10 +36,7 @@ class trxappgateusbbike : public bike { public: trxappgateusbbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -54,13 +50,12 @@ class trxappgateusbbike : public bike { void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, bool wait_for_response); void startDiscover(); - uint16_t watts(); + uint16_t watts() override; double GetWattFromPacket(const QByteArray &packet); double GetWattFromPacketFytter(const QByteArray &packet); double GetCadenceFromPacket(const QByteArray &packet); QTimer *refresh; - virtualbike *virtualBike = nullptr; #ifdef Q_OS_IOS lockscreen *h = 0; @@ -105,6 +100,7 @@ class trxappgateusbbike : public bike { VIRTUFIT_2 = 15, TUNTURI = 16, TUNTURI_2 = 17, + FITHIWAY = 18, } TYPE; TYPE bike_type = TRXAPPGATE; diff --git a/src/trxappgateusbtreadmill.cpp b/src/trxappgateusbtreadmill.cpp index 5b9918da7..9e942c93a 100644 --- a/src/trxappgateusbtreadmill.cpp +++ b/src/trxappgateusbtreadmill.cpp @@ -1,5 +1,7 @@ #include "trxappgateusbtreadmill.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualtreadmill.h" #include #include @@ -34,12 +36,20 @@ void trxappgateusbtreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); + } if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -62,7 +72,13 @@ void trxappgateusbtreadmill::forceIncline(double requestIncline) { if (requestIncline < 0) requestIncline = 0; - if (!reebok_fr30_treadmill) { + if (treadmill_type == TYPE::ADIDAS) { + uint8_t write[] = {0xf0, 0xac, 0x5b, 0xd3, 0x08, 0x64, 0x64, 0x9a}; + write[4] = (requestIncline + 1); + write[7] = write[4] + 0x92; + + writeCharacteristic(write, sizeof(write), QStringLiteral("forceIncline"), false, true); + } else if (!reebok_fr30_treadmill) { uint8_t write[] = {0xf0, 0xac, 0x01, 0xd3, 0x03, 0x64, 0x64, 0x3b}; write[4] = (requestIncline + 1); write[7] = write[4] + 0x38; @@ -112,12 +128,15 @@ void trxappgateusbtreadmill::update() { } bool toorx30 = settings.value(QZSettings::toorx_3_0, QZSettings::default_toorx_3_0).toBool(); - if (treadmill_type == TYPE::REEBOK) { + if (treadmill_type == TYPE::REEBOK || treadmill_type == TYPE::REEBOK_2) { const uint8_t noOpData[] = {0xf0, 0xa2, 0x32, 0xd3, 0x97}; writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true); } else if (treadmill_type == TYPE::DKN_2) { const uint8_t noOpData[] = {0xf0, 0xa2, 0x04, 0x01, 0x97}; writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true); + } else if (treadmill_type == TYPE::ADIDAS) { + const uint8_t noOpData[] = {0xf0, 0xa2, 0x5b, 0xd3, 0xc0}; + writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true); } else if (treadmill_type == TYPE::DKN || treadmill_type == TYPE::DKN_2 || toorx30 == false || jtx_fitness_sprint_treadmill) { const uint8_t noOpData[] = {0xf0, 0xa2, 0x01, 0xd3, 0x66}; @@ -148,12 +167,16 @@ void trxappgateusbtreadmill::update() { if (requestStart != -1) { emit debug(QStringLiteral("starting...")); // btinit(true); - if (treadmill_type == TYPE::REEBOK) { + if (treadmill_type == TYPE::REEBOK || treadmill_type == TYPE::REEBOK_2) { const uint8_t startTape[] = {0xf0, 0xa5, 0x32, 0xd3, 0x02, 0x9c}; writeCharacteristic((uint8_t *)startTape, sizeof(startTape), QStringLiteral("startTape"), false, true); } else if (treadmill_type == TYPE::DKN || treadmill_type == TYPE::DKN_2 || toorx30 == false) { const uint8_t startTape[] = {0xf0, 0xa5, 0x01, 0xd3, 0x02, 0x6b}; writeCharacteristic((uint8_t *)startTape, sizeof(startTape), QStringLiteral("startTape"), false, true); + } else if (treadmill_type == TYPE::ADIDAS) { + const uint8_t startTape[] = {0xf0, 0xa5, 0x5b, 0xd3, 0x02, 0xc5}; + writeCharacteristic((uint8_t *)startTape, sizeof(startTape), QStringLiteral("startTape"), false, true); + writeCharacteristic((uint8_t *)startTape, sizeof(startTape), QStringLiteral("startTape"), false, true); } else { const uint8_t startTape[] = {0xf0, 0xa5, 0x23, 0xd3, 0x02, 0x8d}; writeCharacteristic((uint8_t *)startTape, sizeof(startTape), QStringLiteral("startTape"), false, true); @@ -165,6 +188,12 @@ void trxappgateusbtreadmill::update() { if (requestStop != -1) { emit debug(QStringLiteral("stopping...")); // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + + if (treadmill_type == TYPE::ADIDAS) { + const uint8_t stopTape[] = {0xf0, 0xa5, 0x5b, 0xd3, 0x04, 0xc7}; + writeCharacteristic((uint8_t *)stopTape, sizeof(stopTape), QStringLiteral("stopTape"), false, true); + } + requestStop = -1; } if (requestIncreaseFan != -1) { @@ -205,7 +234,7 @@ void trxappgateusbtreadmill::characteristicChanged(const QLowEnergyCharacteristi readyToStart = true; requestStart = 1; } - } else if (treadmill_type != TYPE::REEBOK && treadmill_type != TYPE::DKN && treadmill_type != TYPE::DKN_2) { + } else if (treadmill_type != TYPE::REEBOK && treadmill_type != TYPE::REEBOK_2 && treadmill_type != TYPE::DKN && treadmill_type != TYPE::DKN_2) { if (newValue.at(16) == 0x04 && newValue.at(17) == 0x03 && readyToStart == false) { readyToStart = true; requestStart = 1; @@ -224,20 +253,7 @@ void trxappgateusbtreadmill::characteristicChanged(const QLowEnergyCharacteristi #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#else - Heart = 0; -#endif -#else - Heart = 0; -#endif + update_hr_from_external(); } } FanSpeed = 0; @@ -357,7 +373,45 @@ void trxappgateusbtreadmill::btinit(bool startTape) { writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, false); QThread::msleep(400); - } else if (treadmill_type == TYPE::REEBOK) { + } else if (treadmill_type == TYPE::ADIDAS) { + const uint8_t initData1[] = {0xf0, 0xa0, 0x02, 0x02, 0x94}; + const uint8_t initData2[] = {0xf0, 0xa0, 0x5b, 0xd3, 0xbe}; + const uint8_t initData3[] = {0xf0, 0xa5, 0x5b, 0xd3, 0x04, 0xc7}; + const uint8_t initData4[] = {0xf0, 0xa1, 0x5b, 0xd3, 0xbf}; + const uint8_t initData5[] = {0xf0, 0xa3, 0x5b, 0xd3, 0x04, 0x01, 0x01, 0x01, 0x01, 0x01, 0x47, 0x11}; + const uint8_t initData6[] = {0xf0, 0xac, 0x5b, 0xd3, 0x01, 0x64, 0x64, 0x93}; + const uint8_t initData7[] = {0xf0, 0xa4, 0x5b, 0xd3, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x0b, 0x01, 0x01, 0xd7}; + const uint8_t initData8[] = {0xf0, 0xaf, 0x5b, 0xd3, 0x02, 0xcf}; + + writeCharacteristic((uint8_t *)initData1, sizeof(initData1), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData2, sizeof(initData2), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData5, sizeof(initData5), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData6, sizeof(initData6), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData6, sizeof(initData6), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData6, sizeof(initData6), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData7, sizeof(initData7), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData8, sizeof(initData8), QStringLiteral("init"), false, true); + QThread::msleep(400); + writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, false); + QThread::msleep(400); + + } else if (treadmill_type == TYPE::REEBOK || treadmill_type == TYPE::REEBOK_2) { const uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x01, 0x92}; const uint8_t initData2[] = {0xf0, 0xa0, 0x32, 0xd3, 0x95}; const uint8_t initData3[] = {0xf0, 0xa1, 0x32, 0xd3, 0x96}; @@ -541,16 +595,17 @@ void trxappgateusbtreadmill::stateChanged(QLowEnergyService::ServiceState state) &trxappgateusbtreadmill::descriptorWritten); // ******************************************* virtual treadmill init ************************************* - if (!firstVirtualTreadmill && !virtualTreadMill) { + if (!firstVirtualTreadmill && !this->hasVirtualDevice()) { QSettings settings; bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual treadmill interface...")); - virtualTreadMill = new virtualtreadmill(this, false); + auto virtualTreadMill = new virtualtreadmill(this, false); connect(virtualTreadMill, &virtualtreadmill::debug, this, &trxappgateusbtreadmill::debug); connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, &trxappgateusbtreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstVirtualTreadmill = 1; @@ -604,6 +659,17 @@ void trxappgateusbtreadmill::serviceScanDone(void) { treadmill_type = TYPE::IRUNNING_2; qDebug() << QStringLiteral("treadmill_type IRUNNING_2"); } + } else if (treadmill_type == TYPE::REEBOK) { + uuid = QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb"); + QBluetoothUuid _gattCommunicationChannelServiceId2((QString)uuid); + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId2); + if (gattCommunicationChannelService == nullptr) { + qDebug() << QStringLiteral("invalid service") << uuid; + return; + } else { + treadmill_type = TYPE::REEBOK_2; + qDebug() << QStringLiteral("treadmill_type REEBOK_2"); + } } else { qDebug() << QStringLiteral("invalid service") << uuid; return; @@ -645,6 +711,7 @@ void trxappgateusbtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device device.name().toUpper().startsWith(QStringLiteral("DKN RUN")) || device.name().toUpper().startsWith(QStringLiteral("K80_")) || device.name().toUpper().startsWith(QStringLiteral("XT900")) || + device.name().toUpper().startsWith(QStringLiteral("ADIDAS ")) || device.name().toUpper().startsWith(QStringLiteral("XT485"))) { if (dkn_endurun_treadmill) { treadmill_type = TYPE::DKN; @@ -660,6 +727,8 @@ void trxappgateusbtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device treadmill_type = TYPE::IRUNNING; } else if (device.name().toUpper().startsWith(QStringLiteral("REEBOK"))) { treadmill_type = TYPE::REEBOK; + } else if (device.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) { + treadmill_type = TYPE::ADIDAS; } else { treadmill_type = TYPE::TRXAPPGATE; } @@ -705,10 +774,6 @@ bool trxappgateusbtreadmill::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *trxappgateusbtreadmill::VirtualTreadMill() { return virtualTreadMill; } - -void *trxappgateusbtreadmill::VirtualDevice() { return VirtualTreadMill(); } - void trxappgateusbtreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { qDebug() << QStringLiteral("controllerStateChanged") << state; if (state == QLowEnergyController::UnconnectedState && m_control) { diff --git a/src/trxappgateusbtreadmill.h b/src/trxappgateusbtreadmill.h index dd3ee5012..4d9bc296f 100644 --- a/src/trxappgateusbtreadmill.h +++ b/src/trxappgateusbtreadmill.h @@ -26,18 +26,14 @@ #include #include "treadmill.h" -#include "virtualtreadmill.h" class trxappgateusbtreadmill : public treadmill { Q_OBJECT public: trxappgateusbtreadmill(); - bool connected(); + bool connected() override; - void *VirtualTreadMill(); - void *VirtualDevice(); - - double minStepInclination(); + double minStepInclination() override; private: double GetSpeedFromPacket(const QByteArray &packet); @@ -56,7 +52,6 @@ class trxappgateusbtreadmill : public treadmill { double DistanceCalculated = 0; QTimer *refresh; - virtualtreadmill *virtualTreadMill = nullptr; uint8_t firstVirtualTreadmill = 0; bool firstCharChanged = true; @@ -73,7 +68,7 @@ class trxappgateusbtreadmill : public treadmill { bool initRequest = false; bool readyToStart = false; - typedef enum TYPE { TRXAPPGATE = 0, IRUNNING = 1, REEBOK = 2, DKN = 3, DKN_2 = 4, IRUNNING_2 = 5 } TYPE; + typedef enum TYPE { TRXAPPGATE = 0, IRUNNING = 1, REEBOK = 2, DKN = 3, DKN_2 = 4, IRUNNING_2 = 5, ADIDAS = 6, REEBOK_2 = 7 } TYPE; TYPE treadmill_type = TRXAPPGATE; signals: diff --git a/src/ultrasportbike.cpp b/src/ultrasportbike.cpp index 3f8e52726..b2d3f0ee2 100644 --- a/src/ultrasportbike.cpp +++ b/src/ultrasportbike.cpp @@ -1,6 +1,7 @@ #include "ultrasportbike.h" -#include "ios/lockscreen.h" +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -42,10 +43,10 @@ void ultrasportbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const // one for the resistance changed event (spontaneous), and one for the other ones. if (wait_for_response) { connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); - timeout.singleShot(300ms, &loop, &QEventLoop::quit); + timeout.singleShot(1000ms, &loop, &QEventLoop::quit); } else { connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); - timeout.singleShot(300ms, &loop, &QEventLoop::quit); + timeout.singleShot(1000ms, &loop, &QEventLoop::quit); } if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered || @@ -59,12 +60,20 @@ void ultrasportbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const return; } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); + } if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info; + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } loop.exec(); @@ -92,7 +101,6 @@ void ultrasportbike::update() { if (sec1Update++ == (1000 / refresh->interval())) { sec1Update = 0; - } else { uint8_t initData1[] = {0xff, 0xfd, 0x03, 0x00, 0xec}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("noOp"), false, true); } @@ -178,11 +186,16 @@ void ultrasportbike::characteristicChanged(const QLowEnergyCharacteristic &chara /*if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = GetSpeedFromPacket(newValue); } else*/ - { Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } + { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -204,23 +217,15 @@ void ultrasportbike::characteristicChanged(const QLowEnergyCharacteristic &chara #endif { if (heartRateBeltName.startsWith(QLatin1String("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - qDebug() << "Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -251,6 +256,14 @@ void ultrasportbike::btinit() { uint8_t initData1[] = {0xff, 0xfd, 0x03, 0x00, 0xec}; uint8_t initData2[] = {0xff, 0xaa, 0xec}; + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); @@ -291,7 +304,7 @@ void ultrasportbike::stateChanged(QLowEnergyService::ServiceState state) { &ultrasportbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -299,11 +312,14 @@ void ultrasportbike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -313,10 +329,11 @@ void ultrasportbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { qDebug() << QStringLiteral("creating virtual bike interface..."); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&ultrasportbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &ultrasportbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -415,10 +432,6 @@ bool ultrasportbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *ultrasportbike::VirtualBike() { return virtualBike; } - -void *ultrasportbike::VirtualDevice() { return VirtualBike(); } - uint16_t ultrasportbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/ultrasportbike.h b/src/ultrasportbike.h index 99d290e57..47af6f85d 100644 --- a/src/ultrasportbike.h +++ b/src/ultrasportbike.h @@ -38,12 +38,9 @@ class ultrasportbike : public bike { public: ultrasportbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - resistance_t maxResistance() { return max_resistance; } - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + resistance_t maxResistance() override { return max_resistance; } + bool connected() override; private: bool r92 = false; @@ -56,7 +53,7 @@ class ultrasportbike : public bike { void startDiscover(); void forceResistance(resistance_t requestResistance); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; virtualbike *virtualBike = nullptr; diff --git a/src/virtualbike.cpp b/src/virtualbike.cpp index cfa16a802..4088e9b63 100644 --- a/src/virtualbike.cpp +++ b/src/virtualbike.cpp @@ -1,5 +1,5 @@ #include "virtualbike.h" -#include "ftmsbike.h" +#include "bike.h" #include #include @@ -57,7 +57,8 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear qDebug() << "ios_zwift_workaround activated!"; h = new lockscreen(); - h->virtualbike_zwift_ios(); + h->virtualbike_zwift_ios( + settings.value(QZSettings::bike_heartrate_service, QZSettings::default_bike_heartrate_service).toBool()); } else #endif @@ -218,7 +219,7 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear QLowEnergyCharacteristicData charData3; charData3.setUuid(QBluetoothUuid::CharacteristicType::CyclingPowerMeasurement); - charData3.setProperties(QLowEnergyCharacteristic::Notify); + charData3.setProperties(QLowEnergyCharacteristic::Notify | QLowEnergyCharacteristic::Read); QByteArray descriptor; descriptor.append((char)0x01); descriptor.append((char)0x00); @@ -443,7 +444,11 @@ virtualbike::virtualbike(bluetoothdevice *t, bool noWriteResistance, bool noHear //! [Provide Heartbeat] QObject::connect(&bikeTimer, &QTimer::timeout, this, &virtualbike::bikeProvider); - bikeTimer.start(1s); + if (settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool()) + bikeTimer.start(100ms); + else + bikeTimer.start(1s); + //! [Provide Heartbeat] QObject::connect(leController, &QLowEnergyController::disconnected, this, &virtualbike::reconnect); QObject::connect( diff --git a/src/virtualbike.h b/src/virtualbike.h index e1040cad0..35f022e27 100644 --- a/src/virtualbike.h +++ b/src/virtualbike.h @@ -25,16 +25,16 @@ #ifdef Q_OS_IOS #include "ios/lockscreen.h" #endif -#include "bike.h" #include "dirconmanager.h" +#include "virtualdevice.h" -class virtualbike : public QObject { +class virtualbike : public virtualdevice { Q_OBJECT public: virtualbike(bluetoothdevice *t, bool noWriteResistance = false, bool noHeartService = false, uint8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0); - bool connected(); + bool connected() override; bool ftmsDeviceConnected() { return lastFTMSFrameReceived != 0 || lastDirconFTMSFrameReceived != 0; } qint64 whenLastFTMSFrameReceived() { if (lastFTMSFrameReceived != 0) diff --git a/src/virtualdevice.cpp b/src/virtualdevice.cpp new file mode 100644 index 000000000..851154467 --- /dev/null +++ b/src/virtualdevice.cpp @@ -0,0 +1,12 @@ +#include "virtualdevice.h" +#include + +virtualdevice::virtualdevice(QObject *parent) + : QObject{parent} +{ + +} + +virtualdevice::~virtualdevice() { + qDebug() << "Deleting virtual device"; +} diff --git a/src/virtualdevice.h b/src/virtualdevice.h new file mode 100644 index 000000000..ccdab9235 --- /dev/null +++ b/src/virtualdevice.h @@ -0,0 +1,18 @@ +#ifndef VIRTUALDEVICE_H +#define VIRTUALDEVICE_H + +#include + +class virtualdevice : public QObject +{ + Q_OBJECT +public: + explicit virtualdevice(QObject *parent = nullptr); + ~virtualdevice() override; + virtual bool connected()=0; + +signals: + +}; + +#endif // VIRTUALDEVICE_H diff --git a/src/virtualrower.cpp b/src/virtualrower.cpp index dd366d01e..b8f0b3a9f 100644 --- a/src/virtualrower.cpp +++ b/src/virtualrower.cpp @@ -1,6 +1,6 @@ #include "virtualrower.h" -#include "ftmsrower.h" #include "qsettings.h" +#include "rower.h" #include #include @@ -59,12 +59,13 @@ virtualrower::virtualrower(bluetoothdevice *t, bool noWriteResistance, bool noHe QLowEnergyCharacteristicData charDataFIT; charDataFIT.setUuid((QBluetoothUuid::CharacteristicType)0x2ACC); // FitnessMachineFeatureCharacteristicUuid QByteArray valueFIT; - valueFIT.append((char)0xA2); // cadence, pace and resistance level supported - valueFIT.append((char)0x45); // stride count,heart rate, power supported - valueFIT.append((char)0x00); + + valueFIT.append((char)0x83); + valueFIT.append((char)0x14); valueFIT.append((char)0x00); - valueFIT.append((char)0x0C); // resistance and power target supported valueFIT.append((char)0x00); + valueFIT.append((char)0x0C); + valueFIT.append((char)0xe0); valueFIT.append((char)0x00); valueFIT.append((char)0x00); charDataFIT.setValue(valueFIT); @@ -175,7 +176,11 @@ virtualrower::virtualrower(bluetoothdevice *t, bool noWriteResistance, bool noHe //! [Provide Heartbeat] QObject::connect(&rowerTimer, &QTimer::timeout, this, &virtualrower::rowerProvider); - rowerTimer.start(1s); + if (settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool()) + rowerTimer.start(100ms); + else + rowerTimer.start(1s); + //! [Provide Heartbeat] QObject::connect(leController, &QLowEnergyController::disconnected, this, &virtualrower::reconnect); QObject::connect( @@ -378,19 +383,29 @@ void virtualrower::rowerProvider() { if (!heart_only) { - value.append((char)0xA0); // resistance level, power and speed - value.append((char)0x02); // heart rate + value.append((char)0x2C); + value.append((char)0x03); - value.append((char)((uint8_t)Rower->currentCadence().value() & 0xFF)); // Stroke Rate + value.append((char)((uint8_t)(Rower->currentCadence().value() * 2) & 0xFF)); // Stroke Rate value.append((char)((uint16_t)(((rower *)Rower)->currentStrokesCount().value()) & 0xFF)); // Stroke Count value.append((char)(((uint16_t)(((rower *)Rower)->currentStrokesCount().value()) >> 8) & 0xFF)); // Stroke Count + value.append((char)(((uint16_t)(((rower *)Rower)->odometer() * 1000.0)) & 0xFF)); // Distance + value.append((char)(((uint16_t)(((rower *)Rower)->odometer() * 1000.0) >> 8) & 0xFF)); // Distance + value.append((char)(((uint16_t)(((rower *)Rower)->odometer() * 1000.0) >> 16) & 0xFF)); // Distance + + value.append((char)(((uint16_t)QTime(0, 0, 0).secsTo(((rower *)Rower)->currentPace())) & 0xFF)); // pace + value.append((char)(((uint16_t)QTime(0, 0, 0).secsTo(((rower *)Rower)->currentPace())) >> 8) & 0xFF); // pace + value.append((char)(((uint16_t)Rower->wattsMetric().value()) & 0xFF)); // watts value.append((char)(((uint16_t)Rower->wattsMetric().value()) >> 8) & 0xFF); // watts - value.append((char)((uint16_t)(Rower->currentResistance().value()) & 0xFF)); // resistance - value.append((char)(((uint16_t)(Rower->currentResistance().value()) >> 8) & 0xFF)); // resistance + value.append((char)((uint16_t)(Rower->calories().value()) & 0xFF)); // calories + value.append((char)(((uint16_t)(Rower->calories().value()) >> 8) & 0xFF)); // calories + value.append((char)((uint16_t)(Rower->calories().value()) & 0xFF)); // calories + value.append((char)(((uint16_t)(Rower->calories().value()) >> 8) & 0xFF)); // calories + value.append((char)((uint16_t)(Rower->calories().value()) & 0xFF)); // calories value.append(char(Rower->currentHeart().value())); // Actual value. value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix diff --git a/src/virtualrower.h b/src/virtualrower.h index 8ef639eda..f99cb5f6f 100644 --- a/src/virtualrower.h +++ b/src/virtualrower.h @@ -25,14 +25,15 @@ #ifdef Q_OS_IOS #include "ios/lockscreen.h" #endif -#include "bike.h" +#include "bluetoothdevice.h" +#include "virtualdevice.h" -class virtualrower : public QObject { +class virtualrower : public virtualdevice { Q_OBJECT public: virtualrower(bluetoothdevice *t, bool noWriteResistance = false, bool noHeartService = false); - bool connected(); + bool connected() override; private: QLowEnergyController *leController = nullptr; diff --git a/src/virtualtreadmill.cpp b/src/virtualtreadmill.cpp index 616076842..b9c4de675 100644 --- a/src/virtualtreadmill.cpp +++ b/src/virtualtreadmill.cpp @@ -1,6 +1,4 @@ #include "virtualtreadmill.h" -#include "elliptical.h" -#include "ftmsbike.h" #include #include #include @@ -259,7 +257,10 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) { } //! [Provide Heartbeat] QObject::connect(&treadmillTimer, &QTimer::timeout, this, &virtualtreadmill::treadmillProvider); - treadmillTimer.start(1s); + if (settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool()) + treadmillTimer.start(100ms); + else + treadmillTimer.start(1s); } void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &characteristic, diff --git a/src/virtualtreadmill.h b/src/virtualtreadmill.h index 7b1fbaeba..44bb6758d 100644 --- a/src/virtualtreadmill.h +++ b/src/virtualtreadmill.h @@ -25,15 +25,15 @@ #include #include #include - -#include "dirconmanager.h" #include "treadmill.h" +#include "dirconmanager.h" +#include "virtualdevice.h" -class virtualtreadmill : public QObject { +class virtualtreadmill : public virtualdevice { Q_OBJECT public: virtualtreadmill(bluetoothdevice *t, bool noHeartService); - bool connected(); + bool connected() override; bool autoInclinationEnabled() { return m_autoInclinationEnabled; } private: diff --git a/src/wahookickrheadwind.cpp b/src/wahookickrheadwind.cpp index 30d79d66e..80ff0dcbb 100644 --- a/src/wahookickrheadwind.cpp +++ b/src/wahookickrheadwind.cpp @@ -18,6 +18,10 @@ wahookickrheadwind::wahookickrheadwind(bluetoothdevice *parentDevice) { QZ_EnableDiscoveryCharsAndDescripttors = true; #endif this->parentDevice = parentDevice; + + refresh = new QTimer(this); + connect(refresh, &QTimer::timeout, this, &wahookickrheadwind::update); + refresh->start(1000ms); } void wahookickrheadwind::update() { @@ -25,34 +29,34 @@ void wahookickrheadwind::update() { initRequest = false; uint8_t init1[] = {0x01}; - writeCharacteristic(&gattWrite1Characteristic, init1, sizeof(init1), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init1, sizeof(init1), "init"); uint8_t init2[] = {0x03}; - writeCharacteristic(&gattWrite1Characteristic, init2, sizeof(init2), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init2, sizeof(init2), "init"); uint8_t init3[] = {0x05}; - writeCharacteristic(&gattWrite1Characteristic, init3, sizeof(init3), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init3, sizeof(init3), "init"); uint8_t init4[] = {0x06, 0x00, 0x00, 0x00, 0x03, 0xbb, 0x08, 0x00, 0x00}; - writeCharacteristic(&gattWrite1Characteristic, init4, sizeof(init4), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init4, sizeof(init4), "init"); uint8_t init5[] = {0x04, 0x02}; - writeCharacteristic(&gattWrite1Characteristic, init5, sizeof(init5), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init5, sizeof(init5), "init"); uint8_t init6[] = {0x20, 0xee, 0xfc}; - writeCharacteristic(&gattWrite2Characteristic, init6, sizeof(init6), "init"); + writeCharacteristic(gattWrite2Service, &gattWrite2Characteristic, init6, sizeof(init6), "init"); uint8_t init7[] = {0x23}; - writeCharacteristic(&gattWrite2Characteristic, init7, sizeof(init7), "init"); + writeCharacteristic(gattWrite2Service, &gattWrite2Characteristic, init7, sizeof(init7), "init"); uint8_t init8[] = {0x04, 0x03}; - writeCharacteristic(&gattWrite1Characteristic, init8, sizeof(init8), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init8, sizeof(init8), "init"); uint8_t init9[] = {0x04, 0x04}; - writeCharacteristic(&gattWrite1Characteristic, init9, sizeof(init9), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init9, sizeof(init9), "init"); uint8_t init10[] = {0x02, 0x00}; - writeCharacteristic(&gattWrite1Characteristic, init10, sizeof(init10), "init"); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init10, sizeof(init10), "init"); initDone = true; } @@ -89,15 +93,17 @@ void wahookickrheadwind::fanSpeedRequest(uint8_t speed) { uint8_t init10[] = {0x02, 0x00}; init10[1] = speed8; - writeCharacteristic(&gattWrite1Characteristic, init10, sizeof(init10), "forcing fan" + QString::number(speed)); + writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, init10, sizeof(init10), + "forcing fan" + QString::number(speed)); } -void wahookickrheadwind::writeCharacteristic(QLowEnergyCharacteristic *writeChar, uint8_t *data, uint8_t data_len, - const QString &info, bool disable_log, bool wait_for_response) { +void wahookickrheadwind::writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic *writeChar, + uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response) { QEventLoop loop; QTimer timeout; - if (gattCommunicationChannelService == nullptr || writeChar->isValid() == false) { + if (service == nullptr || writeChar->isValid() == false) { qDebug() << QStringLiteral( "wahookickrheadwind trying to change the fan speed before the connection is estabilished"); return; @@ -106,14 +112,14 @@ void wahookickrheadwind::writeCharacteristic(QLowEnergyCharacteristic *writeChar // if there are some crash here, maybe it's better to use 2 separate event for the characteristicChanged. // one for the resistance changed event (spontaneous), and one for the other ones. if (wait_for_response) { - connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); + connect(service, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); } else { - connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + connect(service, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered || + if (service->state() != QLowEnergyService::ServiceState::ServiceDiscovered || m_control->state() == QLowEnergyController::UnconnectedState) { qDebug() << QStringLiteral("writeCharacteristic error because the connection is closed"); return; @@ -124,11 +130,15 @@ void wahookickrheadwind::writeCharacteristic(QLowEnergyCharacteristic *writeChar return; } - gattCommunicationChannelService->writeCharacteristic(*writeChar, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + service->writeCharacteristic(*writeChar, *writeBuffer, QLowEnergyService::WriteWithoutResponse); if (!disable_log) { - qDebug() << QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info; + qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info; } loop.exec(); @@ -141,49 +151,92 @@ void wahookickrheadwind::stateChanged(QLowEnergyService::ServiceState state) { QMetaEnum metaEnum = QMetaEnum::fromType(); emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); - if (state == QLowEnergyService::ServiceDiscovered) { - auto characteristics_list = gattCommunicationChannelService->characteristics(); - for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) { - emit debug(QStringLiteral("characteristic ") + c.uuid().toString()); + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state(); + if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) { + qDebug() << QStringLiteral("not all services discovered"); + return; } + } - gattNotify1Characteristic = gattCommunicationChannelService->characteristic( - QBluetoothUuid(QStringLiteral("a026e002-0a7d-4ab3-97fa-f1500f9feb8b"))); - Q_ASSERT(gattNotify1Characteristic.isValid()); - gattNotify2Characteristic = gattCommunicationChannelService->characteristic( - QBluetoothUuid(QStringLiteral("a026e038-0a7d-4ab3-97fa-f1500f9feb8b"))); - Q_ASSERT(gattNotify2Characteristic.isValid()); - - gattWrite1Characteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId1); - Q_ASSERT(gattWrite1Characteristic.isValid()); - gattWrite2Characteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId2); - Q_ASSERT(gattWrite2Characteristic.isValid()); - - // establish hook into notifications - connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this, - &wahookickrheadwind::characteristicChanged); - connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this, - &wahookickrheadwind::characteristicWritten); - connect(gattCommunicationChannelService, - static_cast(&QLowEnergyService::error), + if (state != QLowEnergyService::ServiceState::ServiceDiscovered) { + qDebug() << QStringLiteral("ignoring this state"); + return; + } + + qDebug() << QStringLiteral("all services discovered!"); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + if (s->state() == QLowEnergyService::ServiceDiscovered) { + // establish hook into notifications + connect(s, &QLowEnergyService::characteristicChanged, this, &wahookickrheadwind::characteristicChanged); + connect(s, &QLowEnergyService::characteristicWritten, this, &wahookickrheadwind::characteristicWritten); + connect( + s, static_cast(&QLowEnergyService::error), this, &wahookickrheadwind::errorService); - connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this, - &wahookickrheadwind::descriptorWritten); - - QByteArray descriptor; - descriptor.append((char)0x01); - descriptor.append((char)0x00); - gattCommunicationChannelService->writeDescriptor( - gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); - gattCommunicationChannelService->writeDescriptor( - gattNotify2Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); - - initRequest = true; + connect(s, &QLowEnergyService::descriptorWritten, this, &wahookickrheadwind::descriptorWritten); + + qDebug() << s->serviceUuid() << QStringLiteral("connected!"); + + auto characteristics_list = s->characteristics(); + for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) { + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle(); + auto descriptors_list = c.descriptors(); + for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) { + qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); + } + + if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) { + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Indicate) == + QLowEnergyCharacteristic::Indicate) { + QByteArray descriptor; + descriptor.append((char)0x02); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Read) == QLowEnergyCharacteristic::Read) { + // s->readCharacteristic(c); + // qDebug() << s->serviceUuid() << c.uuid() << "reading!"; + } + + if (c.uuid() == _gattWriteCharacteristicId1) { + qDebug() << QStringLiteral("_gattWriteCharacteristicId1 found"); + gattWrite1Characteristic = c; + gattWrite1Service = s; + } else if (c.uuid() == _gattWriteCharacteristicId2) { + qDebug() << QStringLiteral("_gattWriteCharacteristicId2 found"); + gattWrite2Characteristic = c; + gattWrite2Service = s; + } + } + } } } void wahookickrheadwind::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ')); + initRequest = true; } void wahookickrheadwind::characteristicWritten(const QLowEnergyCharacteristic &characteristic, @@ -195,10 +248,19 @@ void wahookickrheadwind::characteristicWritten(const QLowEnergyCharacteristic &c void wahookickrheadwind::serviceScanDone(void) { emit debug(QStringLiteral("serviceScanDone")); - QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("a026ee0c-0a7d-4ab3-97fa-f1500f9feb8b")); - gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); - connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &wahookickrheadwind::stateChanged); - gattCommunicationChannelService->discoverDetails(); + initRequest = false; + + auto services_list = m_control->services(); + for (const QBluetoothUuid &s : qAsConst(services_list)) { + gattCommunicationChannelService.append(m_control->createServiceObject(s)); + if (gattCommunicationChannelService.constLast()) { + connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, + &wahookickrheadwind::stateChanged); + gattCommunicationChannelService.constLast()->discoverDetails(); + } else { + m_control->disconnectFromDevice(); + } + } } void wahookickrheadwind::errorService(QLowEnergyService::ServiceError err) { diff --git a/src/wahookickrheadwind.h b/src/wahookickrheadwind.h index 6e24c997b..d16c4122b 100644 --- a/src/wahookickrheadwind.h +++ b/src/wahookickrheadwind.h @@ -31,23 +31,28 @@ class wahookickrheadwind : public bluetoothdevice { Q_OBJECT public: wahookickrheadwind(bluetoothdevice *parentDevice); - bool connected(); + bool connected() override; private: - QLowEnergyService *gattCommunicationChannelService = nullptr; + QList gattCommunicationChannelService; QLowEnergyCharacteristic gattNotify1Characteristic; QLowEnergyCharacteristic gattNotify2Characteristic; QLowEnergyCharacteristic gattWrite1Characteristic; + QLowEnergyService *gattWrite1Service; QLowEnergyCharacteristic gattWrite2Characteristic; + QLowEnergyService *gattWrite2Service; - void writeCharacteristic(QLowEnergyCharacteristic *writeChar, uint8_t *data, uint8_t data_len, const QString &info, - bool disable_log = false, bool wait_for_response = false); + void writeCharacteristic(QLowEnergyService *service, QLowEnergyCharacteristic *writeChar, uint8_t *data, + uint8_t data_len, const QString &info, bool disable_log = false, + bool wait_for_response = false); bluetoothdevice *parentDevice = nullptr; bool initDone = false; bool initRequest = false; + QTimer *refresh; + signals: void disconnected(); void debug(QString string); diff --git a/src/wahookickrsnapbike.cpp b/src/wahookickrsnapbike.cpp index ad6b92f3c..5ec12e1f8 100644 --- a/src/wahookickrsnapbike.cpp +++ b/src/wahookickrsnapbike.cpp @@ -1,5 +1,5 @@ #include "wahookickrsnapbike.h" -#include "ios/lockscreen.h" + #include "virtualbike.h" #include #include @@ -9,9 +9,10 @@ #include #include #ifdef Q_OS_ANDROID +#include "keepawakehelper.h" #include #endif -#include "keepawakehelper.h" + #include using namespace std::chrono_literals; @@ -44,17 +45,22 @@ void wahookickrsnapbike::writeCharacteristic(uint8_t *data, uint8_t data_len, QS if (wait_for_response) { connect(gattPowerChannelService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic, QByteArray)), &loop, SLOT(quit())); - timeout.singleShot(300, &loop, SLOT(quit())); + timeout.singleShot(1000, &loop, SLOT(quit())); } else { connect(gattPowerChannelService, SIGNAL(characteristicWritten(QLowEnergyCharacteristic, QByteArray)), &loop, SLOT(quit())); - timeout.singleShot(300, &loop, SLOT(quit())); + timeout.singleShot(1000, &loop, SLOT(quit())); } - gattPowerChannelService->writeCharacteristic(gattWriteCharacteristic, QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattPowerChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) - debug(" >> " + QByteArray((const char *)data, data_len).toHex(' ') + " // " + info); + debug(" >> " + writeBuffer->toHex(' ') + " // " + info); loop.exec(); } @@ -171,11 +177,13 @@ void wahookickrsnapbike::update() { uint8_t b[20]; memcpy(b, a.constData(), a.length()); writeCharacteristic(b, a.length(), "init", false, true); + QThread::msleep(700); QByteArray c = setSimMode(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(), 0.004, 0.4); // wind and rolling should arrive from FTMS memcpy(b, c.constData(), c.length()); writeCharacteristic(b, c.length(), "setSimMode", false, true); + QThread::msleep(700); // required to the SS2K only one time Resistance = 0; @@ -217,6 +225,7 @@ void wahookickrsnapbike::update() { requestResistance = 1; } + auto virtualBike = this->VirtualBike(); if (requestResistance != currentResistance().value() && ((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) { emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); @@ -225,7 +234,10 @@ void wahookickrsnapbike::update() { uint8_t b[20]; memcpy(b, a.constData(), a.length()); writeCharacteristic(b, a.length(), "setResistance", false, true); + } else if (virtualBike && virtualBike->ftmsDeviceConnected() && lastGearValue != gears()) { + inclinationChanged(lastGrade, lastGrade); } + lastGearValue = gears(); requestResistance = -1; } if (requestStart != -1) { @@ -390,8 +402,10 @@ void wahookickrsnapbike::characteristicChanged(const QLowEnergyCharacteristic &c .startsWith(QStringLiteral("Disabled"))) { if (CrankRevs != oldCrankRevs && deltaT) { double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * time_division * 60; - if(!crank_rev_present) - cadence = cadence / 2; // I really don't like this, there is no releationship between wheel rev and crank rev + if (!crank_rev_present) + cadence = + cadence / + 2; // I really don't like this, there is no relationship between wheel rev and crank rev if (cadence >= 0) { Cadence = cadence; } @@ -489,24 +503,10 @@ void wahookickrsnapbike::characteristicChanged(const QLowEnergyCharacteristic &c } else #endif if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } - if (Cadence.value() > 0) { - CrankRevs++; - LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); - } - { #ifdef Q_OS_IOS #ifndef IO_UNDER_QT @@ -544,6 +544,8 @@ void wahookickrsnapbike::stateChanged(QLowEnergyService::ServiceState state) { } } + notificationSubscribed = 0; + qDebug() << QStringLiteral("all services discovered!"); for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { @@ -578,6 +580,7 @@ void wahookickrsnapbike::stateChanged(QLowEnergyService::ServiceState state) { QByteArray descriptor; descriptor.append((char)0x01); descriptor.append((char)0x00); + notificationSubscribed++; if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); } else { @@ -593,6 +596,7 @@ void wahookickrsnapbike::stateChanged(QLowEnergyService::ServiceState state) { QByteArray descriptor; descriptor.append((char)0x02); descriptor.append((char)0x00); + notificationSubscribed++; if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); } else { @@ -612,7 +616,7 @@ void wahookickrsnapbike::stateChanged(QLowEnergyService::ServiceState state) { } // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -637,21 +641,28 @@ void wahookickrsnapbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); // connect(virtualBike,&virtualbike::debug ,this,&wahookickrsnapbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &wahookickrsnapbike::inclinationChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; // ******************************************************************************************************** - initRequest = true; // here because it can't be in the descriptorWritten event since it will be called several times } void wahookickrsnapbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { - emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' ')); + qDebug() << QStringLiteral("descriptorWritten ") << descriptor.name() << newValue.toHex(' ') + << notificationSubscribed; - emit connectedAndDiscovered(); + if (notificationSubscribed) + notificationSubscribed--; + + if (!notificationSubscribed) { + initRequest = true; + emit connectedAndDiscovered(); + } } void wahookickrsnapbike::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { @@ -752,10 +763,6 @@ bool wahookickrsnapbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *wahookickrsnapbike::VirtualBike() { return virtualBike; } - -void *wahookickrsnapbike::VirtualDevice() { return VirtualBike(); } - uint16_t wahookickrsnapbike::watts() { if (currentCadence().value() == 0) { return 0; @@ -780,8 +787,9 @@ void wahookickrsnapbike::controllerStateChanged(QLowEnergyController::Controller void wahookickrsnapbike::inclinationChanged(double grade, double percentage) { Q_UNUSED(percentage); + lastGrade = grade; emit debug(QStringLiteral("writing inclination ") + QString::number(grade)); - QByteArray a = setSimGrade(grade); + QByteArray a = setSimGrade(grade + gears()); uint8_t b[20]; memcpy(b, a.constData(), a.length()); writeCharacteristic(b, a.length(), "setSimGrade", false, true); diff --git a/src/wahookickrsnapbike.h b/src/wahookickrsnapbike.h index 0e589db97..e69e7c8f7 100644 --- a/src/wahookickrsnapbike.h +++ b/src/wahookickrsnapbike.h @@ -38,12 +38,9 @@ class wahookickrsnapbike : public bike { public: wahookickrsnapbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - resistance_t pelotonToBikeResistance(int pelotonResistance); - bool connected(); - resistance_t maxResistance() { return 100; } - - void *VirtualBike(); - void *VirtualDevice(); + resistance_t pelotonToBikeResistance(int pelotonResistance) override; + bool connected() override; + resistance_t maxResistance() override { return 100; } enum OperationCode : uint8_t { _unlock = 32, @@ -75,7 +72,7 @@ class wahookickrsnapbike : public bike { uint16_t wattsFromResistance(double resistance); metric ResistanceFromFTMSAccessory; void startDiscover(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; virtualbike *virtualBike = nullptr; @@ -86,6 +83,8 @@ class wahookickrsnapbike : public bike { uint8_t sec1Update = 0; QByteArray lastPacket; + double lastGearValue = -1; + double lastGrade = 0; QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); QDateTime lastGoodCadence = QDateTime::currentDateTime(); uint8_t firstStateChanged = 0; @@ -104,6 +103,8 @@ class wahookickrsnapbike : public bike { bool WAHOO_KICKR = false; + volatile int notificationSubscribed = 0; + resistance_t lastForcedResistance = -1; #ifdef Q_OS_IOS @@ -116,7 +117,7 @@ class wahookickrsnapbike : public bike { public slots: void deviceDiscovered(const QBluetoothDeviceInfo &device); - void resistanceFromFTMSAccessory(resistance_t res); + void resistanceFromFTMSAccessory(resistance_t res) override; private slots: diff --git a/src/windows/setup.bat b/src/windows/setup.bat new file mode 100644 index 000000000..0d99f348a --- /dev/null +++ b/src/windows/setup.bat @@ -0,0 +1,21 @@ +@echo off +setlocal enabledelayedexpansion + +REM Get the current directory of the script +set "script_dir=%~dp0" + +REM Check if the directory is already in the PATH +echo !PATH! | find /i "!script_dir!python\x64\Scripts\;!script_dir!python\x64\" > nul +if errorlevel 1 ( + REM Append the directory to the PATH variable + set "PATH=!PATH!;!script_dir!python\x64\Scripts\;!script_dir!python\x64\" + echo !PATH! + echo The script directory has been added to the PATH. +) else ( + echo The script directory is already in the PATH. +) + +REM Update the system PATH variable +setx PATH "!PATH!" /M + +endlocal diff --git a/src/windows/zwift-incline-climb-portal.py b/src/windows/zwift-incline-climb-portal.py new file mode 100644 index 000000000..b605b6a82 --- /dev/null +++ b/src/windows/zwift-incline-climb-portal.py @@ -0,0 +1,107 @@ +# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR +# Author: Al Udell +# Revised: August 16, 2023 + +# zwift-incline-climb-portal.py - take Zwift screenshot, crop incline, OCR incline + +# imports +import cv2 +import numpy as np +import re +from datetime import datetime +from paddleocr import PaddleOCR +from PIL import Image, ImageGrab + +# Take Zwift screenshot +screenshot = ImageGrab.grab() + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to incline area +screenwidth, screenheight = screenshot.size + +# Values for Zwift climb portal incline +col1 = int(screenwidth/3000 * 2822) +row1 = int(screenheight/2000 * 218) +col2 = int(screenwidth/3000 * 2980) +row2 = int(screenheight/2000 * 302) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Scale image to correct size for borderless window mode +width, height = cropped.size +cropped = cropped.resize((int(width * 1.3), int(height * 1.3))) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Convert np array to PIL +cropped_pil = Image.fromarray(cropped_np) + +# Convert PIL image to cv2 RGB +cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) + +# Convert cv2 RGB to HSV +result = cropped_cv2.copy() +image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) + +# Isolate white mask +lower = np.array([0,0,159]) +upper = np.array([0,0,255]) +mask0 = cv2.inRange(image, lower, upper) +result0 = cv2.bitwise_and(result, result, mask=mask0) + +# Isolate yellow mask +lower = np.array([24,239,241]) +upper = np.array([24,253,255]) +mask1 = cv2.inRange(image, lower, upper) +result1 = cv2.bitwise_and(result, result, mask=mask1) + +# Isolate orange mask +lower = np.array([8,191,243]) +upper = np.array([8,192,243]) +mask2 = cv2.inRange(image, lower, upper) +result2 = cv2.bitwise_and(result, result, mask=mask2) + +# Isolate red mask +lower = np.array([0,255,255]) +upper = np.array([10,255,255]) +mask3 = cv2.inRange(image, lower, upper) +result3 = cv2.bitwise_and(result, result, mask=mask3) + +# Join colour masks +mask = mask0+mask1+mask2+mask3 + +# Set output image to zero everywhere except mask +merge = image.copy() +merge[np.where(mask==0)] = 0 + +# Convert to grayscale +gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) + +# Convert to black/white by threshold +ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) + +# Apply gaussian blur +gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) + +# OCR image +ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False) +result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True) + +# Extract OCR text +ocr_text = '' +for line in result: + for word in line: + ocr_text += f"{word[1][0]}" + +# Remove all characters that are not "-" and integers from OCR text +pattern = r"[^-\d]+" +ocr_text = re.sub(pattern, "", ocr_text) +if ocr_text: + incline = ocr_text +else: + incline = 'None' + +print(incline) diff --git a/src/windows/zwift-incline.py b/src/windows/zwift-incline.py new file mode 100644 index 000000000..7b883fa29 --- /dev/null +++ b/src/windows/zwift-incline.py @@ -0,0 +1,103 @@ +# iFit-Wolf3 - Autoincline control of treadmill via ADB and OCR +# Author: Al Udell +# Revised: August 16, 2023 + +# zwift-incline.py - take Zwift screenshot, crop incline, OCR incline + +# imports +import cv2 +import numpy as np +import re +from datetime import datetime +from paddleocr import PaddleOCR +from PIL import Image, ImageGrab + +# Take Zwift screenshot +screenshot = ImageGrab.grab() + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to incline area +screenwidth, screenheight = screenshot.size + +# Values for Zwift regular incline +col1 = int(screenwidth/3000 * 2800) +row1 = int(screenheight/2000 * 90) +col2 = int(screenwidth/3000 * 2975) +row2 = int(screenheight/2000 * 195) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Convert image to np array +cropped_np = np.array(cropped) + +# Convert np array to PIL +cropped_pil = Image.fromarray(cropped_np) + +# Convert PIL image to cv2 RGB +cropped_cv2 = cv2.cvtColor(np.array(cropped_pil), cv2.COLOR_RGB2BGR) + +# Convert cv2 RGB to HSV +result = cropped_cv2.copy() +image = cv2.cvtColor(cropped_cv2, cv2.COLOR_BGR2HSV) + +# Isolate white mask +lower = np.array([0,0,159]) +upper = np.array([0,0,255]) +mask0 = cv2.inRange(image, lower, upper) +result0 = cv2.bitwise_and(result, result, mask=mask0) + +# Isolate yellow mask +lower = np.array([24,239,241]) +upper = np.array([24,253,255]) +mask1 = cv2.inRange(image, lower, upper) +result1 = cv2.bitwise_and(result, result, mask=mask1) + +# Isolate orange mask +lower = np.array([8,191,243]) +upper = np.array([8,192,243]) +mask2 = cv2.inRange(image, lower, upper) +result2 = cv2.bitwise_and(result, result, mask=mask2) + +# Isolate red mask +lower = np.array([0,255,255]) +upper = np.array([10,255,255]) +mask3 = cv2.inRange(image, lower, upper) +result3 = cv2.bitwise_and(result, result, mask=mask3) + +# Join colour masks +mask = mask0+mask1+mask2+mask3 + +# Set output image to zero everywhere except mask +merge = image.copy() +merge[np.where(mask==0)] = 0 + +# Convert to grayscale +gray = cv2.cvtColor(merge, cv2.COLOR_BGR2GRAY) + +# Convert to black/white by threshold +ret,bin = cv2.threshold(gray, 70, 255, cv2.THRESH_BINARY_INV) + +# Apply gaussian blur +gaussianBlur = cv2.GaussianBlur(bin,(3,3),0) + +# OCR image +ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False) +result = ocr.ocr(gaussianBlur, cls=False, det=True, rec=True) + +# Extract OCR text +ocr_text = '' +for line in result: + for word in line: + ocr_text += f"{word[1][0]}" + +# Remove all characters that are not "-" and integers from OCR text +pattern = r"[^-\d]+" +ocr_text = re.sub(pattern, "", ocr_text) +if ocr_text: + incline = ocr_text +else: + incline = 'None' + +print(incline) diff --git a/src/windows/zwift-workout.py b/src/windows/zwift-workout.py new file mode 100644 index 000000000..b5f2c76b8 --- /dev/null +++ b/src/windows/zwift-workout.py @@ -0,0 +1,71 @@ +# iFit-Workout - Auto-incline and auto-speed control of treadmill via ADB and OCR for Zwift workouts +# Author: Al Udell +# Revised: August 16, 2023 + +# zwift-workout.py - take Zwift screenshot, crop speed/incline instruction, OCR speed/incline + +# imports +import cv2 +import numpy as np +import re +from datetime import datetime +from paddleocr import PaddleOCR +from PIL import Image, ImageGrab + +# Take Zwift screenshot +screenshot = ImageGrab.grab() + +# Scale image to 3000 x 2000 +screenshot = screenshot.resize((3000, 2000)) + +# Crop image to workout instruction area +screenwidth, screenheight = screenshot.size + +# Values for Zwift workout instructions +col1 = int(screenwidth/3000 * 1010) +row1 = int(screenheight/2000 * 260) +col2 = int(screenwidth/3000 * 1285) +row2 = int(screenheight/2000 * 480) + +cropped = screenshot.crop((col1, row1, col2, row2)) + +# Scale image to correct size for borderless window mode +width, height = cropped.size +cropped = cropped.resize((int(width * 0.99), int(height * 0.99))) + +# Convert image to np array +cropped_np = np.array(cropped) + +# OCR image +ocr = PaddleOCR(lang='en', use_gpu=False, enable_mkldnn=True, use_angle_cls=False, table=False, layout=False, show_log=False) +result = ocr.ocr(cropped_np, cls=False, det=True, rec=True) + +# Extract OCR text +ocr_text = '' +for line in result: + for word in line: + ocr_text += f"{word[1][0]} " + +# Find the speed number +num_pattern = r'\d+(\.\d+)?' # Regular expression pattern to match numbers with optional decimal places +unit_pattern = r'\s+(kph|mph)' # Regular expression pattern to match "kph" or "mph" units +speed_match = re.search(num_pattern + unit_pattern, ocr_text) +if speed_match: + speed = speed_match.group(0) + pattern = r'\d+\.\d+' + speed = re.findall(pattern, speed)[0] +else: + speed = 'None' + +# Find the incline number +incline_pattern = r'-?\d+\s*%' # Regular expression pattern to match numbers with "%" +incline_match = re.search(incline_pattern, ocr_text) +if incline_match: + incline = incline_match.group(0) + pattern = r'-?\d+' + incline = re.findall(pattern, incline)[0] +else: + incline = 'None' + +print(f"{speed};{incline}") + diff --git a/src/windows_zwift_incline_paddleocr_thread.cpp b/src/windows_zwift_incline_paddleocr_thread.cpp new file mode 100644 index 000000000..33b49338f --- /dev/null +++ b/src/windows_zwift_incline_paddleocr_thread.cpp @@ -0,0 +1,53 @@ +#include "windows_zwift_incline_paddleocr_thread.h" +#include "elliptical.h" +#include "treadmill.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +windows_zwift_incline_paddleocr_thread::windows_zwift_incline_paddleocr_thread(bluetoothdevice *device) { + this->device = device; + emit debug("windows_zwift_incline_paddleocr_thread created!"); +} + +void windows_zwift_incline_paddleocr_thread::run() { + while (1) { + QSettings settings; + QString ret; + if (settings.value(QZSettings::zwift_ocr_climb_portal, QZSettings::default_zwift_ocr_climb_portal).toBool()) + ret = runPython("zwift-incline-climb-portal.py"); + else + ret = runPython("zwift-incline.py"); + if (!ret.toUpper().contains("NONE") && ret.length() > 0) { + emit debug("windows_zwift_incline_paddleocr_thread onInclination " + QString::number(ret.toFloat())); + emit onInclination(ret.toFloat(), ret.toFloat()); + } + msleep(100); + } +} + +QString windows_zwift_incline_paddleocr_thread::runPython(QString command) { +#ifdef Q_OS_WINDOWS + QProcess process; + qDebug() << "run >> " << command; + process.start("python\\x64\\python.exe", QStringList(command.split(' '))); + process.waitForFinished(-1); // will wait forever until finished + + QString out = process.readAllStandardOutput(); + QString err = process.readAllStandardError(); + + emit debug("python << OUT " + out); + emit debug("python << ERR " + err); +#else + QString out; +#endif + return out; +} diff --git a/src/windows_zwift_incline_paddleocr_thread.h b/src/windows_zwift_incline_paddleocr_thread.h new file mode 100644 index 000000000..96f9a876f --- /dev/null +++ b/src/windows_zwift_incline_paddleocr_thread.h @@ -0,0 +1,38 @@ +#ifndef WINDOWS_ZWIFT_INCLINE_PADDLEOCR_THREAD_H +#define WINDOWS_ZWIFT_INCLINE_PADDLEOCR_THREAD_H +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include "bluetoothdevice.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class windows_zwift_incline_paddleocr_thread : public QThread { + Q_OBJECT + + public: + explicit windows_zwift_incline_paddleocr_thread(bluetoothdevice *device); + + void run(); + + signals: + void onInclination(double inclination, double grade); + void debug(QString string); + + private: + double inclination = 0; + bluetoothdevice *device; + QString runPython(QString command); +}; + +#endif // WINDOWS_ZWIFT_INCLINE_PADDLEOCR_THREAD_H diff --git a/src/windows_zwift_workout_paddleocr_thread.cpp b/src/windows_zwift_workout_paddleocr_thread.cpp new file mode 100644 index 000000000..a7ea7313a --- /dev/null +++ b/src/windows_zwift_workout_paddleocr_thread.cpp @@ -0,0 +1,64 @@ +#include "windows_zwift_workout_paddleocr_thread.h" +#include "elliptical.h" +#include "treadmill.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +windows_zwift_workout_paddleocr_thread::windows_zwift_workout_paddleocr_thread(bluetoothdevice *device) { + this->device = device; + emit debug("windows_zwift_workout_paddleocr_thread created!"); +} + +void windows_zwift_workout_paddleocr_thread::run() { + float lastInclination = -100; + float lastSpeed = -100; + while (1) { + QString ret = runPython("zwift-workout.py"); + if (ret.length() > 0) { + QStringList list = ret.split(";"); + if (list.length() >= 2) { + emit debug("windows_zwift_workout_paddleocr_thread onInclination " + list.at(1) + " onSpeed " + + list.at(0).toFloat()); + if (!list.at(1).toUpper().contains("NONE")) { + float inc = list.at(1).toFloat(); + if (inc != lastInclination) + emit onInclination(inc, inc); + lastInclination = inc; + } + if (!list.at(0).toUpper().contains("NONE")) { + float speed = list.at(0).toFloat(); + if (speed != lastSpeed) + emit onSpeed(speed); + lastSpeed = speed; + } + } + } + } +} + +QString windows_zwift_workout_paddleocr_thread::runPython(QString command) { +#ifdef Q_OS_WINDOWS + QProcess process; + qDebug() << "run >> " << command; + process.start("python\\x64\\python.exe", QStringList(command.split(' '))); + process.waitForFinished(-1); // will wait forever until finished + + QString out = process.readAllStandardOutput(); + QString err = process.readAllStandardError(); + + emit debug("python << OUT " + out); + emit debug("python << ERR " + err); +#else + QString out; +#endif + return out; +} diff --git a/src/windows_zwift_workout_paddleocr_thread.h b/src/windows_zwift_workout_paddleocr_thread.h new file mode 100644 index 000000000..ba5c4fd9c --- /dev/null +++ b/src/windows_zwift_workout_paddleocr_thread.h @@ -0,0 +1,41 @@ +#ifndef WINDOWS_ZWIFT_WORKOUT_PADDLEOCR_THREAD_H +#define WINDOWS_ZWIFT_WORKOUT_PADDLEOCR_THREAD_H + +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include "bluetoothdevice.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class windows_zwift_workout_paddleocr_thread : public QThread { + Q_OBJECT + + public: + explicit windows_zwift_workout_paddleocr_thread(bluetoothdevice *device); + + void run(); + + signals: + void onInclination(double inclination, double grade); + void onSpeed(double speed); + void debug(QString string); + + private: + double inclination = 0; + double speed = 0; + bluetoothdevice *device; + QString runPython(QString command); +}; + +#endif // WINDOWS_ZWIFT_WORKOUT_PADDLEOCR_THREAD_H diff --git a/src/yesoulbike.cpp b/src/yesoulbike.cpp index aebd24a3a..30148e974 100644 --- a/src/yesoulbike.cpp +++ b/src/yesoulbike.cpp @@ -1,6 +1,8 @@ #include "yesoulbike.h" -#include "ios/lockscreen.h" + +#ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#endif #include "virtualbike.h" #include #include @@ -38,12 +40,15 @@ void yesoulbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStr timeout.singleShot(300ms, &loop, &QEventLoop::quit); } - gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, - QByteArray((const char *)data, data_len)); + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); if (!disable_log) { - emit debug(QStringLiteral(" >> ") + QByteArray((const char *)data, data_len).toHex(' ') + - QStringLiteral(" // ") + info); + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); } loop.exec(); @@ -132,11 +137,14 @@ void yesoulbike::characteristicChanged(const QLowEnergyCharacteristic &character if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { Speed = 0.37497622 * ((double)Cadence.value()); } else { - Speed = metric::calculateSpeedFromPower(watts(), Inclination.value(), Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); } if (watts()) KCal += - ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in kg @@ -145,9 +153,10 @@ void yesoulbike::characteristicChanged(const QLowEnergyCharacteristic &character ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); if (!settings.value(QZSettings::yesoul_peloton_formula, QZSettings::default_yesoul_peloton_formula).toBool()) { - m_pelotonResistance = - ((Resistance.value() * 0.88) * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + - settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); // 15% lower than yesoul bike + m_pelotonResistance = ((Resistance.value() * 0.88) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset) + .toDouble(); // 15% lower than yesoul bike } else { m_pelotonResistance = (((Resistance.value() * 1.1099) - 20.769) * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + @@ -168,23 +177,15 @@ void yesoulbike::characteristicChanged(const QLowEnergyCharacteristic &character #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { -#ifdef Q_OS_IOS -#ifndef IO_UNDER_QT - lockscreen h; - long appleWatchHeartRate = h.heartRate(); - h.setKcal(KCal.value()); - h.setDistance(Distance.value()); - Heart = appleWatchHeartRate; - debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); -#endif -#endif + update_hr_from_external(); } } #ifdef Q_OS_IOS #ifndef IO_UNDER_QT bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && firstStateChanged) { h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); @@ -253,7 +254,7 @@ void yesoulbike::stateChanged(QLowEnergyService::ServiceState state) { &yesoulbike::descriptorWritten); // ******************************************* virtual bike init ************************************* - if (!firstStateChanged && !virtualBike + if (!firstStateChanged && !this->hasVirtualDevice() #ifdef Q_OS_IOS #ifndef IO_UNDER_QT && !h @@ -261,11 +262,14 @@ void yesoulbike::stateChanged(QLowEnergyService::ServiceState state) { #endif ) { QSettings settings; - bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); #ifdef Q_OS_IOS #ifndef IO_UNDER_QT - bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); - bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence) { qDebug() << "ios_peloton_workaround activated!"; h = new lockscreen(); @@ -275,9 +279,10 @@ void yesoulbike::stateChanged(QLowEnergyService::ServiceState state) { #endif if (virtual_device_enabled) { emit debug(QStringLiteral("creating virtual bike interface...")); - virtualBike = new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); // connect(virtualBike,&virtualbike::debug ,this,&yesoulbike::debug); connect(virtualBike, &virtualbike::changeInclination, this, &yesoulbike::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); } } firstStateChanged = 1; @@ -323,7 +328,7 @@ void yesoulbike::error(QLowEnergyController::Error err) { void yesoulbike::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + device.address().toString() + ')'); - //if (device.name().startsWith(QStringLiteral("YESOUL"))) + // if (device.name().startsWith(QStringLiteral("YESOUL"))) { bluetoothDevice = device; @@ -367,10 +372,6 @@ bool yesoulbike::connected() { return m_control->state() == QLowEnergyController::DiscoveredState; } -void *yesoulbike::VirtualBike() { return virtualBike; } - -void *yesoulbike::VirtualDevice() { return VirtualBike(); } - uint16_t yesoulbike::watts() { if (currentCadence().value() == 0) { return 0; diff --git a/src/yesoulbike.h b/src/yesoulbike.h index f146e8db2..dfbfb65ab 100644 --- a/src/yesoulbike.h +++ b/src/yesoulbike.h @@ -27,7 +27,6 @@ #include #include "bike.h" -#include "virtualbike.h" #ifdef Q_OS_IOS #include "ios/lockscreen.h" @@ -43,10 +42,7 @@ class yesoulbike : public bike { yesoulbike(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, double bikeResistanceGain); - bool connected(); - - void *VirtualBike(); - void *VirtualDevice(); + bool connected() override; private: double GetDistanceFromPacket(const QByteArray &packet); @@ -56,10 +52,9 @@ class yesoulbike : public bike { bool wait_for_response = false); void startDiscover(); void sendPoll(); - uint16_t watts(); + uint16_t watts() override; QTimer *refresh; - virtualbike *virtualBike = nullptr; QLowEnergyService *gattCommunicationChannelService = nullptr; QLowEnergyCharacteristic gattWriteCharacteristic; diff --git a/src/ypooelliptical.cpp b/src/ypooelliptical.cpp new file mode 100644 index 000000000..ff0c23dd8 --- /dev/null +++ b/src/ypooelliptical.cpp @@ -0,0 +1,671 @@ +#include "ypooelliptical.h" +#include "ftmsbike.h" +#include "ios/lockscreen.h" +#include "virtualtreadmill.h" +#include +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_ANDROID +#include +#endif +#include "keepawakehelper.h" +#include + +using namespace std::chrono_literals; + +ypooelliptical::ypooelliptical(bool noWriteResistance, bool noHeartService, uint8_t bikeResistanceOffset, + double bikeResistanceGain) { + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->bikeResistanceGain = bikeResistanceGain; + this->bikeResistanceOffset = bikeResistanceOffset; + initDone = false; + connect(refresh, &QTimer::timeout, this, &ypooelliptical::update); + refresh->start(200ms); + + // this bike doesn't send resistance, so I have to use the default value + Resistance = default_resistance; +} + +void ypooelliptical::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response) { + QEventLoop loop; + QTimer timeout; + + if (!gattCustomService) { + qDebug() << "gattCustomService nullptr"; + return; + } + + if (wait_for_response) { + connect(gattCustomService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } else { + connect(gattCustomService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCustomService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); + } + + loop.exec(); +} + +void ypooelliptical::forceResistance(resistance_t requestResistance) { + + uint8_t write[] = {0x02, 0x44, 0x05, 0x01, 0x00, 0x40, 0x03}; + + write[3] = (uint8_t)(requestResistance); + write[5] = (uint8_t)(0x39 + requestResistance); + + writeCharacteristic(write, sizeof(write), QStringLiteral("forceResistance ") + QString::number(requestResistance)); + + // this bike doesn't send resistance, so I have to use the value forced + Resistance = requestResistance; +} + +void ypooelliptical::update() { + if (m_control->state() == QLowEnergyController::UnconnectedState) { + emit disconnected(); + return; + } + + if (initRequest) { + initRequest = false; + uint8_t init1[] = {0x02, 0x42, 0x42, 0x03}; + uint8_t init2[] = {0x02, 0x41, 0x02, 0x43, 0x03}; + uint8_t init3[] = {0x02, 0x43, 0x01, 0x42, 0x03}; + uint8_t init4[] = {0x02, 0x44, 0x01, 0x45, 0x03}; + uint8_t init5[] = {0x02, 0x44, 0x05, 0x01, 0x00, 0x40, 0x03}; + + writeCharacteristic(init1, sizeof(init1), QStringLiteral("init"), false, true); + writeCharacteristic(init2, sizeof(init2), QStringLiteral("init"), false, true); + writeCharacteristic(init3, sizeof(init3), QStringLiteral("init"), false, true); + writeCharacteristic(init1, sizeof(init1), QStringLiteral("init"), false, true); + writeCharacteristic(init4, sizeof(init4), QStringLiteral("init"), false, true); + writeCharacteristic(init3, sizeof(init3), QStringLiteral("init"), false, true); + writeCharacteristic(init5, sizeof(init5), QStringLiteral("init"), false, true); + writeCharacteristic(init1, sizeof(init1), QStringLiteral("init"), false, true); + writeCharacteristic(init5, sizeof(init5), QStringLiteral("init"), false, true); + } else if (bluetoothDevice.isValid() && + m_control->state() == QLowEnergyController::DiscoveredState //&& + // gattCommunicationChannelService && + // gattWriteCharacteristic.isValid() && + // gattNotify1Characteristic.isValid() && + /*initDone*/) { + update_metrics(false, watts()); + + // updating the treadmill console every second + if (sec1Update++ == (500 / refresh->interval())) { + sec1Update = 0; + // updateDisplay(elapsed); + } + + uint8_t init1[] = {0x02, 0x42, 0x42, 0x03}; + uint8_t init3[] = {0x02, 0x43, 0x01, 0x42, 0x03}; + + if (counterPoll == 0) + writeCharacteristic(init1, sizeof(init1), QStringLiteral("init"), false, true); + else + writeCharacteristic(init3, sizeof(init3), QStringLiteral("init"), false, true); + + counterPoll++; + if (counterPoll > 1) + counterPoll = 0; + + if (requestResistance != -1) { + if (requestResistance > max_resistance) { + requestResistance = max_resistance; + } + + if (requestResistance != currentResistance().value()) { + auto virtualBike = dynamic_cast(this->VirtualDevice()); + if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) { + emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance)); + forceResistance(requestResistance); + } + } + requestResistance = -1; + } + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + emit bikeStarted(); + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } + } +} + +void ypooelliptical::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newvalue) { + // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length(); + Q_UNUSED(characteristic); + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + + emit debug(QStringLiteral(" << ") + newvalue.toHex(' ')); + + if (characteristic.uuid() == QBluetoothUuid::HeartRate && newvalue.length() > 1) { + Heart = (uint8_t)newvalue[1]; + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); + return; + } + + union flags { + struct { + uint32_t moreData : 1; + uint32_t avgSpeed : 1; + uint32_t totDistance : 1; + uint32_t stepCount : 1; + uint32_t strideCount : 1; + uint32_t elevationGain : 1; + uint32_t rampAngle : 1; + uint32_t resistanceLvl : 1; + uint32_t instantPower : 1; + uint32_t avgPower : 1; + uint32_t expEnergy : 1; + uint32_t heartRate : 1; + uint32_t metabolicEq : 1; + uint32_t elapsedTime : 1; + uint32_t remainingTime : 1; + uint32_t movementDirection : 1; + uint32_t spare : 8; + }; + + uint32_t word_flags; + }; + + flags Flags; + + if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACE)) { + + if (newvalue.length() == 18) { + qDebug() << QStringLiteral("let's wait for the next piece of frame"); + lastPacket = newvalue; + return; + } else if (newvalue.length() == 17) { + lastPacket.append(newvalue); + } else { + qDebug() << "packet not handled!!"; + return; + } + + int index = 0; + Flags.word_flags = (lastPacket.at(2) << 16) | (lastPacket.at(1) << 8) | lastPacket.at(0); + index += 3; + + if (!Flags.moreData) { + /*Speed = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | + (uint16_t)((uint8_t)newValue.at(index)))) / + 100.0; + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));*/ + index += 2; + } + + // this particular device, seems to send the actual speed here + if (Flags.avgSpeed) { + // double avgSpeed; + Speed = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint16_t)((uint8_t)lastPacket.at(index)))) / + 100.0; + index += 2; + emit debug(QStringLiteral("Current Average Speed: ") + QString::number(Speed.value())); + } + + if (Flags.totDistance) { + Distance = ((double)((((uint32_t)((uint8_t)lastPacket.at(index + 2)) << 16) | + (uint32_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint32_t)((uint8_t)lastPacket.at(index)))) / + 1000.0; + index += 3; + } else { + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + } + + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + + if (Flags.stepCount) { + if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) { + Cadence = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint16_t)((uint8_t)lastPacket.at(index)))); + } + emit debug(QStringLiteral("Current Cadence: ") + QString::number(Cadence.value())); + + index += 2; + index += 2; + } + + if (Flags.strideCount) { + index += 2; + } + + if (Flags.elevationGain) { + index += 2; + index += 2; + } + + if (Flags.rampAngle) { + index += 2; + index += 2; + } + + if (Flags.resistanceLvl) { + Resistance = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint16_t)((uint8_t)lastPacket.at(index)))); + // emit resistanceRead(Resistance.value()); + index += 2; + emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); + } else { + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + if (Cadence.value() && m_watt.value()) { + m_pelotonResistance = + (((sqrt(pow(br, 2.0) - 4.0 * ar * + (cr - (m_watt.value() * 132.0 / + (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + Resistance = m_pelotonResistance; + // emit resistanceRead(Resistance.value()); + } + } + + if (Flags.instantPower) { + if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) + m_watt = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint16_t)((uint8_t)lastPacket.at(index)))) / + 100.0; // i added this because this device seems to send it multiplied by 100 + emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value())); + index += 2; + } + + if (Flags.avgPower && lastPacket.length() > index + 1) { + double avgPower; + avgPower = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint16_t)((uint8_t)lastPacket.at(index)))); + emit debug(QStringLiteral("Current Average Watt: ") + QString::number(avgPower)); + index += 2; + } + + if (Flags.expEnergy && lastPacket.length() > index + 1) { + KCal = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) | + (uint16_t)((uint8_t)lastPacket.at(index)))); + index += 2; + + // energy per hour + index += 2; + + // energy per minute + index += 1; + } else { + if (watts()) + KCal += ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / + ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 + } + + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else +#endif + { + if (Flags.heartRate && !disable_hr_frommachinery && lastPacket.length() > index) { + Heart = ((double)((lastPacket.at(index)))); + // index += 1; // NOTE: clang-analyzer-deadcode.DeadStores + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); + } else { + Flags.heartRate = false; + } + } + + if (Flags.metabolicEq) { + // todo + } + + if (Flags.elapsedTime) { + // todo + } + + if (Flags.remainingTime) { + // todo + } + } else { + return; + } + + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && + (!Flags.heartRate || Heart.value() == 0 || disable_hr_frommachinery)) { + update_hr_from_external(); + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT +/* + bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = settings.value(QZSettings::ios_peloton_workaround, + QZSettings::default_ios_peloton_workaround).toBool(); if (ios_peloton_workaround && cadence && h && + firstStateChanged) { h->virtualTreadmill_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualTreadmill_setHeartRate((uint8_t)metrics_override_heartrate()); + } + */ +#endif +#endif + + emit debug(QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs)); + emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); + + if (m_control->error() != QLowEnergyController::NoError) { + qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); + } +} + +void ypooelliptical::stateChanged(QLowEnergyService::ServiceState state) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state(); + if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) { + qDebug() << QStringLiteral("not all services discovered"); + return; + } + } + + qDebug() << QStringLiteral("all services discovered!"); + + for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { + if (s->state() == QLowEnergyService::ServiceDiscovered) { + // establish hook into notifications + connect(s, &QLowEnergyService::characteristicChanged, this, &ypooelliptical::characteristicChanged); + connect(s, &QLowEnergyService::characteristicWritten, this, &ypooelliptical::characteristicWritten); + connect(s, &QLowEnergyService::characteristicRead, this, &ypooelliptical::characteristicRead); + connect( + s, static_cast(&QLowEnergyService::error), + this, &ypooelliptical::errorService); + connect(s, &QLowEnergyService::descriptorWritten, this, &ypooelliptical::descriptorWritten); + connect(s, &QLowEnergyService::descriptorRead, this, &ypooelliptical::descriptorRead); + + qDebug() << s->serviceUuid() << QStringLiteral("connected!"); + + auto characteristics_list = s->characteristics(); + for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) { + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle(); + auto descriptors_list = c.descriptors(); + for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) { + qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); + } + + if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) { + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("notification subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Indicate) == + QLowEnergyCharacteristic::Indicate) { + QByteArray descriptor; + descriptor.append((char)0x02); + descriptor.append((char)0x00); + if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) { + s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } else { + qDebug() << QStringLiteral("ClientCharacteristicConfiguration") << c.uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).uuid() + << c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).handle() + << QStringLiteral(" is not valid"); + } + + qDebug() << s->serviceUuid() << c.uuid() << QStringLiteral("indication subscribed!"); + } else if ((c.properties() & QLowEnergyCharacteristic::Read) == QLowEnergyCharacteristic::Read) { + // s->readCharacteristic(c); + // qDebug() << s->serviceUuid() << c.uuid() << "reading!"; + } + + QBluetoothUuid _gattWriteCharControlPointId((quint16)0xFFF2); + if (c.uuid() == _gattWriteCharControlPointId) { + qDebug() << QStringLiteral("Custom service and Control Point found"); + gattWriteCharControlPointId = c; + gattCustomService = s; + } + } + } + } + + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + QSettings settings; + if (!this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); + if (virtual_device_enabled) { + if (!virtual_device_force_bike) { + debug("creating virtual treadmill interface..."); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); + connect(virtualTreadmill, &virtualtreadmill::debug, this, &ypooelliptical::debug); + connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, + &ypooelliptical::changeInclinationRequested); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + debug("creating virtual bike interface..."); + auto virtualBike = new virtualbike(this); + connect(virtualBike, &virtualbike::changeInclination, this, + &ypooelliptical::changeInclinationRequested); + connect(virtualBike, &virtualbike::changeInclination, this, &ypooelliptical::changeInclination); + /*connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, + &ypooelliptical::ftmsCharacteristicChanged);*/ + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); + } + } + } + } + firstStateChanged = 1; + // ******************************************************************************************************** +} + +void ypooelliptical::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} + +/* +void ypooelliptical::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, + const QByteArray &newValue) { + QByteArray b = newValue; + if (gattWriteCharControlPointId.isValid()) { + qDebug() << "routing FTMS packet to the bike from virtualBike" << characteristic.uuid() << newValue.toHex(' '); + + // handling reading current resistance + if (b.at(0) == 0x11) { + int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8)); + Resistance = (slope / 33) + default_resistance; + } + + gattCustomService->writeCharacteristic(gattWriteCharControlPointId, b); + } +}*/ + +void ypooelliptical::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' ')); + + if (gattCustomService != nullptr) { + initRequest = true; + emit connectedAndDiscovered(); + } +} + +void ypooelliptical::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + qDebug() << QStringLiteral("descriptorRead ") << descriptor.name() << descriptor.uuid() << newValue.toHex(' '); +} + +void ypooelliptical::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + Q_UNUSED(characteristic); + emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' ')); +} + +void ypooelliptical::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + qDebug() << QStringLiteral("characteristicRead ") << characteristic.uuid() << newValue.toHex(' '); +} + +void ypooelliptical::serviceScanDone(void) { + emit debug(QStringLiteral("serviceScanDone")); + +#ifdef Q_OS_ANDROID + QLowEnergyConnectionParameters c; + c.setIntervalRange(24, 40); + c.setLatency(0); + c.setSupervisionTimeout(420); + m_control->requestConnectionUpdate(c); +#endif + + initRequest = false; + auto services_list = m_control->services(); + for (const QBluetoothUuid &s : qAsConst(services_list)) { + gattCommunicationChannelService.append(m_control->createServiceObject(s)); + connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, + &ypooelliptical::stateChanged); + gattCommunicationChannelService.constLast()->discoverDetails(); + } +} + +void ypooelliptical::errorService(QLowEnergyService::ServiceError err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("ypooelliptical::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void ypooelliptical::error(QLowEnergyController::Error err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("ypooelliptical::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void ypooelliptical::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + + device.address().toString() + ')'); + { + bluetoothDevice = device; + + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); + connect(m_control, &QLowEnergyController::serviceDiscovered, this, &ypooelliptical::serviceDiscovered); + connect(m_control, &QLowEnergyController::discoveryFinished, this, &ypooelliptical::serviceScanDone); + connect(m_control, + static_cast(&QLowEnergyController::error), + this, &ypooelliptical::error); + connect(m_control, &QLowEnergyController::stateChanged, this, &ypooelliptical::controllerStateChanged); + + connect(m_control, + static_cast(&QLowEnergyController::error), + this, [this](QLowEnergyController::Error error) { + Q_UNUSED(error); + Q_UNUSED(this); + emit debug(QStringLiteral("Cannot connect to remote device.")); + emit disconnected(); + }); + connect(m_control, &QLowEnergyController::connected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("Controller connected. Search services...")); + m_control->discoverServices(); + }); + connect(m_control, &QLowEnergyController::disconnected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("LowEnergy controller disconnected")); + emit disconnected(); + }); + + // Connect + m_control->connectToDevice(); + return; + } +} + +bool ypooelliptical::connected() { + if (!m_control) { + return false; + } + return m_control->state() == QLowEnergyController::DiscoveredState; +} + +uint16_t ypooelliptical::watts() { + if (currentCadence().value() == 0) { + return 0; + } + + return m_watt.value(); +} + +void ypooelliptical::controllerStateChanged(QLowEnergyController::ControllerState state) { + qDebug() << QStringLiteral("controllerStateChanged") << state; + if (state == QLowEnergyController::UnconnectedState && m_control) { + qDebug() << QStringLiteral("trying to connect back again..."); + initDone = false; + m_control->connectToDevice(); + } +} diff --git a/src/ypooelliptical.h b/src/ypooelliptical.h new file mode 100644 index 000000000..8b3dc1927 --- /dev/null +++ b/src/ypooelliptical.h @@ -0,0 +1,104 @@ +#ifndef YPOOELLIPTICAL_H +#define YPOOELLIPTICAL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include + +#include "elliptical.h" +#include "virtualbike.h" +#include "virtualtreadmill.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class ypooelliptical : public elliptical { + Q_OBJECT + public: + ypooelliptical(bool noWriteResistance = false, bool noHeartService = false, uint8_t bikeResistanceOffset = 4, + double bikeResistanceGain = 1.0); + bool connected() override; + + private: + void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, + bool wait_for_response = false); + void startDiscover(); + uint16_t watts(); + void forceResistance(resistance_t requestResistance); + + QTimer *refresh; + + QList gattCommunicationChannelService; + QLowEnergyCharacteristic gattWriteCharControlPointId; + QLowEnergyService *gattCustomService = nullptr; + + uint8_t sec1Update = 0; + QByteArray lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + uint8_t bikeResistanceOffset = 4; + double bikeResistanceGain = 1.0; + const uint8_t max_resistance = 72; // 24; + const uint8_t default_resistance = 6; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + + uint8_t counterPoll = 0; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + Q_SIGNALS: + void disconnected(); + void debug(QString string); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + + private slots: + + void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void stateChanged(QLowEnergyService::ServiceState state); + void controllerStateChanged(QLowEnergyController::ControllerState state); + + void serviceDiscovered(const QBluetoothUuid &gatt); + void serviceScanDone(void); + void update(); + void error(QLowEnergyController::Error err); + void errorService(QLowEnergyService::ServiceError); + void changeInclinationRequested(double grade, double percentage); + // void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); +}; + +#endif // YPOOELLIPTICAL_H diff --git a/src/ziprotreadmill.cpp b/src/ziprotreadmill.cpp new file mode 100644 index 000000000..da2196c35 --- /dev/null +++ b/src/ziprotreadmill.cpp @@ -0,0 +1,408 @@ +#include "ziprotreadmill.h" +#include "keepawakehelper.h" +#include "virtualtreadmill.h" +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +ziprotreadmill::ziprotreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed, + double forceInitInclination) { + + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + this->noConsole = noConsole; + this->noHeartService = noHeartService; + + if (forceInitSpeed > 0) + lastSpeed = forceInitSpeed; + + if (forceInitInclination > 0) + lastInclination = forceInitInclination; + + refresh = new QTimer(this); + initDone = false; + connect(refresh, &QTimer::timeout, this, &ziprotreadmill::update); + refresh->start(500ms); +} + +void ziprotreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response) { + QEventLoop loop; + QTimer timeout; + + if (wait_for_response) { + connect(this, &ziprotreadmill::packetReceived, &loop, &QEventLoop::quit); + timeout.singleShot(400ms, &loop, &QEventLoop::quit); + } else { + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(400ms, &loop, &QEventLoop::quit); + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); + } + + // packets sent from the characChanged event, i don't want to block everything + if (wait_for_response) { + loop.exec(); + + if (timeout.isActive() == false) + emit debug(QStringLiteral(" exit for timeout")); + } +} + +void ziprotreadmill::updateDisplay(uint16_t elapsed) { + Q_UNUSED(elapsed); + // uint8_t noOpData[] = {0x55, 0x0d, 0x0a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + + // writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("noOp")); +} + +void ziprotreadmill::forceIncline(double requestIncline) { + // uint8_t incline[] = {0x55, 0x11, 0x01, 0x06}; + // incline[3] = (uint8_t)requestIncline; + // writeCharacteristic(incline, sizeof(incline), QStringLiteral("forceIncline ") + QString::number(requestIncline)); +} + +double ziprotreadmill::minStepInclination() { return 1.0; } +double ziprotreadmill::minStepSpeed() { return 0.1; } + +void ziprotreadmill::forceSpeed(double requestSpeed) { + // uint8_t speed[] = {0x55, 0x0f, 0x02, 0x08, 0x00}; + // speed[3] = (uint8_t)requestSpeed; + // writeCharacteristic(speed, sizeof(speed), QStringLiteral("forceSpeed ") + QString::number(requestSpeed)); +} + +void ziprotreadmill::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} + +void ziprotreadmill::update() { + if (m_control->state() == QLowEnergyController::UnconnectedState) { + emit disconnected(); + return; + } + + qDebug() << m_control->state() << bluetoothDevice.isValid() << gattCommunicationChannelService + << gattWriteCharacteristic.isValid() << initDone << requestSpeed << requestInclination; + + if (initRequest) { + initRequest = false; + btinit((lastSpeed > 0 ? true : false)); + } else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState && + gattCommunicationChannelService && gattWriteCharacteristic.isValid() && initDone) { + QSettings settings; + // ******************************************* virtual treadmill init ************************************* + if (!firstInit && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + if (virtual_device_enabled) { + emit debug(QStringLiteral("creating virtual treadmill interface...")); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); + connect(virtualTreadMill, &virtualtreadmill::debug, this, &ziprotreadmill::debug); + connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, + &ziprotreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); + firstInit = 1; + } + } + // ******************************************************************************************************** + + update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())); + + uint8_t noop[] = {0xfb, 0x07, 0xa1, 0x02, 0x00, 0x00, 0x00, 0xaa, 0xfc}; + noop[5] = (uint8_t)(Speed.value() * 10.0); + if (requestSpeed != -1) { + noop[4] = 1; // force speed and inclination + noop[5] = (uint8_t)(requestSpeed * 10.0); + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); + requestSpeed = -1; + } + noop[6] = (uint8_t)(Inclination.value()); + if (requestInclination != -100) { + noop[4] = 1; // force speed and inclination + noop[6] = (uint8_t)(requestInclination); + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + requestInclination = -100; + } + noop[7] += noop[5] + noop[6] + noop[4]; + writeCharacteristic(noop, sizeof(noop), "noop", false, false); + + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + lastStart = QDateTime::currentMSecsSinceEpoch(); + uint8_t start[] = {0xfb, 0x05, 0xa2, 0x01, 0x01, 0xa9, 0xfc}; + writeCharacteristic(start, sizeof(start), "start", false, true); + writeCharacteristic(start, sizeof(start), "start", false, true); + if (lastSpeed == 0.0) { + lastSpeed = 0.8; + } + requestStart = -1; + emit tapeStarted(); + } else if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + uint8_t stop[] = {0xfb, 0x05, 0xa2, 0x04, 0x01, 0xac, 0xfc}; + writeCharacteristic(stop, sizeof(stop), "stop", false, true); + writeCharacteristic(stop, sizeof(stop), "stop", false, true); + writeCharacteristic(stop, sizeof(stop), "stop", false, true); + requestStop = -1; + lastStop = QDateTime::currentMSecsSinceEpoch(); + } else if (sec1Update++ >= (400 / refresh->interval())) { + updateDisplay(elapsed.value()); + sec1Update = 0; + } + } +} + +void ziprotreadmill::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void ziprotreadmill::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + // qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length(); + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + Q_UNUSED(characteristic); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + QByteArray value = newValue; + + emit debug(QStringLiteral(" << ") + QString::number(value.length()) + QStringLiteral(" ") + value.toHex(' ')); + + emit packetReceived(); + + if ((newValue.length() != 18)) + return; + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else +#endif + { + uint8_t heart = ((uint8_t)value.at(15)); + if (heart == 0 || disable_hr_frommachinery) { + update_hr_from_external(); + } else { + Heart = heart; + } + } + + double speed = ((double)((uint8_t)newValue.at(5))) / 10.0; + emit debug(QStringLiteral("Current speed: ") + QString::number(speed)); + + Inclination = ((double)newValue.at(6)); + emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value())); + + if (Speed.value() != speed) { + emit speedChanged(speed); + } + Speed = speed; + + if (speed > 0) { + lastSpeed = speed; + } + + // this treadmill has a bug that always send 1km/h even if the tape is stopped + if (speed > 1.0) { + lastStart = 0; + } else { + lastStop = 0; + } + + if (!firstCharacteristicChanged) { + if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + KCal += + ((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 + + Distance += ((Speed.value() / 3600.0) / + (1000.0 / (lastTimeCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); + } + + cadenceFromAppleWatch(); + + emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value())); + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + + if (m_control->error() != QLowEnergyController::NoError) { + qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); + } + + lastTimeCharacteristicChanged = QDateTime::currentDateTime(); + firstCharacteristicChanged = false; +} + +void ziprotreadmill::btinit(bool startTape) { + Q_UNUSED(startTape) + uint8_t initData1[] = {0xfb, 0x05, 0xa0, 0x00, 0x01, 0xa6, 0xfc}; + uint8_t initData2[] = {0xfb, 0x05, 0xa0, 0x01, 0x01, 0xa7, 0xfc}; + uint8_t initData3[] = {0xfb, 0x05, 0xa0, 0x02, 0x01, 0xa8, 0xfc}; + uint8_t initData4[] = {0xfb, 0x05, 0xa0, 0x03, 0x01, 0xa9, 0xfc}; + uint8_t initData5[] = {0xfb, 0x05, 0xa1, 0x00, 0x01, 0xa7, 0xfc}; + uint8_t initData6[] = {0xfb, 0x06, 0xa1, 0x00, 0x00, 0x00, 0xa7, 0xfc}; + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true); + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, true); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, true); + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, true); + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, true); + + initDone = true; +} + +void ziprotreadmill::stateChanged(QLowEnergyService::ServiceState state) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); + if (state == QLowEnergyService::ServiceDiscovered) { + QBluetoothUuid _gattWriteCharacteristicId((quint16)0xfff2); + QBluetoothUuid _gattNotify1CharacteristicId((quint16)0xfff1); + + // qDebug() << gattCommunicationChannelService->characteristics(); + + gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId); + gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId); + Q_ASSERT(gattWriteCharacteristic.isValid()); + Q_ASSERT(gattNotify1Characteristic.isValid()); + + // establish hook into notifications + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this, + &ziprotreadmill::characteristicChanged); + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this, + &ziprotreadmill::characteristicWritten); + connect(gattCommunicationChannelService, + static_cast(&QLowEnergyService::error), + this, &ziprotreadmill::errorService); + connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this, + &ziprotreadmill::descriptorWritten); + + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + gattCommunicationChannelService->writeDescriptor( + gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } +} + +void ziprotreadmill::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ')); + + initRequest = true; + emit connectedAndDiscovered(); +} + +void ziprotreadmill::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + Q_UNUSED(characteristic); + emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' ')); +} + +void ziprotreadmill::serviceScanDone(void) { + emit debug(QStringLiteral("serviceScanDone")); + + QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0); + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); + connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &ziprotreadmill::stateChanged); + gattCommunicationChannelService->discoverDetails(); +} + +void ziprotreadmill::errorService(QLowEnergyService::ServiceError err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("ziprotreadmill::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void ziprotreadmill::error(QLowEnergyController::Error err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("ziprotreadmill::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void ziprotreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { + emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + + device.address().toString() + ')'); + { + bluetoothDevice = device; + + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); + connect(m_control, &QLowEnergyController::serviceDiscovered, this, &ziprotreadmill::serviceDiscovered); + connect(m_control, &QLowEnergyController::discoveryFinished, this, &ziprotreadmill::serviceScanDone); + connect(m_control, + static_cast(&QLowEnergyController::error), + this, &ziprotreadmill::error); + connect(m_control, &QLowEnergyController::stateChanged, this, &ziprotreadmill::controllerStateChanged); + + connect(m_control, + static_cast(&QLowEnergyController::error), + this, [this](QLowEnergyController::Error error) { + Q_UNUSED(error); + Q_UNUSED(this); + emit debug(QStringLiteral("Cannot connect to remote device.")); + emit disconnected(); + }); + connect(m_control, &QLowEnergyController::connected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("Controller connected. Search services...")); + m_control->discoverServices(); + }); + connect(m_control, &QLowEnergyController::disconnected, this, [this]() { + Q_UNUSED(this); + emit debug(QStringLiteral("LowEnergy controller disconnected")); + emit disconnected(); + }); + + // Connect + m_control->connectToDevice(); + return; + } +} + +void ziprotreadmill::controllerStateChanged(QLowEnergyController::ControllerState state) { + qDebug() << QStringLiteral("controllerStateChanged") << state; + if (state == QLowEnergyController::UnconnectedState && m_control) { + qDebug() << QStringLiteral("trying to connect back again..."); + initDone = false; + m_control->connectToDevice(); + } +} + +bool ziprotreadmill::connected() { + if (!m_control) { + return false; + } + return m_control->state() == QLowEnergyController::DiscoveredState; +} + +bool ziprotreadmill::autoPauseWhenSpeedIsZero() { + if (lastStart == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStart + 10000)) + return true; + else + return false; +} + +bool ziprotreadmill::autoStartWhenSpeedIsGreaterThenZero() { + if ((lastStop == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStop + 25000)) && requestStop == -1) + return true; + else + return false; +} diff --git a/src/ziprotreadmill.h b/src/ziprotreadmill.h new file mode 100644 index 000000000..8cfe3f43d --- /dev/null +++ b/src/ziprotreadmill.h @@ -0,0 +1,97 @@ +#ifndef ZIPROTREADMILL_H +#define ZIPROTREADMILL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include + +#include "treadmill.h" + +class ziprotreadmill : public treadmill { + Q_OBJECT + public: + ziprotreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, + double forceInitSpeed = 0.0, double forceInitInclination = 0.0); + bool connected() override; + double minStepInclination() override; + double minStepSpeed() override; + bool autoPauseWhenSpeedIsZero() override; + bool autoStartWhenSpeedIsGreaterThenZero() override; + bool canStartStop() override{ return false; } + + private: + void forceSpeed(double requestSpeed); + void forceIncline(double requestIncline); + void updateDisplay(uint16_t elapsed); + void btinit(bool startTape); + void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, + bool wait_for_response = false); + void startDiscover(); + bool noConsole = false; + bool noHeartService = false; + uint32_t pollDeviceTime = 200; + uint8_t sec1Update = 0; + uint8_t firstInit = 0; + QByteArray lastPacket; + QDateTime lastTimeCharacteristicChanged; + bool firstCharacteristicChanged = true; + + int64_t lastStart = 0; + int64_t lastStop = 0; + + QTimer *refresh; + + QLowEnergyService *gattCommunicationChannelService = nullptr; + QLowEnergyCharacteristic gattWriteCharacteristic; + QLowEnergyCharacteristic gattNotify1Characteristic; + + bool initDone = false; + bool initRequest = false; + + Q_SIGNALS: + void disconnected(); + void debug(QString string); + void speedChanged(double speed); + void packetReceived(); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + + private slots: + + void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void stateChanged(QLowEnergyService::ServiceState state); + void controllerStateChanged(QLowEnergyController::ControllerState state); + + void serviceDiscovered(const QBluetoothUuid &gatt); + void serviceScanDone(void); + void update(); + void error(QLowEnergyController::Error err); + void errorService(QLowEnergyService::ServiceError); + + void changeInclinationRequested(double grade, double percentage); +}; + +#endif // ZIPROTREADMILL_H diff --git a/src/zwiftworkout.cpp b/src/zwiftworkout.cpp index c41066bd4..cf6194090 100644 --- a/src/zwiftworkout.cpp +++ b/src/zwiftworkout.cpp @@ -67,6 +67,7 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType double OnPower = 1; double OffPower = 1; int Pace = -1; + int Cadence = -1; repeat = va_arg(args, uint32_t); if (repeat <= 0) repeat = 1; @@ -75,6 +76,7 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType OnPower = va_arg(args, double); OffPower = va_arg(args, double); Pace = va_arg(args, int); + Cadence = va_arg(args, int); for (uint32_t i = 0; i < repeat; i++) { trainrow row; if (!durationAsDistance(sportType, durationType)) @@ -100,6 +102,8 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType } else { row.power = OffPower * settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble(); } + if(Cadence != -1) + row.cadence = Cadence; qDebug() << "TrainRow" << row.toString(); list.append(row); } @@ -108,10 +112,12 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType double PowerLow = 1; double PowerHigh = 1; int Pace = -1; + int Cadence = -1; Duration = va_arg(args, uint32_t); PowerLow = va_arg(args, double); PowerHigh = va_arg(args, double); Pace = va_arg(args, int); + Cadence = va_arg(args, int); if (sportType.toLower().contains(QStringLiteral("run")) && !durationAsDistance(sportType, durationType)) { double speed = speedFromPace(Pace); int speedDelta = qAbs(qCeil((((60.0 / speed) * 60.0) * (PowerHigh - PowerLow)) * 10)) + 1; @@ -147,15 +153,25 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType } else { row.distance = 0.001; } + if(Cadence != -1) + row.cadence = Cadence; if (PowerHigh > PowerLow) { if (!sportType.toLower().contains(QStringLiteral("run"))) { row.power = (PowerLow + (((PowerHigh - PowerLow) / Duration) * i)) * settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble(); + } else { + double speed = speedFromPace(Pace); + row.speed = (double)qFloor(((((60.0 / speed) * 60.0) * (PowerLow + (((PowerHigh - PowerLow) / Duration) * i))) * 10.0)) / 10.0; + row.forcespeed = 1; } } else { if (!sportType.toLower().contains(QStringLiteral("run"))) { row.power = (PowerLow - (((PowerLow - PowerHigh) / Duration) * i)) * settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble(); + } else { + double speed = speedFromPace(Pace); + row.speed = (double)qFloor(((((60.0 / speed) * 60.0) * ((PowerLow - (((PowerLow - PowerHigh) / Duration) * i)))) * 10.0)) / 10.0; + row.forcespeed = 1; } } qDebug() << "TrainRow" << row.toString(); @@ -179,12 +195,14 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType double Power = 1; double Incline = -100; int Pace = -1; + int Cadence = -1; trainrow row; Duration = va_arg(args, uint32_t); Power = va_arg(args, double); Pace = va_arg(args, int); Incline = va_arg(args, double); + Cadence = va_arg(args, int); if (sportType.toLower().contains(QStringLiteral("run")) && Duration != 1) { if (thresholdSecPerKm != 0) { @@ -208,6 +226,9 @@ void zwiftworkout::convertTag(double thresholdSecPerKm, const QString &sportType row.inclination = Incline * 100; } + if(Cadence != -1) + row.cadence = Cadence; + qDebug() << "TrainRow" << row.toString(); list.append(row); } @@ -256,6 +277,7 @@ QList zwiftworkout::loadJSON(const QString &input, QString *descriptio double OnPower = 1; double OffPower = 1; int Pace = -1; + int Cadence = -1; repeat = element[QStringLiteral("Repeat")].toInt(); if (!repeat) repeat = 1; @@ -266,8 +288,11 @@ QList zwiftworkout::loadJSON(const QString &input, QString *descriptio if (element.contains(QStringLiteral("pace"))) { Pace = element[QStringLiteral("pace")].toInt(); } + if (element.contains(QStringLiteral("Cadence"))) { + Cadence = element[QStringLiteral("Cadence")].toInt(); + } convertTag(0.0, sportType, durationType, list, type.toUtf8().constData(), repeat, OnDuration, - OffDuration, OnPower, OffPower, Pace); + OffDuration, OnPower, OffPower, Pace, Cadence); } else if (type == QStringLiteral("FreeRide")) { uint32_t Duration = 1; // double FlatRoad = 1; @@ -280,19 +305,24 @@ QList zwiftworkout::loadJSON(const QString &input, QString *descriptio double PowerLow = 1; double PowerHigh = 1; int Pace = -1; + int Cadence = -1; Duration = element[QStringLiteral("Duration")].toDouble(); PowerLow = element[QStringLiteral("PowerLow")].toDouble(); PowerHigh = element[QStringLiteral("PowerHigh")].toDouble(); if (element.contains(QStringLiteral("pace"))) { Pace = element[QStringLiteral("pace")].toInt(); } + if (element.contains(QStringLiteral("Cadence"))) { + Cadence = element[QStringLiteral("Cadence")].toInt(); + } convertTag(0.0, sportType, durationType, list, type.toUtf8().constData(), Duration, PowerLow, - PowerHigh, Pace); + PowerHigh, Pace, Cadence); } else if (type == QStringLiteral("SteadyState")) { uint32_t Duration = 1; double Power = 1; int Pace = -1; double Incline = -100; + int Cadence = -1; Duration = element[QStringLiteral("Duration")].toDouble(); if (element.contains(QStringLiteral("pace"))) { @@ -305,7 +335,10 @@ QList zwiftworkout::loadJSON(const QString &input, QString *descriptio if (element.contains(QStringLiteral("Incline"))) { Incline = element[QStringLiteral("Incline")].toDouble(); } - convertTag(0.0, sportType, durationType, list, type.toUtf8().constData(), Duration, Power, Pace, Incline); + if (element.contains(QStringLiteral("Cadence"))) { + Cadence = element[QStringLiteral("Cadence")].toInt(); + } + convertTag(0.0, sportType, durationType, list, type.toUtf8().constData(), Duration, Power, Pace, Incline, Cadence); } } } @@ -355,6 +388,7 @@ QList zwiftworkout::load(const QByteArray &input, QString *description double OnPower = 1; double OffPower = 1; int Pace = -1; + int Cadence = -1; if (atts.hasAttribute(QStringLiteral("Repeat"))) { repeat = atts.value(QStringLiteral("Repeat")).toUInt(); } @@ -373,9 +407,12 @@ QList zwiftworkout::load(const QByteArray &input, QString *description if (atts.hasAttribute(QStringLiteral("pace"))) { Pace = atts.value(QStringLiteral("pace")).toUInt(); } + if (atts.hasAttribute(QStringLiteral("Cadence"))) { + Cadence = atts.value(QStringLiteral("Cadence")).toUInt(); + } convertTag(thresholdSecPerKm, sportType, durationType, list, name.toUtf8().constData(), repeat, - OnDuration, OffDuration, OnPower, OffPower, Pace); + OnDuration, OffDuration, OnPower, OffPower, Pace, Cadence); } else if (name.contains(QStringLiteral("FreeRide"))) { uint32_t Duration = 1; // double FlatRoad = 1; @@ -395,6 +432,8 @@ QList zwiftworkout::load(const QByteArray &input, QString *description double PowerLow = 1; double PowerHigh = 1; int Pace = -1; + int Cadence = -1; + if (atts.hasAttribute(QStringLiteral("Duration"))) { Duration = atts.value(QStringLiteral("Duration")).toDouble(); } @@ -407,14 +446,18 @@ QList zwiftworkout::load(const QByteArray &input, QString *description if (atts.hasAttribute(QStringLiteral("pace"))) { Pace = atts.value(QStringLiteral("pace")).toUInt(); } + if (atts.hasAttribute(QStringLiteral("Cadence"))) { + Cadence = atts.value(QStringLiteral("Cadence")).toUInt(); + } convertTag(thresholdSecPerKm, sportType, durationType, list, name.toUtf8().constData(), Duration, - PowerLow, PowerHigh, Pace); + PowerLow, PowerHigh, Pace, Cadence); } else if (name.contains(QStringLiteral("SteadyState"))) { uint32_t Duration = 1; double Power = 1; int Pace = -1; double Incline = -100; + int Cadence = -1; if (atts.hasAttribute(QStringLiteral("Duration"))) { Duration = atts.value(QStringLiteral("Duration")).toDouble(); @@ -431,8 +474,11 @@ QList zwiftworkout::load(const QByteArray &input, QString *description if (atts.hasAttribute(QStringLiteral("Incline"))) { Incline = atts.value(QStringLiteral("Incline")).toDouble(); } + if (atts.hasAttribute(QStringLiteral("Cadence"))) { + Cadence = atts.value(QStringLiteral("Cadence")).toUInt(); + } convertTag(thresholdSecPerKm, sportType, durationType, list, name.toUtf8().constData(), Duration, Power, - Pace, Incline); + Pace, Incline, Cadence); } } } diff --git a/tst/Devices/BkoolBike/bkoolbiketestdata.h b/tst/Devices/BkoolBike/bkoolbiketestdata.h new file mode 100644 index 000000000..d15668f21 --- /dev/null +++ b/tst/Devices/BkoolBike/bkoolbiketestdata.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Devices/bluetoothdevicetestdata.h" +#include "bkoolbike.h" + +class BkoolBikeTestData : public BluetoothDeviceTestData { + +public: + BkoolBikeTestData() : BluetoothDeviceTestData("Bkool Bike") { + this->addDeviceName("BKOOLSMARTPRO", comparison::StartsWithIgnoreCase); + } + + deviceType get_expectedDeviceType() const override { return deviceType::BkoolBike; } + + bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override { + return dynamic_cast(detectedDevice)!=nullptr; + } +}; + diff --git a/tst/Devices/Concept2SkiErg/concept2skiergtestdata.h b/tst/Devices/Concept2SkiErg/concept2skiergtestdata.h index 93362f6ac..5045f2edc 100644 --- a/tst/Devices/Concept2SkiErg/concept2skiergtestdata.h +++ b/tst/Devices/Concept2SkiErg/concept2skiergtestdata.h @@ -8,6 +8,7 @@ class Concept2SkiErgTestData : public BluetoothDeviceTestData { public: Concept2SkiErgTestData() : BluetoothDeviceTestData("Concept2 Ski Erg") { this->addDeviceName("PM5", "SKI", comparison::IgnoreCase); + this->addDeviceName("PM5", comparison::IgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::Concept2SkiErg; } diff --git a/tst/Devices/FTMSBike/ftmsbiketestdata.h b/tst/Devices/FTMSBike/ftmsbiketestdata.h index bf10c41e4..57fe7b1c0 100644 --- a/tst/Devices/FTMSBike/ftmsbiketestdata.h +++ b/tst/Devices/FTMSBike/ftmsbiketestdata.h @@ -45,6 +45,7 @@ class FTMSBike2TestData : public FTMSBikeTestData { this->addDeviceName("DHZ-", comparison::StartsWithIgnoreCase); // JK fitness 577 this->addDeviceName("MKSM", comparison::StartsWithIgnoreCase); // MKSM3600036 this->addDeviceName("YS_C1_", comparison::StartsWithIgnoreCase);// Yesoul C1H + this->addDeviceName("YS_G1_", comparison::StartsWithIgnoreCase);// Yesoul S3 this->addDeviceName("DS25-", comparison::StartsWithIgnoreCase); // Bodytone DS25 this->addDeviceName("SCHWINN 510T", comparison::StartsWithIgnoreCase); this->addDeviceName("ZWIFT HUB", comparison::StartsWithIgnoreCase); @@ -52,6 +53,7 @@ class FTMSBike2TestData : public FTMSBikeTestData { this->addDeviceName("FLXCY-", comparison::StartsWithIgnoreCase); // Pro FlexBike this->addDeviceName("KICKR CORE", comparison::StartsWithIgnoreCase); this->addDeviceName("B94", comparison::StartsWithIgnoreCase); + this->addDeviceName("DBF135", comparison::IgnoreCase); this->addDeviceName("STAGES BIKE", comparison::StartsWithIgnoreCase); this->addDeviceName("SUITO", comparison::StartsWithIgnoreCase); this->addDeviceName("D2RIDE", comparison::StartsWithIgnoreCase); @@ -59,7 +61,9 @@ class FTMSBike2TestData : public FTMSBikeTestData { this->addDeviceName("SMB1", comparison::StartsWithIgnoreCase); this->addDeviceName("INRIDE", comparison::StartsWithIgnoreCase); this->addDeviceName("XBR55", comparison::StartsWithIgnoreCase); + this->addDeviceName("EW-JS-", comparison::StartsWithIgnoreCase); this->addDeviceName("HAMMER ", comparison::StartsWithIgnoreCase); // HAMMER 64123 + this->addDeviceName("QB-WC01", comparison::StartsWithIgnoreCase); // Starts with DT- and is 14+ characters long. @@ -67,6 +71,8 @@ class FTMSBike2TestData : public FTMSBikeTestData { this->addDeviceName("DT-0123456789A", comparison::IgnoreCase); // Sole SB700 this->addDeviceName("DT-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", comparison::IgnoreCase); // Sole SB700 this->addInvalidDeviceName("DT-0123456789", comparison::IgnoreCase); // too short for Sole SB700 + this->addInvalidDeviceName("DBF13", comparison::IgnoreCase); // too short for DBF135 + this->addInvalidDeviceName("DBF1355", comparison::IgnoreCase); // too long for DBF135 } }; diff --git a/tst/Devices/FTMSRower/ftmsrowertestdata.h b/tst/Devices/FTMSRower/ftmsrowertestdata.h index 1ca420a17..c175d9f0c 100644 --- a/tst/Devices/FTMSRower/ftmsrowertestdata.h +++ b/tst/Devices/FTMSRower/ftmsrowertestdata.h @@ -15,9 +15,8 @@ class FTMSRowerTestData : public BluetoothDeviceTestData { this->addDeviceName("I-ROWER", comparison::StartsWithIgnoreCase); this->addDeviceName("PM5ROW", comparison::IgnoreCase); this->addDeviceName("PM5XROW", comparison::IgnoreCase); - this->addDeviceName("PM5XROWX", comparison::IgnoreCase); - this->addDeviceName("PM5ROWX", comparison::IgnoreCase); this->addDeviceName("SF-RW", comparison::IgnoreCase); + this->addDeviceName("S4 COMMS", comparison::StartsWithIgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::FTMSRower; } diff --git a/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h b/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h index d27f5a8e7..6206770be 100644 --- a/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h +++ b/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h @@ -17,14 +17,19 @@ class HorizonTreadmillTestData : public BluetoothDeviceTestData { this->addDeviceName("T218_", comparison::StartsWithIgnoreCase); this->addDeviceName("TRX3500", comparison::StartsWithIgnoreCase); this->addDeviceName("JFTMPARAGON", comparison::StartsWithIgnoreCase); + this->addDeviceName("PARAGON X", comparison::StartsWithIgnoreCase); this->addDeviceName("JFTM", comparison::StartsWithIgnoreCase); this->addDeviceName("CT800", comparison::StartsWithIgnoreCase); this->addDeviceName("TRX4500", comparison::StartsWithIgnoreCase); this->addDeviceName("MOBVOI TM", comparison::StartsWithIgnoreCase); this->addDeviceName("ESANGLINKER", comparison::StartsWithIgnoreCase); this->addDeviceName("DK202000725", comparison::StartsWithIgnoreCase); + this->addDeviceName("CTM780102C6BB32D62", comparison::StartsWithIgnoreCase); this->addDeviceName("MX-TM ", comparison::StartsWithIgnoreCase); this->addDeviceName("MATRIXTF50", comparison::StartsWithIgnoreCase); + this->addDeviceName("MOBVOI TM", comparison::StartsWithIgnoreCase); + this->addDeviceName("KETTLER TREADMILL", comparison::StartsWithIgnoreCase); + this->addDeviceName("ASSAULTRUNNER", comparison::StartsWithIgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::HorizonTreadmill; } @@ -73,3 +78,30 @@ class HorizonTreadmillToorxTestData : public BluetoothDeviceTestData { return dynamic_cast(detectedDevice) != nullptr; } }; + +class HorizonTreadmillBodyToneTestData : public BluetoothDeviceTestData { + void configureSettings(const DeviceDiscoveryInfo &info, bool enable, + std::vector &configurations) const override { + DeviceDiscoveryInfo config(info); + + if (enable) { + config.horizon_treadmill_force_ftms = true; + configurations.push_back(config); + } else { + // Basic case where the device is disabled in the settings + config.horizon_treadmill_force_ftms = false; + configurations.push_back(config); + } + } + + public: + HorizonTreadmillBodyToneTestData() : BluetoothDeviceTestData("Horizon Treadmill (Bodytone)") { + this->addDeviceName("TF-", comparison::StartsWithIgnoreCase); + } + + deviceType get_expectedDeviceType() const override { return deviceType::HorizonTreadmill; } + + bool get_isExpectedDevice(bluetoothdevice *detectedDevice) const override { + return dynamic_cast(detectedDevice) != nullptr; + } +}; \ No newline at end of file diff --git a/tst/Devices/KingsmithR1ProTreadmill/kingsmithr1protreadmilltestdata.h b/tst/Devices/KingsmithR1ProTreadmill/kingsmithr1protreadmilltestdata.h index ca13dfc67..b271b5ab7 100644 --- a/tst/Devices/KingsmithR1ProTreadmill/kingsmithr1protreadmilltestdata.h +++ b/tst/Devices/KingsmithR1ProTreadmill/kingsmithr1protreadmilltestdata.h @@ -17,6 +17,8 @@ class KingsmithR1ProTreadmillTestData : public BluetoothDeviceTestData { this->addDeviceName("KINGSMITH", comparison::StartsWithIgnoreCase); this->addDeviceName("KS-H", comparison::StartsWithIgnoreCase); this->addDeviceName("DYNAMAX", comparison::StartsWithIgnoreCase); + this->addDeviceName("WALKINGPAD", comparison::StartsWithIgnoreCase); + this->addDeviceName("KS-BLR", comparison::StartsWithIgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::KingsmithR1ProTreadmill; } diff --git a/tst/Devices/KingsmithR2Treadmill/kingsmithr2treadmilltestdata.h b/tst/Devices/KingsmithR2Treadmill/kingsmithr2treadmilltestdata.h index 9506d6f90..f65e41aed 100644 --- a/tst/Devices/KingsmithR2Treadmill/kingsmithr2treadmilltestdata.h +++ b/tst/Devices/KingsmithR2Treadmill/kingsmithr2treadmilltestdata.h @@ -19,6 +19,7 @@ class KingsmithR2TreadmillTestData : public BluetoothDeviceTestData { this->addDeviceName("KS-HDSC-X21C", comparison::StartsWithIgnoreCase); this->addDeviceName("KS-HDSY-X21C", comparison::StartsWithIgnoreCase); this->addDeviceName("KS-NGCH-X21C", comparison::StartsWithIgnoreCase); + this->addDeviceName("KS-NACH-X21C", comparison::StartsWithIgnoreCase); } diff --git a/tst/Devices/M3IBike/m3ibiketestdata.cpp b/tst/Devices/M3IBike/m3ibiketestdata.cpp index ba78660d9..fee07f1fd 100644 --- a/tst/Devices/M3IBike/m3ibiketestdata.cpp +++ b/tst/Devices/M3IBike/m3ibiketestdata.cpp @@ -4,7 +4,7 @@ /** * @brief hex2bytes Converts a hexadecimal string to bytes, 2 characters at a time. - * @param s A hexidecimal string e.g. "023F4A" to { 0x02, 0x3F, 0x4A } + * @param s An hexadecimal string e.g. "023F4A" to { 0x02, 0x3F, 0x4A } */ static QByteArray hex2bytes(const std::string& s) { diff --git a/tst/Devices/Schwinn170Bike/schwinn170biketestdata.h b/tst/Devices/Schwinn170Bike/schwinn170biketestdata.h new file mode 100644 index 000000000..929f7bc4e --- /dev/null +++ b/tst/Devices/Schwinn170Bike/schwinn170biketestdata.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Devices/bluetoothdevicetestdata.h" +#include "schwinn170bike.h" + +class Schwinn170BikeTestData : public BluetoothDeviceTestData { + +public: + Schwinn170BikeTestData() : BluetoothDeviceTestData("Schwinn 170 Bike") { + + this->addDeviceName("SCHWINN 170/270", comparison::StartsWithIgnoreCase); + } + + + deviceType get_expectedDeviceType() const override { return deviceType::Schwinn170Bike; } + + bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override { + return dynamic_cast(detectedDevice)!=nullptr; + } +}; + diff --git a/tst/Devices/SmartRowRower/smartrowrowertestdata.h b/tst/Devices/SmartRowRower/smartrowrowertestdata.h index 641eefe1b..a2c4fc907 100644 --- a/tst/Devices/SmartRowRower/smartrowrowertestdata.h +++ b/tst/Devices/SmartRowRower/smartrowrowertestdata.h @@ -7,7 +7,7 @@ class SmartRowRowerTestData : public BluetoothDeviceTestData { public: SmartRowRowerTestData() : BluetoothDeviceTestData("Smart Row Rower") { - this->addDeviceName("SMARTROW", comparison::StartsWith); + this->addDeviceName("SMARTROW", comparison::StartsWithIgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::SmartRowRower; } diff --git a/tst/Devices/SnodeBike/snodebiketestdata.h b/tst/Devices/SnodeBike/snodebiketestdata.h index 3fdebbe37..a8edb4397 100644 --- a/tst/Devices/SnodeBike/snodebiketestdata.h +++ b/tst/Devices/SnodeBike/snodebiketestdata.h @@ -32,9 +32,23 @@ class SnodeBike1TestData : public SnodeBikeTestData { class SnodeBike2TestData : public SnodeBikeTestData { + void configureSettings(const DeviceDiscoveryInfo &info, bool enable, + std::vector &configurations) const override { + DeviceDiscoveryInfo config(info); + + if (enable) { + config.horizon_treadmill_force_ftms = false; + configurations.push_back(config); + } else { + // Basic case where the device is disabled in the settings + config.horizon_treadmill_force_ftms = true; + configurations.push_back(config); + } + } + public: SnodeBike2TestData() : SnodeBikeTestData("Snode Bike TF") { - this->addDeviceName("TF-", comparison::StartsWith); + this->addDeviceName("TF-", comparison::StartsWithIgnoreCase); } }; diff --git a/tst/Devices/SoleF80Treadmill/solef80treadmilltestdata.h b/tst/Devices/SoleF80Treadmill/solef80treadmilltestdata.h index 8369e5548..8de01e267 100644 --- a/tst/Devices/SoleF80Treadmill/solef80treadmilltestdata.h +++ b/tst/Devices/SoleF80Treadmill/solef80treadmilltestdata.h @@ -7,13 +7,11 @@ class SoleF80TreadmillTestData : public BluetoothDeviceTestData { public: SoleF80TreadmillTestData() : BluetoothDeviceTestData("Sole F80") { - this->addDeviceName("F80", comparison::StartsWithIgnoreCase); this->addDeviceName("F65", comparison::StartsWithIgnoreCase); this->addDeviceName("S77", comparison::StartsWithIgnoreCase); this->addDeviceName("TT8", comparison::StartsWithIgnoreCase); this->addDeviceName("F63", comparison::StartsWithIgnoreCase); - this->addDeviceName("ST90", comparison::StartsWithIgnoreCase); - this->addDeviceName("F85", comparison::StartsWithIgnoreCase); + this->addDeviceName("ST90", comparison::StartsWithIgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::SoleF80Treadmill; } @@ -23,3 +21,30 @@ class SoleF80TreadmillTestData : public BluetoothDeviceTestData { } }; +class SoleF85TreadmillTestData : public BluetoothDeviceTestData { + void configureSettings(const DeviceDiscoveryInfo &info, bool enable, + std::vector &configurations) const override { + DeviceDiscoveryInfo config(info); + + if (enable) { + config.sole_treadmill_inclination = true; + configurations.push_back(config); + } else { + // Basic case where the device is disabled in the settings + config.sole_treadmill_inclination = false; + configurations.push_back(config); + } + } + + public: + SoleF85TreadmillTestData() : BluetoothDeviceTestData("Sole F85 Treadmill") { + this->addDeviceName("F85", comparison::StartsWithIgnoreCase); + this->addDeviceName("F80", comparison::StartsWithIgnoreCase); + } + + deviceType get_expectedDeviceType() const override { return deviceType::SoleF80Treadmill; } + + bool get_isExpectedDevice(bluetoothdevice *detectedDevice) const override { + return dynamic_cast(detectedDevice) != nullptr; + } +}; \ No newline at end of file diff --git a/tst/Devices/TrueTreadmill/truetreadmilltestdata.h b/tst/Devices/TrueTreadmill/truetreadmilltestdata.h index e1c80e01e..083a3f40e 100644 --- a/tst/Devices/TrueTreadmill/truetreadmilltestdata.h +++ b/tst/Devices/TrueTreadmill/truetreadmilltestdata.h @@ -9,6 +9,7 @@ class TrueTreadmillTestData : public BluetoothDeviceTestData { TrueTreadmillTestData() : BluetoothDeviceTestData("True Treadmill") { this->addDeviceName("TRUE", comparison::StartsWithIgnoreCase); this->addDeviceName("TREADMILL", comparison::StartsWithIgnoreCase); + this->addDeviceName("ASSAULT TREADMILL ", comparison::StartsWithIgnoreCase); } deviceType get_expectedDeviceType() const override { return deviceType::TrueTreadmill; } diff --git a/tst/Devices/TrxAppGateUSBBike/trxappgateusbbiketestdata.h b/tst/Devices/TrxAppGateUSBBike/trxappgateusbbiketestdata.h index 9f193ea4b..1ae57b896 100644 --- a/tst/Devices/TrxAppGateUSBBike/trxappgateusbbiketestdata.h +++ b/tst/Devices/TrxAppGateUSBBike/trxappgateusbbiketestdata.h @@ -24,7 +24,7 @@ class TrxAppGateUSBBikeTestData : public BluetoothDeviceTestData { class TrxAppGateUSBBike1TestData : public TrxAppGateUSBBikeTestData { protected: void configureSettings(const DeviceDiscoveryInfo& info, bool enable, std::vector& configurations) const override { - // This particular case of TrxAppGateUSBBike is independant of the setting + // This particular case of TrxAppGateUSBBike is independent of the setting DeviceDiscoveryInfo config(info); config.toorx_bike = true; diff --git a/tst/Devices/YpooElliptical/ypooellipticaltestdata.h b/tst/Devices/YpooElliptical/ypooellipticaltestdata.h new file mode 100644 index 000000000..de6611358 --- /dev/null +++ b/tst/Devices/YpooElliptical/ypooellipticaltestdata.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Devices/bluetoothdevicetestdata.h" +#include "ypooelliptical.h" + +class YpooEllipticalTestData : public BluetoothDeviceTestData { + +public: + YpooEllipticalTestData() : BluetoothDeviceTestData("Ypoo Elliptical") { + this->addDeviceName("YPOO-U3-", comparison::StartsWithIgnoreCase); + } + + + deviceType get_expectedDeviceType() const override { return deviceType::YpooElliptical; } + + bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override { + return dynamic_cast(detectedDevice)!=nullptr; + } +}; diff --git a/tst/Devices/ZiproTreadmill/ziprotreadmilltestdata.h b/tst/Devices/ZiproTreadmill/ziprotreadmilltestdata.h new file mode 100644 index 000000000..5daf9dbad --- /dev/null +++ b/tst/Devices/ZiproTreadmill/ziprotreadmilltestdata.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Devices/bluetoothdevicetestdata.h" +#include "ziprotreadmill.h" + +class ZiproTreadmillTestData : public BluetoothDeviceTestData { + +public: + ZiproTreadmillTestData() : BluetoothDeviceTestData("Zipro Treadmill") { + this->addDeviceName("RZ_TREADMIL", comparison::StartsWithIgnoreCase); + } + + deviceType get_expectedDeviceType() const override { return deviceType::ZiproTreadmill; } + + bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override { + return dynamic_cast(detectedDevice)!=nullptr; + } +}; + diff --git a/tst/Devices/bluetoothdevicetestdata.h b/tst/Devices/bluetoothdevicetestdata.h index b5d4c5d20..940241c4f 100644 --- a/tst/Devices/bluetoothdevicetestdata.h +++ b/tst/Devices/bluetoothdevicetestdata.h @@ -16,6 +16,7 @@ enum deviceType { None, ApexBike, + BkoolBike, M3IBike, FakeBike, FakeElliptical, @@ -62,6 +63,7 @@ enum deviceType { EchelonRower, EchelonConnectSport, SchwinnIC4Bike, + Schwinn170Bike, SportsTechBike, SportsPlusBike, YesoulBike, @@ -75,6 +77,7 @@ enum deviceType { MCFBike, ToorxTreadmill, IConceptBike, + IConceptElliptical, SpiritTreadmill, ActivioTreadmill, TrxAppGateUSBTreadmill, @@ -92,6 +95,8 @@ enum deviceType { ChronoBike, MepanelBike, LifeFitnessTreadmill, + YpooElliptical, + ZiproTreadmill, CompuTrainerBike }; diff --git a/tst/Devices/bluetoothdevicetestsuite.cpp b/tst/Devices/bluetoothdevicetestsuite.cpp index 29df02b7a..43b9c8e27 100644 --- a/tst/Devices/bluetoothdevicetestsuite.cpp +++ b/tst/Devices/bluetoothdevicetestsuite.cpp @@ -1,13 +1,14 @@ #include "bluetoothdevicetestsuite.h" #include "discoveryoptions.h" #include "bluetooth.h" +#include "bluetoothsignalreceiver.h" const QString testUUID = QStringLiteral("b8f79bac-32e5-11ed-a261-0242ac120002"); QBluetoothUuid uuid{testUUID}; template -void BluetoothDeviceTestSuite::tryDetectDevice(bluetooth &bt, const QBluetoothDeviceInfo &deviceInfo) const { - +void BluetoothDeviceTestSuite::tryDetectDevice(bluetooth &bt, + const QBluetoothDeviceInfo &deviceInfo) const { try { // It is possible to use an EXPECT_NO_THROW here, but this // way is easier to place a breakpoint on the call to bt.deviceDiscovered. @@ -35,6 +36,54 @@ std::string BluetoothDeviceTestSuite::getTypeName(bluetoothdevice *b) const { return name.toStdString(); } +template +std::string BluetoothDeviceTestSuite::formatString(std::string format, bluetoothdevice *b) const { + + QString qs = QString(format.c_str()); + QString typeName = this->getTypeName(b).c_str(); + return qs.replace("{typeName}", typeName).toStdString(); +} + +template +void BluetoothDeviceTestSuite::testDeviceDetection(BluetoothDeviceTestData * testData, bluetooth &bt, + const QBluetoothDeviceInfo &deviceInfo, + bool expectMatch, + bool restart, + const QString& failMessage) const { + this->testDeviceDetection(testData, bt, deviceInfo, expectMatch, restart, failMessage.toStdString()); +} + + + +template +void BluetoothDeviceTestSuite::testDeviceDetection(BluetoothDeviceTestData * testData, bluetooth &bt, + const QBluetoothDeviceInfo &deviceInfo, + bool expectMatch, + bool restart, + const std::string& failMessage) const { + + BluetoothSignalReceiver signalReceiver(bt); + + + this->tryDetectDevice(bt, deviceInfo); + + bluetoothdevice * device = bt.device(); + + if(expectMatch) { + std::string formattedFailMessage = this->formatString(failMessage, device); + EXPECT_TRUE(testData->get_isExpectedDevice(device)) << formattedFailMessage; + + EXPECT_EQ(device, signalReceiver.get_device()) << "Connection signal not received"; + } else { + EXPECT_FALSE(testData->get_isExpectedDevice(device)) << this->formatString(failMessage, device); + } + + if(restart) { + bt.restart(); + EXPECT_EQ(nullptr, signalReceiver.get_device()) << "Disconnection signal not received"; + } +} + template void BluetoothDeviceTestSuite::SetUp() { @@ -51,7 +100,6 @@ void BluetoothDeviceTestSuite::SetUp() { this->defaultDiscoveryOptions = discoveryoptions{}; this->defaultDiscoveryOptions.startDiscovery = false; - this->defaultDiscoveryOptions.createTemplateManagers = false; this->names = this->typeParam.get_deviceNames(); @@ -60,6 +108,7 @@ void BluetoothDeviceTestSuite::SetUp() { this->testSettings.activate(); } + template void BluetoothDeviceTestSuite::test_deviceDetection_validNames_enabled() { BluetoothDeviceTestData& testData = this->typeParam; @@ -71,17 +120,10 @@ void BluetoothDeviceTestSuite::test_deviceDetection_validNames_enabled() { { this->testSettings.loadFrom(discoveryInfo); - QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName); - - // try to create the device - this->tryDetectDevice(bt, deviceInfo); - auto detectedDevice = bt.device(); - EXPECT_TRUE(testData.get_isExpectedDevice(detectedDevice)) - << "Failed to detect device for " << testData.get_testName() << " using name: " << deviceName.toStdString() - << ", got a " << this->getTypeName(detectedDevice) << " instead"; - - // restart the bluetooth manager to clear the device - bt.restart(); + QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName); + QString failMessage = QString("Failed to detect device for %1s using name: %2s, got a {typeName} instead") + .arg(testData.get_testName().c_str()).arg(deviceName); + this->testDeviceDetection(&testData, bt, deviceInfo, true, true, failMessage); } } } @@ -101,18 +143,12 @@ void BluetoothDeviceTestSuite::test_deviceDetection_validNames_disabled() { { this->testSettings.loadFrom(discoveryInfo); - QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName); + QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName); + QString failMessage = QString("Created the device %1 when expected not to: %2") + .arg(testData.get_testName().c_str()).arg(deviceName); // try to create the device - this->tryDetectDevice(bt, deviceInfo); - EXPECT_FALSE(testData.get_isExpectedDevice(bt.device())) - << "Created the device " - << testData.get_testName() - << " when expected not to: " - << deviceName.toStdString(); - - // restart the bluetooth manager to clear the device - bt.restart(); + this->testDeviceDetection(&testData, bt, deviceInfo, false, true, failMessage); } } } @@ -141,17 +177,11 @@ void BluetoothDeviceTestSuite::test_deviceDetection_validNames_invalidBluetoo this->testSettings.loadFrom(discoveryInfo); QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName, false); + QString failMessage = QString("Created the device %1 when bluetooth device info is invalid: %2") + .arg(testData.get_testName().c_str()).arg(deviceName); // try to create the device - this->tryDetectDevice(bt, deviceInfo); - EXPECT_FALSE(testData.get_isExpectedDevice(bt.device())) - << "Created the device " - << testData.get_testName() - << "when bluetooth device info is invalid: " - << deviceName.toStdString(); - - // restart the bluetooth manager to clear the device - bt.restart(); + this->testDeviceDetection(&testData, bt, deviceInfo, false, true, failMessage); } } } @@ -190,25 +220,18 @@ void BluetoothDeviceTestSuite::test_deviceDetection_exclusions() { } this->testSettings.loadFrom(exclusionDiscoveryInfo); + QString failMessage = QString("Failed to create exclusion device: %1, got a {typeName} instead") + .arg(exclusion.get()->get_testName().c_str()); // try to create the excluding device - this->tryDetectDevice(bt, exclusionDeviceInfo); - - auto detectedExclusionDevice = bt.device(); - EXPECT_TRUE(exclusion.get()->get_isExpectedDevice(detectedExclusionDevice)) - << "Failed to create exclusion device: " << exclusion.get()->get_testName() - << " got a " << this->getTypeName(detectedExclusionDevice) << " instead"; + this->testDeviceDetection(exclusion.get(), bt, exclusionDeviceInfo, true, false, failMessage); // now configure to have the bluetooth object try, but fail to detect the target device this->testSettings.loadFrom(discoveryInfo); QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName); - this->tryDetectDevice(bt, deviceInfo); - EXPECT_FALSE(testData.get_isExpectedDevice(bt.device())) << "Detected the " << testData.get_testName() - << " from " - << deviceName.toStdString() - << " in spite of exclusion"; - - bt.restart(); + failMessage = QString("Detected the %1 from %2 in spite of exclusion") + .arg(testData.get_testName().c_str()).arg(deviceName); + this->testDeviceDetection(&testData, bt, deviceInfo, false, true, failMessage); } } } @@ -237,18 +260,10 @@ void BluetoothDeviceTestSuite::test_deviceDetection_invalidNames_enabled() QBluetoothDeviceInfo deviceInfo = testData.get_bluetoothDeviceInfo(uuid, deviceName); - // try to create the device - this->tryDetectDevice(bt, deviceInfo); - EXPECT_FALSE(testData.get_isExpectedDevice(bt.device())) - << "Detected device " - << testData.get_testName() - << " from invalid name: " - << deviceName.toStdString(); - - // restart the bluetooth manager to clear the device - bt.restart(); + QString failMessage = QString("Detected device %1 from %2") + .arg(testData.get_testName().c_str()).arg(deviceName); + + this->testDeviceDetection(&testData, bt, deviceInfo, false, true, failMessage); } } } - - diff --git a/tst/Devices/bluetoothdevicetestsuite.h b/tst/Devices/bluetoothdevicetestsuite.h index fb164b6ed..e4307154b 100644 --- a/tst/Devices/bluetoothdevicetestsuite.h +++ b/tst/Devices/bluetoothdevicetestsuite.h @@ -8,6 +8,7 @@ template class BluetoothDeviceTestSuite : public testing::Test { + protected: T typeParam; @@ -27,7 +28,7 @@ class BluetoothDeviceTestSuite : public testing::Test { QStringList names; /** - * @brief The default options for dicovery by an instance of the bluetooth class. + * @brief The default options for discovery by an instance of the bluetooth class. */ discoveryoptions defaultDiscoveryOptions; @@ -39,11 +40,34 @@ class BluetoothDeviceTestSuite : public testing::Test { /** * @brief Call bt.deviceDiscovered on the deviceInfo to try to detect and create the bluetoothdevice object for it. * If an exception is thrown, the test is failed with a call to FAIL(). - * Bascially replaces EXPECT_NO_THROW, for ease of breakpoint placement. + * Basically replaces EXPECT_NO_THROW, for ease of breakpoint placement. * @param bt * @param deviceInfo */ - void tryDetectDevice(bluetooth& bt, const QBluetoothDeviceInfo& deviceInfo) const; + void tryDetectDevice(bluetooth &bt, const QBluetoothDeviceInfo &deviceInfo) const; + + /** + * @brief Tests device detection. + * @param testData The test data object for the device to be detected (or not). + * @param bt The object that will do the detecting. + * @param deviceInfo The device info for the device for which detection will be attempted. + * @param expectMatch Indicates if the device is expected to be detected (true) or not (false). + * @param restart Indicates if the bluetooth (bt) object should be restarted. + * @param failMessage The failure message if the device is not detected when expected to be, or detected when not expected to be. + */ + void testDeviceDetection(BluetoothDeviceTestData * testData, bluetooth& bt, const QBluetoothDeviceInfo& deviceInfo, bool expectMatch, bool restart, const QString& failMessage) const; + + /** + * @brief Tests device detection. + * @param testData The test data object for the device to be detected (or not). + * @param bt The object that will do the detecting. + * @param deviceInfo The device info for the device for which detection will be attempted. + * @param expectMatch Indicates if the device is expected to be detected (true) or not (false). + * @param restart Indicates if the bluetooth (bt) object should be restarted. + * @param failMessage The failure message if the device is not detected when expected to be, or detected when not expected to be. + */ + void testDeviceDetection(BluetoothDeviceTestData * testData, bluetooth& bt, const QBluetoothDeviceInfo& deviceInfo, bool expectMatch, bool restart, const std::string& failMessage) const; + /** * @brief Gets the type name for the specified device object. Attempts to strip metadata from typeid result. @@ -51,6 +75,14 @@ class BluetoothDeviceTestSuite : public testing::Test { * @return */ std::string getTypeName(bluetoothdevice *b) const; + + /** + * @brief Replaces {typeName} in the format string with the type name of the provided object + * @param format The format string. The text "{typeName}" will be replaced with type name of the provided object. + * @param b + * @return + */ + std::string formatString(std::string format, bluetoothdevice *b) const; public: BluetoothDeviceTestSuite() : testSettings("Roberto Viola", "QDomyos-Zwift Testing") {} @@ -62,13 +94,13 @@ class BluetoothDeviceTestSuite : public testing::Test { /** - * @brief Tests that a device is not detected if its exluding devices have already been detected. + * @brief Tests that a device is not detected if its excluding devices have already been detected. */ void test_deviceDetection_exclusions(); /** * @brief Test that if a device is enabled in the settings, and no excluding devices have already been detected, - * the device under test will be created if a valud bluetooth name is provided. + * the device under test will be created if a valid bluetooth name is provided. */ void test_deviceDetection_validNames_enabled(); @@ -86,7 +118,7 @@ class BluetoothDeviceTestSuite : public testing::Test { /** * @brief Test that if a device is enabled in the settings, and no excluding devices have already been detected, - * the device under test will NOT be created if an invald name is provided. + * the device under test will NOT be created if an invalid name is provided. * e.g.starts with correct text, but not the right length and/or wrong case. */ void test_deviceDetection_invalidNames_enabled(); diff --git a/tst/Devices/bluetoothsignalreceiver.cpp b/tst/Devices/bluetoothsignalreceiver.cpp new file mode 100644 index 000000000..8f48717a0 --- /dev/null +++ b/tst/Devices/bluetoothsignalreceiver.cpp @@ -0,0 +1,16 @@ +#include "bluetoothsignalreceiver.h" + + +BluetoothSignalReceiver::BluetoothSignalReceiver(bluetooth &b, QObject *parent) : QObject(parent) { + connect(&b, &bluetooth::bluetoothDeviceConnected, this, &BluetoothSignalReceiver::bluetoothDeviceConnected); + connect(&b, &bluetooth::bluetoothDeviceDisconnected, this, &BluetoothSignalReceiver::bluetoothDeviceDisconnected); +} + +BluetoothSignalReceiver::~BluetoothSignalReceiver() {} + +bluetoothdevice *BluetoothSignalReceiver::get_device() const { return this->device; } + +void BluetoothSignalReceiver::bluetoothDeviceConnected(bluetoothdevice *b) { this->device = b;} + +void BluetoothSignalReceiver::bluetoothDeviceDisconnected() { this->device = nullptr; } + diff --git a/tst/Devices/bluetoothsignalreceiver.h b/tst/Devices/bluetoothsignalreceiver.h new file mode 100644 index 000000000..61fc0067f --- /dev/null +++ b/tst/Devices/bluetoothsignalreceiver.h @@ -0,0 +1,30 @@ +#ifndef BLUETOOTHSIGNALRECEIVER_H +#define BLUETOOTHSIGNALRECEIVER_H + +#include +#include "bluetoothdevice.h" +#include "bluetooth.h" + +/** + * @brief Catches bluetoothdevice connection signals from a bluetooth object. + */ +class BluetoothSignalReceiver : public QObject +{ + Q_OBJECT + + private: + bluetoothdevice* device = nullptr; + + +public: + explicit BluetoothSignalReceiver(bluetooth& b, QObject *parent = nullptr); + ~BluetoothSignalReceiver(); + + bluetoothdevice * get_device() const; + +public Q_SLOTS: + void bluetoothDeviceConnected(bluetoothdevice *b); + void bluetoothDeviceDisconnected(); +}; + +#endif // BLUETOOTHSIGNALRECEIVER_H diff --git a/tst/Devices/devicediscoveryinfo.cpp b/tst/Devices/devicediscoveryinfo.cpp index fc8825bea..ccd8f8d7d 100644 --- a/tst/Devices/devicediscoveryinfo.cpp +++ b/tst/Devices/devicediscoveryinfo.cpp @@ -28,6 +28,7 @@ void DeviceDiscoveryInfo::setValues(QSettings &settings, bool clear) const { settings.setValue(QZSettings::toorx_bike, this->toorx_bike); settings.setValue(QZSettings::toorx_ftms, this->toorx_ftms); settings.setValue(QZSettings::toorx_ftms_treadmill, this->toorx_ftms_treadmill); + settings.setValue(QZSettings::horizon_treadmill_force_ftms, this->horizon_treadmill_force_ftms); settings.setValue(QZSettings::snode_bike, this->snode_bike); settings.setValue(QZSettings::fitplus_bike, this->fitplus_bike); settings.setValue(QZSettings::technogym_myrun_treadmill_experimental, this->technogym_myrun_treadmill_experimental); @@ -35,6 +36,8 @@ void DeviceDiscoveryInfo::setValues(QSettings &settings, bool clear) const { settings.setValue(QZSettings::ss2k_peloton, this->ss2k_peloton); settings.setValue(QZSettings::ftms_accessory_name, this->ftmsAccessoryName); settings.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, this->pafers_treadmill_bh_iboxster_plus); + settings.setValue(QZSettings::iconcept_elliptical, this->iconcept_elliptical); + settings.setValue(QZSettings::sole_treadmill_inclination, this->sole_treadmill_inclination); } void DeviceDiscoveryInfo::getValues(QSettings &settings){ @@ -57,6 +60,7 @@ void DeviceDiscoveryInfo::getValues(QSettings &settings){ this->toorx_bike = settings.value(QZSettings::toorx_bike, QZSettings::default_toorx_bike).toBool(); this->toorx_ftms = settings.value(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms).toBool(); this->toorx_ftms_treadmill = settings.value(QZSettings::toorx_ftms_treadmill, QZSettings::default_toorx_ftms_treadmill).toBool(); + this->horizon_treadmill_force_ftms = settings.value(QZSettings::horizon_treadmill_force_ftms, QZSettings::default_horizon_treadmill_force_ftms).toBool(); this->snode_bike = settings.value(QZSettings::snode_bike, QZSettings::default_snode_bike).toBool(); this->fitplus_bike = settings.value(QZSettings::fitplus_bike, QZSettings::default_fitplus_bike).toBool(); this->technogym_myrun_treadmill_experimental = settings.value(QZSettings::technogym_myrun_treadmill_experimental, QZSettings::default_technogym_myrun_treadmill_experimental).toBool(); @@ -64,6 +68,8 @@ void DeviceDiscoveryInfo::getValues(QSettings &settings){ this->ss2k_peloton = settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool(); this->ftmsAccessoryName = settings.value(QZSettings::ftms_accessory_name, QZSettings::default_ftms_accessory_name).toString(); this->pafers_treadmill_bh_iboxster_plus = settings.value(QZSettings::pafers_treadmill_bh_iboxster_plus, QZSettings::default_pafers_treadmill_bh_iboxster_plus).toBool(); + this->iconcept_elliptical = settings.value(QZSettings::iconcept_elliptical, QZSettings::default_iconcept_elliptical).toBool(); + this->sole_treadmill_inclination = settings.value(QZSettings::sole_treadmill_inclination, QZSettings::default_sole_treadmill_inclination).toBool(); } void DeviceDiscoveryInfo::loadDefaultValues() { diff --git a/tst/Devices/devicediscoveryinfo.h b/tst/Devices/devicediscoveryinfo.h index 67b2e5f39..969b487c8 100644 --- a/tst/Devices/devicediscoveryinfo.h +++ b/tst/Devices/devicediscoveryinfo.h @@ -36,6 +36,7 @@ public : bool toorx_ftms = false; bool toorx_ftms_treadmill = false; + bool horizon_treadmill_force_ftms = false; bool snode_bike = false; bool fitplus_bike = false; @@ -48,6 +49,10 @@ public : bool pafers_treadmill_bh_iboxster_plus = false; + bool iconcept_elliptical = false; + + bool sole_treadmill_inclination = false; + /** * @brief Constructor. * @param loadDefaults Indicates if the default values should be loaded. diff --git a/tst/Devices/devices.h b/tst/Devices/devices.h index f6c9f738e..2951c857a 100644 --- a/tst/Devices/devices.h +++ b/tst/Devices/devices.h @@ -8,6 +8,7 @@ #include "bluetoothdevicetestdata.h" #include "ActivioTreadmill/activiotreadmilltestdata.h" #include "ApexBike/apexbiketestdata.h" +#include "BkoolBike/bkoolbiketestdata.h" #include "BHFitnessElliptical/bhfitnessellipticaltestdata.h" #include "Bike/biketestdata.h" #include "BowflexT216Treadmill/bowflext216treadmilltestdata.h" @@ -36,6 +37,7 @@ #include "HorizonGR7Bike/horizongr7biketestdata.h" #include "HorizonTreadmill/horizontreadmilltestdata.h" #include "iConceptBike/iconceptbiketestdata.h" +#include "iConceptElliptical/iconceptellipticaltestdata.h" #include "InspireBike/inspirebiketestdata.h" #include "KeepBike/keepbiketestdata.h" #include "LifeFitnessTreadmill/lifefitnesstreadmilltestdata.h" @@ -64,6 +66,7 @@ #include "RenphoBike/renphobiketestdata.h" #include "Rower/rowertestdata.h" #include "SchwinnIC4Bike/schwinnic4biketestdata.h" +#include "Schwinn170Bike/schwinn170biketestdata.h" #include "Shuaa5Treadmill/shuaa5treadmilltestdata.h" #include "SkandikaWiryBike/skandikawirybiketestdata.h" #include "SmartRowRower/smartrowrowertestdata.h" @@ -87,10 +90,13 @@ #include "UltrasportBike/ultrasportbiketestdata.h" #include "WahooKickrSnapBike/wahookickrsnapbiketestdata.h" #include "YesoulBike/yesoulbiketestdata.h" +#include "YpooElliptical/ypooellipticaltestdata.h" +#include "ZiproTreadmill/ziprotreadmilltestdata.h" using BluetoothDeviceTestDataTypes = ::testing::Types< ActivioTreadmillTestData, ApexBikeTestData, +BkoolBikeTestData, BHFitnessEllipticalTestData, BikeTestData, BowflexT216TreadmillTestData, @@ -125,6 +131,7 @@ FlywheelBike2TestData, HorizonGR7BikeTestData, HorizonTreadmillTestData, HorizonTreadmillToorxTestData, +HorizonTreadmillBodyToneTestData, InspireBikeTestData, KeepBikeTestData, KingsmithR1ProTreadmillTestData, @@ -154,6 +161,7 @@ ProFormWiFiTreadmillTestData, RenphoBike1TestData, RenphoBike2TestData, RowerTestData, +Schwinn170BikeTestData, SchwinnIC4BikeTestData, Shuaa5TreadmillTestData, SkandikaWiryBikeTestData, @@ -163,6 +171,7 @@ SnodeBike2TestData, SoleBikeTestData, SoleEllipticalTestData, SoleF80TreadmillTestData, +SoleF85TreadmillTestData, SpiritTreadmillTestData, SportsPlusBikeTestData, SportsTechBikeTestData, @@ -181,8 +190,11 @@ TrxAppGateUSBTreadmillTestData, UltrasportBikeTestData, WahooKickrSnapBikeTestData, YesoulBikeTestData, +YpooEllipticalTestData, ZwiftRunpodTestData, -iConceptBikeTestData>; +ZiproTreadmillTestData, +iConceptBikeTestData, +iConceptEllipticalTestData>; #endif diff --git a/tst/Devices/iConceptBike/iconceptbiketestdata.h b/tst/Devices/iConceptBike/iconceptbiketestdata.h index 576cca834..1bd6444ce 100644 --- a/tst/Devices/iConceptBike/iconceptbiketestdata.h +++ b/tst/Devices/iConceptBike/iconceptbiketestdata.h @@ -5,16 +5,28 @@ class iConceptBikeTestData : public BluetoothDeviceTestData { -public: +void configureSettings(const DeviceDiscoveryInfo &info, bool enable, + std::vector &configurations) const override { + DeviceDiscoveryInfo config(info); + + if (enable) { + config.iconcept_elliptical = false; + configurations.push_back(config); + } else { + config.iconcept_elliptical = true; + configurations.push_back(config); + } + } + + public: iConceptBikeTestData() : BluetoothDeviceTestData("iConcept Bike") { this->addDeviceName("BH DUALKIT", comparison::StartsWithIgnoreCase); } - deviceType get_expectedDeviceType() const override { return deviceType::IConceptBike; } - bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override { - return dynamic_cast(detectedDevice)!=nullptr; + bool get_isExpectedDevice(bluetoothdevice *detectedDevice) const override { + return dynamic_cast(detectedDevice) != nullptr; } }; diff --git a/tst/Devices/iConceptElliptical/iconceptellipticaltestdata.h b/tst/Devices/iConceptElliptical/iconceptellipticaltestdata.h new file mode 100644 index 000000000..691f6c80d --- /dev/null +++ b/tst/Devices/iConceptElliptical/iconceptellipticaltestdata.h @@ -0,0 +1,31 @@ +#pragma once + +#include "Devices/bluetoothdevicetestdata.h" +#include "iconceptelliptical.h" + +class iConceptEllipticalTestData : public BluetoothDeviceTestData { + void configureSettings(const DeviceDiscoveryInfo &info, bool enable, + std::vector &configurations) const override { + DeviceDiscoveryInfo config(info); + + if (enable) { + config.iconcept_elliptical = true; + configurations.push_back(config); + } else { + config.iconcept_elliptical = false; + configurations.push_back(config); + } + } + + public: + iConceptEllipticalTestData() : BluetoothDeviceTestData("iConcept Elliptical") { + this->addDeviceName("BH DUALKIT", comparison::StartsWithIgnoreCase); + } + + deviceType get_expectedDeviceType() const override { return deviceType::IConceptElliptical; } + + bool get_isExpectedDevice(bluetoothdevice *detectedDevice) const override { + return dynamic_cast(detectedDevice) != nullptr; + } +}; + diff --git a/tst/qdomyos-zwift-tests.pro b/tst/qdomyos-zwift-tests.pro index 057565170..54fdf751d 100644 --- a/tst/qdomyos-zwift-tests.pro +++ b/tst/qdomyos-zwift-tests.pro @@ -20,6 +20,7 @@ SOURCES += \ Devices/TrxAppGateUSBTreadmill/trxappgateusbtreadmilltestdata.cpp \ Devices/bluetoothdevicetestdata.cpp \ Devices/bluetoothdevicetestsuite.cpp \ + Devices/bluetoothsignalreceiver.cpp \ Devices/devicediscoveryinfo.cpp \ ToolTests/testsettingstestsuite.cpp \ Tools/testsettings.cpp \ @@ -98,6 +99,7 @@ HEADERS += \ Devices/ProFormWiFiTreadmill/proformwifitreadmilltestdata.h \ Devices/RenphoBike/renphobiketestdata.h \ Devices/Rower/rowertestdata.h \ + Devices/Schwinn170Bike/schwinn170biketestdata.h \ Devices/SchwinnIC4Bike/schwinnic4biketestdata.h \ Devices/Shuaa5Treadmill/shuaa5treadmilltestdata.h \ Devices/SkandikaWiryBike/skandikawirybiketestdata.h \ @@ -122,10 +124,14 @@ HEADERS += \ Devices/UltrasportBike/ultrasportbiketestdata.h \ Devices/WahooKickrSnapBike/wahookickrsnapbiketestdata.h \ Devices/YesoulBike/yesoulbiketestdata.h \ + Devices/ZiproTreadmill/ziprotreadmilltestdata.h \ Devices/bluetoothdevicetestdata.h \ Devices/bluetoothdevicetestsuite.h \ + Devices/bluetoothsignalreceiver.h \ Devices/devicediscoveryinfo.h \ Devices/devices.h \ Devices/iConceptBike/iconceptbiketestdata.h \ + Devices/iConceptElliptical/iconceptellipticaltestdata.h \ + Devices/YpooElliptical/ypooellipticaltestdata.h \ ToolTests/testsettingstestsuite.h \ Tools/testsettings.h