diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16f55e228..704b46484 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -195,14 +195,14 @@ jobs: if: ${{ ! matrix.config.python }} - name: Archive windows binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: windows-binary path: windows-binary.zip if: matrix.config.python - name: Archive windows binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: windows-binary-no-python path: windows-binary-no-python.zip @@ -433,7 +433,7 @@ jobs: run: qmake; make -j8 - name: Archive linux-desktop binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: linux-desktop-binary path: src/qdomyos-zwift @@ -442,7 +442,7 @@ jobs: run: cd tst; GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests; cd .. - name: Upload test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() with: name: test_results_xml @@ -585,7 +585,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '11' + java-version: '11.0.23+9' - name: patching qt for bluetooth run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/ @@ -593,14 +593,6 @@ jobs: - name: download 3rd party files for qthttpserver run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/ - - name: Build qthttpserver - run: | - cd src/qthttpserver - qmake - make -j8 - make install - cd ../.. - - name: Set Android NDK 21 && build run: | # Install NDK 21 after GitHub update @@ -622,6 +614,14 @@ jobs: ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393 + + # QTHTTPSERVER must use the same NDK + cd src/qthttpserver + qmake + make -j8 + make install + cd ../.. + qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-deployment-settings.json cat src/android-qdomyos-zwift-deployment-settings.json @@ -630,7 +630,7 @@ jobs: run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab - name: Archive apk binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: fdroid-android-trial path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/ @@ -904,14 +904,14 @@ jobs: if: ${{ ! matrix.config.python }} - name: Archive windows binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: windows-msvc2019-binary path: windows-msvc2019-binary.zip if: matrix.config.python - name: Archive windows binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: windows-msvc2019-binary-no-python path: windows-msvc2019-binary-no-python.zip @@ -1047,7 +1047,7 @@ jobs: run: Compress-Archive src/debug/output windows-msvc2019-ai-server-binary.zip - name: Archive windows binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: windows-msvc2019-ai-server-binary path: windows-msvc2019-ai-server-binary.zip @@ -1059,7 +1059,7 @@ jobs: 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 + uses: actions/download-artifact@v4 - name: Update nightly release uses: andelf/nightly-release@main env: 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 a05b47edb..c4d12b515 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 @@ -152,6 +152,8 @@ 871189192893CECF006A04D1 /* libqavfmediaplayer.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 871189182893CECC006A04D1 /* libqavfmediaplayer.a */; }; 871235BF26B297670012D0F2 /* kingsmithr1protreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 871235BE26B297660012D0F2 /* kingsmithr1protreadmill.cpp */; }; 871235C126B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 871235C026B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp */; }; + 8715A3E72C75E6C9009BAC05 /* moc_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8715A3E62C75E6C9009BAC05 /* moc_antbike.cpp */; }; + 8715A3EA2C75E6DB009BAC05 /* antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8715A3E82C75E6DB009BAC05 /* antbike.cpp */; }; 87182A09276BBAF600141463 /* virtualrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87182A08276BBAF600141463 /* virtualrower.cpp */; }; 87182A0B276BBB1200141463 /* moc_virtualrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87182A0A276BBB1200141463 /* moc_virtualrower.cpp */; }; 8718CBA2263063BD004BF4EE /* soleelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8718CB9A263063BC004BF4EE /* soleelliptical.cpp */; }; @@ -174,6 +176,8 @@ 8727C7D12B3BF1B8005429EB /* QTelnet.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7CF2B3BF1B8005429EB /* QTelnet.cpp */; }; 8727C7D42B3BF1E4005429EB /* moc_QTelnet.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7D22B3BF1E4005429EB /* moc_QTelnet.cpp */; }; 8727C7D52B3BF1E4005429EB /* moc_proformtelnetbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8727C7D32B3BF1E4005429EB /* moc_proformtelnetbike.cpp */; }; + 872973822C6F13B100D6D9A4 /* moc_nordictrackifitadbelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872973812C6F13B000D6D9A4 /* moc_nordictrackifitadbelliptical.cpp */; }; + 872973852C6F13C400D6D9A4 /* nordictrackifitadbelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872973832C6F13C300D6D9A4 /* nordictrackifitadbelliptical.cpp */; }; 872A20DA28C5EC380037774D /* faketreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 872A20D928C5EC380037774D /* faketreadmill.cpp */; }; 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 */; }; @@ -354,6 +358,8 @@ 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 */; }; + 877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877758B22C98627300BB1697 /* moc_sportstechelliptical.cpp */; }; + 877758B62C98629B00BB1697 /* sportstechelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877758B52C98629B00BB1697 /* sportstechelliptical.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 */; }; @@ -916,6 +922,9 @@ 871235BD26B297660012D0F2 /* kingsmithr1protreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = kingsmithr1protreadmill.h; path = ../src/devices/kingsmithr1protreadmill/kingsmithr1protreadmill.h; sourceTree = ""; }; 871235BE26B297660012D0F2 /* kingsmithr1protreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = kingsmithr1protreadmill.cpp; path = ../src/devices/kingsmithr1protreadmill/kingsmithr1protreadmill.cpp; sourceTree = ""; }; 871235C026B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr1protreadmill.cpp; sourceTree = ""; }; + 8715A3E62C75E6C9009BAC05 /* moc_antbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_antbike.cpp; sourceTree = ""; }; + 8715A3E82C75E6DB009BAC05 /* antbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = antbike.cpp; path = ../src/devices/antbike/antbike.cpp; sourceTree = ""; }; + 8715A3E92C75E6DB009BAC05 /* antbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = antbike.h; path = ../src/devices/antbike/antbike.h; sourceTree = ""; }; 87182A07276BBAF600141463 /* virtualrower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = virtualrower.h; path = ../src/virtualdevices/virtualrower.h; sourceTree = ""; }; 87182A08276BBAF600141463 /* virtualrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = virtualrower.cpp; path = ../src/virtualdevices/virtualrower.cpp; sourceTree = ""; }; 87182A0A276BBB1200141463 /* moc_virtualrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_virtualrower.cpp; sourceTree = ""; }; @@ -951,6 +960,9 @@ 8727C7D22B3BF1E4005429EB /* moc_QTelnet.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_QTelnet.cpp; sourceTree = ""; }; 8727C7D32B3BF1E4005429EB /* moc_proformtelnetbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtelnetbike.cpp; sourceTree = ""; }; 8729149E2B2B010600565E33 /* qdomyoszwift-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "qdomyoszwift-Bridging-Header.h"; sourceTree = ""; }; + 872973812C6F13B000D6D9A4 /* moc_nordictrackifitadbelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbelliptical.cpp; sourceTree = ""; }; + 872973832C6F13C300D6D9A4 /* nordictrackifitadbelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbelliptical.cpp; path = ../src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.cpp; sourceTree = ""; }; + 872973842C6F13C400D6D9A4 /* nordictrackifitadbelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbelliptical.h; path = ../src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h; sourceTree = ""; }; 872A20D828C5EC380037774D /* faketreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = faketreadmill.h; path = ../src/devices/faketreadmill/faketreadmill.h; sourceTree = ""; }; 872A20D928C5EC380037774D /* faketreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = faketreadmill.cpp; path = ../src/devices/faketreadmill/faketreadmill.cpp; sourceTree = ""; }; 872A20DB28C5F5CE0037774D /* moc_faketreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_faketreadmill.cpp; sourceTree = ""; }; @@ -1225,6 +1237,9 @@ 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = iconceptelliptical.cpp; path = ../src/devices/iconceptelliptical/iconceptelliptical.cpp; sourceTree = ""; }; 8775008229E876F7008E48B7 /* iconceptelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iconceptelliptical.h; path = ../src/devices/iconceptelliptical/iconceptelliptical.h; sourceTree = ""; }; 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_iconceptelliptical.cpp; sourceTree = ""; }; + 877758B22C98627300BB1697 /* moc_sportstechelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechelliptical.cpp; sourceTree = ""; }; + 877758B42C98629B00BB1697 /* sportstechelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = sportstechelliptical.h; path = ../src/devices/sportstechelliptical/sportstechelliptical.h; sourceTree = ""; }; + 877758B52C98629B00BB1697 /* sportstechelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sportstechelliptical.cpp; path = ../src/devices/sportstechelliptical/sportstechelliptical.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/devices/bowflextreadmill/bowflextreadmill.cpp; sourceTree = ""; }; @@ -2058,6 +2073,15 @@ 2EB56BE3C2D93CDAB0C52E67 /* Sources */ = { isa = PBXGroup; children = ( + 877758B52C98629B00BB1697 /* sportstechelliptical.cpp */, + 877758B42C98629B00BB1697 /* sportstechelliptical.h */, + 877758B22C98627300BB1697 /* moc_sportstechelliptical.cpp */, + 8715A3E82C75E6DB009BAC05 /* antbike.cpp */, + 8715A3E92C75E6DB009BAC05 /* antbike.h */, + 8715A3E62C75E6C9009BAC05 /* moc_antbike.cpp */, + 872973832C6F13C300D6D9A4 /* nordictrackifitadbelliptical.cpp */, + 872973842C6F13C400D6D9A4 /* nordictrackifitadbelliptical.h */, + 872973812C6F13B000D6D9A4 /* moc_nordictrackifitadbelliptical.cpp */, 873D3C4B2C296B3700770CB9 /* jumprope.cpp */, 873D3C4C2C296B3700770CB9 /* jumprope.h */, 873D3C492C296B0100770CB9 /* moc_jumprope.cpp */, @@ -3325,6 +3349,7 @@ 8768C8C92BBC11C80099DBE1 /* adb_client.c in Compile Sources */, 873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */, DD5ED224478CB82859C61B9F /* fit_buffered_record_mesg_broadcaster.cpp in Compile Sources */, + 872973852C6F13C400D6D9A4 /* nordictrackifitadbelliptical.cpp in Compile Sources */, 87368825259C602800C71C7E /* watchAppStart.swift in Compile Sources */, 87BCE6BF29F28F95001F70EB /* moc_ypooelliptical.cpp in Compile Sources */, 876ED21925C3E9000065F3DC /* moc_ftmsbike.cpp in Compile Sources */, @@ -3347,6 +3372,7 @@ 879F16462847E55C00CE4945 /* proformellipticaltrainer.cpp in Compile Sources */, 8730A3922B404159007E336D /* zwift_protobuf_layer.swift in Compile Sources */, 87917A7728E768D200F8D9AC /* Client.swift in Compile Sources */, + 872973822C6F13B100D6D9A4 /* moc_nordictrackifitadbelliptical.cpp in Compile Sources */, 873824B927E64707004F1B46 /* moc_provider.cpp in Compile Sources */, 8727A47727849EA600019B5D /* paferstreadmill.cpp in Compile Sources */, DF1FD9718B75FA591A7E3D80 /* fit_decode.cpp in Compile Sources */, @@ -3407,6 +3433,7 @@ 873824AF27E64706004F1B46 /* moc_characteristicwriteprocessor2ad9.cpp in Compile Sources */, 25F2400F80DAFBD41FE5CC75 /* fit_field.cpp in Compile Sources */, 873824E227E647A8004F1B46 /* dns.cpp in Compile Sources */, + 8715A3EA2C75E6DB009BAC05 /* antbike.cpp in Compile Sources */, 87A3BC27265642A300D302E3 /* moc_echelonrower.cpp in Compile Sources */, 8768C9092BBC12B80099DBE1 /* socket_local_server.c in Compile Sources */, 87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */, @@ -3494,6 +3521,7 @@ 87C5F0C026285E5F0067A1B5 /* mimepart.cpp in Compile Sources */, 47E45EE0BB22C1E4332F1D1D /* trxappgateusbtreadmill.cpp in Compile Sources */, 873824BB27E64707004F1B46 /* moc_prober_p.cpp in Compile Sources */, + 877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */, 8742C2B227C92C30007D3FA0 /* wahookickrsnapbike.cpp in Compile Sources */, 87EB918327EE5FE7002535E1 /* moc_inappstore.cpp in Compile Sources */, 87CF516B293C87B000A7CABC /* moc_characteristicwriteprocessore005.cpp in Compile Sources */, @@ -3558,6 +3586,7 @@ 8783153C25E8DAFD0007817C /* sportstechbike.cpp in Compile Sources */, 873824E527E647A8004F1B46 /* message.cpp in Compile Sources */, 210F6A0A7E2FA7CDD3CA0084 /* qdomyoszwift_qml_plugin_import.cpp in Compile Sources */, + 8715A3E72C75E6C9009BAC05 /* moc_antbike.cpp in Compile Sources */, 87062644259480A600D06586 /* APIFetcher.swift in Compile Sources */, 8768C8C72BBC11C80099DBE1 /* adb_auth_host.c in Compile Sources */, 87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */, @@ -3603,6 +3632,7 @@ 87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */, 87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */, 877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */, + 877758B62C98629B00BB1697 /* sportstechelliptical.cpp in Compile Sources */, 8762D5102601F7EA00F6F049 /* M3iNSQT.cpp in Compile Sources */, 872261EE289EA873006A6F75 /* nordictrackelliptical.cpp in Compile Sources */, 8718CBA3263063BD004BF4EE /* templateinfosender.cpp in Compile Sources */, @@ -4026,7 +4056,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 829; + CURRENT_PROJECT_VERSION = 890; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1"; @@ -4105,7 +4135,7 @@ /Users/cagnulein/Qt/5.15.2/ios/plugins/audio, "/Users/cagnulein/qdomyos-zwift/src/ios/adb", ); - MARKETING_VERSION = 2.16; + MARKETING_VERSION = 2.17; OTHER_CFLAGS = ( "-pipe", "-g", @@ -4217,7 +4247,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 829; + CURRENT_PROJECT_VERSION = 890; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = NO; @@ -4298,7 +4328,7 @@ /Users/cagnulein/Qt/5.15.2/ios/plugins/audio, "/Users/cagnulein/qdomyos-zwift/src/ios/adb", ); - MARKETING_VERSION = 2.16; + MARKETING_VERSION = 2.17; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-pipe", @@ -4444,7 +4474,7 @@ CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 829; + CURRENT_PROJECT_VERSION = 890; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4469,7 +4499,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MARKETING_VERSION = 2.16; + MARKETING_VERSION = 2.17; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4540,7 +4570,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 829; + CURRENT_PROJECT_VERSION = 890; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 6335M7T29D; ENABLE_BITCODE = YES; @@ -4561,7 +4591,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MARKETING_VERSION = 2.16; + MARKETING_VERSION = 2.17; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4632,7 +4662,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 829; + CURRENT_PROJECT_VERSION = 890; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; ENABLE_PREVIEWS = YES; @@ -4677,7 +4707,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.16; + MARKETING_VERSION = 2.17; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; @@ -4746,7 +4776,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 829; + CURRENT_PROJECT_VERSION = 890; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\""; ENABLE_BITCODE = YES; @@ -4787,7 +4817,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.16; + MARKETING_VERSION = 2.17; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der"; diff --git a/src/EventHandler.h b/src/EventHandler.h new file mode 100644 index 000000000..4fe2859aa --- /dev/null +++ b/src/EventHandler.h @@ -0,0 +1,115 @@ +#ifndef EVENTHANDLER_H +#define EVENTHANDLER_H + +#include +#include +#include + +#ifdef Q_OS_LINUX +#ifndef Q_OS_ANDROID +#include +#include "bluetooth.h" + +class EventHandler : public QObject +{ + Q_OBJECT + + public: + EventHandler(const QString& devicePath, QObject* parent = nullptr) + : QObject(parent), m_devicePath(devicePath), m_notifier(nullptr), m_fd(-1) {} + + ~EventHandler() { + if (m_fd != -1) { + ::close(m_fd); + } + } + + bool initialize() { + m_fd = ::open(m_devicePath.toStdString().c_str(), O_RDONLY | O_NONBLOCK); + if (m_fd == -1) { + qDebug() << "Failed to open device:" << m_devicePath; + emit error(QString("Failed to open device: %1").arg(m_devicePath)); + return false; + } + m_notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, &EventHandler::handleEvent); + qDebug() << "Device opened successfully:" << m_devicePath; + return true; + } + + signals: + void keyPressed(int keyCode); + void error(const QString& errorMessage); + + private slots: + void handleEvent() { + input_event ev; + ssize_t bytesRead = ::read(m_fd, &ev, sizeof(ev)); + + if (bytesRead == sizeof(ev)) { + if (ev.type == EV_KEY && ev.value == 1) { // Key press event + emit keyPressed(ev.code); + } + } else if (bytesRead == 0) { + qDebug() << "End of file reached."; + m_notifier->setEnabled(false); + } else if (bytesRead == -1) { + qDebug() << "Read error:" << strerror(errno); + emit error(QString("Failed to read from device: %1").arg(strerror(errno))); + } + } + + private: + QString m_devicePath; + int m_fd; + QSocketNotifier* m_notifier; +}; + +class BluetoothHandler : public QObject +{ + Q_OBJECT + + public: + BluetoothHandler(bluetooth* bl, QString eventDevice, QObject* parent = nullptr) + : QObject(parent), m_bluetooth(bl) + { + m_handler = new EventHandler(eventDevice); // Adjust this path as needed + + if (!m_handler->initialize()) { + qDebug() << "Failed to initialize EventHandler."; + return; + } + + connect(m_handler, &EventHandler::keyPressed, this, &BluetoothHandler::onKeyPressed); + connect(m_handler, &EventHandler::error, this, &BluetoothHandler::onError); + } + + ~BluetoothHandler() { + delete m_handler; + } + + private slots: + void onKeyPressed(int keyCode) + { + qDebug() << "Key pressed:" << keyCode; + if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == bluetoothdevice::BIKE) { + if (keyCode == 115) // up + ((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() + 1); + else if (keyCode == 114) // down + ((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() - 1); + } + } + + void onError(const QString& errorMessage) + { + qDebug() << "Error:" << errorMessage; + } + + private: + EventHandler* m_handler; + bluetooth* m_bluetooth; +}; + +#endif // EVENTHANDLER_H +#endif // EVENTHANDLER_H +#endif // EVENTHANDLER_H diff --git a/src/Wizard.qml b/src/Wizard.qml index 0b3aa7085..53592c93d 100644 --- a/src/Wizard.qml +++ b/src/Wizard.qml @@ -839,7 +839,7 @@ Page { Text { Layout.alignment: Qt.AlignHCenter - text: qsTr("Corrent startup phase:\n\n1. close any app that can connect to your Zwift devices\n2. wake up your Zwift devices\n3. wake up your trainer\n4. open qz\n5. now if you change gear on your Zwift device you will see a reaction in the gear tile on qz and so on your trainer.") + text: qsTr("Correct startup phase:\n\n1. close any app that can connect to your Zwift devices\n2. wake up your Zwift devices\n3. wake up your trainer\n4. open qz\n5. now if you change gear on your Zwift device you will see a reaction in the gear tile on qz and so on your trainer.") wrapMode: Text.WordWrap Layout.fillWidth: true width: stackViewLocal.width * 0.8 @@ -1210,11 +1210,11 @@ Page { Layout.alignment: Qt.AlignHCenter from: settings.miles_unit ? 660 : 300 // 66.0 lbs or 30.0 kg to: settings.miles_unit ? 4400 : 2000 // 440.0 lbs or 200.0 kg - value: settings.weight * 10 + value: settings.miles_unit ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10) stepSize: 1 editable: true - property real realValue: value / 10 + property real realValue: settings.miles_unit ? value / 22.0462 : value / 10 textFromValue: function(value, locale) { return Number(value / 10).toLocaleString(locale, 'f', 1) @@ -1223,6 +1223,10 @@ Page { valueFromText: function(text, locale) { return Number.fromLocaleString(locale, text) * 10 } + + onValueChanged: { + settings.weight = realValue + } } Text { diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index 0b2db42b4..d7bca6606 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -1,5 +1,5 @@ - + @@ -111,6 +111,12 @@ android:value="ocr" /> + + + + + + @@ -119,6 +125,7 @@ + @@ -129,6 +136,7 @@ + diff --git a/src/android/libs/fgchecker-1.1.0.aar b/src/android/libs/fgchecker-1.1.0.aar deleted file mode 100644 index 89666527d..000000000 Binary files a/src/android/libs/fgchecker-1.1.0.aar and /dev/null differ diff --git a/src/android/libs/fgchecker-1.1.0.pom b/src/android/libs/fgchecker-1.1.0.pom deleted file mode 100644 index e001f9852..000000000 --- a/src/android/libs/fgchecker-1.1.0.pom +++ /dev/null @@ -1,17 +0,0 @@ - - - 4.0.0 - com.rvalerio - fgchecker - 1.1.0 - aar - - - com.android.support - appcompat-v7 - 26.0.1 - runtime - - - diff --git a/src/android/src/Garmin.java b/src/android/src/Garmin.java index 6792a9cc0..53c06eafa 100644 --- a/src/android/src/Garmin.java +++ b/src/android/src/Garmin.java @@ -49,12 +49,24 @@ public class Garmin { private static Integer HR = 0; private static Integer FootCad = 0; + private static Double Speed = 0.0; + private static Integer Power = 0; public static int getHR() { Log.d(TAG, "getHR " + HR); return HR; } + public static int getPower() { + Log.d(TAG, "getPower " + Power); + return Power; + } + + public static double getSpeed() { + Log.d(TAG, "getSpeed " + Speed); + return Speed; + } + public static int getFootCad() { Log.d(TAG, "getFootCad " + FootCad); return FootCad; @@ -232,6 +244,10 @@ public void onMessageReceived(IQDevice device, IQApp app, List message, 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]); + if(var.length > 2) { + Power = Integer.parseInt(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]); + Speed = Double.parseDouble(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]); + } } Log.d(TAG, "HR " + HR); Log.d(TAG, "FootCad " + FootCad); diff --git a/src/android/src/MediaButtonReceiver.java b/src/android/src/MediaButtonReceiver.java new file mode 100644 index 000000000..60d9a0f02 --- /dev/null +++ b/src/android/src/MediaButtonReceiver.java @@ -0,0 +1,45 @@ +package org.cagnulen.qdomyoszwift; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.util.Log; + +public class MediaButtonReceiver extends BroadcastReceiver { + private static MediaButtonReceiver instance; + + @Override + public void onReceive(Context context, Intent intent) { + Log.d("MediaButtonReceiver", "Received intent: " + intent.toString()); + String intentAction = intent.getAction(); + if ("android.media.VOLUME_CHANGED_ACTION".equals(intentAction)) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1); + int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1); + + Log.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume); + nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume); + } + } + + private native void nativeOnMediaButtonEvent(int prev, int current, int max); + + public static void registerReceiver(Context context) { + if (instance == null) { + instance = new MediaButtonReceiver(); + } + IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION"); + context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED); + Log.d("MediaButtonReceiver", "registerReceiver"); + } + + public static void unregisterReceiver(Context context) { + if (instance != null) { + context.unregisterReceiver(instance); + instance = null; + } + } +} diff --git a/src/android/src/com/rvalerio/fgchecker/AppChecker.java b/src/android/src/com/rvalerio/fgchecker/AppChecker.java new file mode 100644 index 000000000..afa96ba4a --- /dev/null +++ b/src/android/src/com/rvalerio/fgchecker/AppChecker.java @@ -0,0 +1,126 @@ +package com.rvalerio.fgchecker; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.rvalerio.fgchecker.detectors.Detector; +import com.rvalerio.fgchecker.detectors.LollipopDetector; +import com.rvalerio.fgchecker.detectors.PreLollipopDetector; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + + +public class AppChecker { + static final int DEFAULT_TIMEOUT = 1000; + + int timeout = DEFAULT_TIMEOUT; + ScheduledExecutorService service; + Runnable runnable; + Listener unregisteredPackageListener; + Listener anyPackageListener; + Map listeners; + Detector detector; + Handler handler; + + public interface Listener { + void onForeground(String process); + } + + + public AppChecker() { + listeners = new HashMap<>(); + handler = new Handler(Looper.getMainLooper()); + if(Utils.postLollipop()) + detector = new LollipopDetector(); + else + detector = new PreLollipopDetector(); + } + + public AppChecker timeout(int timeout) { + this.timeout = timeout; + return this; + } + + public AppChecker when(String packageName, Listener listener) { + listeners.put(packageName, listener); + return this; + } + + @Deprecated + public AppChecker other(Listener listener) { + return whenOther(listener); + } + + public AppChecker whenOther(Listener listener) { + unregisteredPackageListener = listener; + return this; + } + + public AppChecker whenAny(Listener listener) { + anyPackageListener = listener; + return this; + } + + public void start(Context context) { + runnable = createRunnable(context.getApplicationContext()); + service = new ScheduledThreadPoolExecutor(1); + service.schedule(runnable, timeout, TimeUnit.MILLISECONDS); + } + + public void stop() { + if(service != null) { + service.shutdownNow(); + service = null; + } + runnable = null; + } + + private Runnable createRunnable(final Context context) { + return new Runnable() { + @Override + public void run() { + getForegroundAppAndNotify(context); + service.schedule(createRunnable(context), timeout, TimeUnit.MILLISECONDS); + } + }; + } + + private void getForegroundAppAndNotify(Context context) { + final String foregroundApp = getForegroundApp(context); + boolean foundRegisteredPackageListener = false; + if(foregroundApp != null) { + for (String packageName : listeners.keySet()) { + if (packageName.equalsIgnoreCase(foregroundApp)) { + foundRegisteredPackageListener = true; + callListener(listeners.get(foregroundApp), foregroundApp); + } + } + + if(!foundRegisteredPackageListener && unregisteredPackageListener != null) { + callListener(unregisteredPackageListener, foregroundApp); + } + } + if(anyPackageListener != null) { + callListener(anyPackageListener, foregroundApp); + } + } + + void callListener(final Listener listener, final String packageName) { + handler.post(new Runnable() { + @Override + public void run() { + listener.onForeground(packageName); + } + }); + } + + public String getForegroundApp(Context context) { + return detector.getForegroundApp(context); + } +} \ No newline at end of file diff --git a/src/android/src/com/rvalerio/fgchecker/Utils.java b/src/android/src/com/rvalerio/fgchecker/Utils.java new file mode 100644 index 000000000..db62c4d8a --- /dev/null +++ b/src/android/src/com/rvalerio/fgchecker/Utils.java @@ -0,0 +1,26 @@ +package com.rvalerio.fgchecker; + +import android.annotation.TargetApi; +import android.app.AppOpsManager; +import android.content.Context; +import android.os.Build; + +public class Utils { + private Utils() { + + } + + public static boolean postLollipop() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static boolean hasUsageStatsPermission(Context context) { + AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + int mode = appOps.checkOpNoThrow("android:get_usage_stats", + android.os.Process.myUid(), context.getPackageName()); + boolean granted = mode == AppOpsManager.MODE_ALLOWED; + return granted; + } + +} diff --git a/src/android/src/com/rvalerio/fgchecker/detectors/Detector.java b/src/android/src/com/rvalerio/fgchecker/detectors/Detector.java new file mode 100644 index 000000000..d61adf25c --- /dev/null +++ b/src/android/src/com/rvalerio/fgchecker/detectors/Detector.java @@ -0,0 +1,8 @@ +package com.rvalerio.fgchecker.detectors; + + +import android.content.Context; + +public interface Detector { + String getForegroundApp(Context context); +} diff --git a/src/android/src/com/rvalerio/fgchecker/detectors/LollipopDetector.java b/src/android/src/com/rvalerio/fgchecker/detectors/LollipopDetector.java new file mode 100644 index 000000000..6c8eb38da --- /dev/null +++ b/src/android/src/com/rvalerio/fgchecker/detectors/LollipopDetector.java @@ -0,0 +1,34 @@ +package com.rvalerio.fgchecker.detectors; + +import android.annotation.TargetApi; +import android.app.Service; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.os.Build; + +import com.rvalerio.fgchecker.Utils; + +public class LollipopDetector implements Detector { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public String getForegroundApp(final Context context) { + if(!Utils.hasUsageStatsPermission(context)) + return null; + + String foregroundApp = null; + + UsageStatsManager mUsageStatsManager = (UsageStatsManager) context.getSystemService(Service.USAGE_STATS_SERVICE); + long time = System.currentTimeMillis(); + + UsageEvents usageEvents = mUsageStatsManager.queryEvents(time - 1000 * 3600, time); + UsageEvents.Event event = new UsageEvents.Event(); + while (usageEvents.hasNextEvent()) { + usageEvents.getNextEvent(event); + if(event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND) { + foregroundApp = event.getPackageName(); + } + } + + return foregroundApp ; + } +} diff --git a/src/android/src/com/rvalerio/fgchecker/detectors/PreLollipopDetector.java b/src/android/src/com/rvalerio/fgchecker/detectors/PreLollipopDetector.java new file mode 100644 index 000000000..0fb74c07c --- /dev/null +++ b/src/android/src/com/rvalerio/fgchecker/detectors/PreLollipopDetector.java @@ -0,0 +1,30 @@ +package com.rvalerio.fgchecker.detectors; + +import android.app.ActivityManager; +import android.app.Service; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +public class PreLollipopDetector implements Detector { + @Override + public String getForegroundApp(Context context) { + ActivityManager am = (ActivityManager) context.getSystemService(Service.ACTIVITY_SERVICE); + ActivityManager.RunningTaskInfo foregroundTaskInfo = am.getRunningTasks(1).get(0); + String foregroundTaskPackageName = foregroundTaskInfo .topActivity.getPackageName(); + PackageManager pm = context.getPackageManager(); + PackageInfo foregroundAppPackageInfo = null; + try { + foregroundAppPackageInfo = pm.getPackageInfo(foregroundTaskPackageName, 0); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return null; + } + + String foregroundApp = null; + if(foregroundAppPackageInfo != null) + foregroundApp = foregroundAppPackageInfo.applicationInfo.packageName; + + return foregroundApp; + } +} diff --git a/src/characteristics/characteristicwriteprocessor2ad9.cpp b/src/characteristics/characteristicwriteprocessor2ad9.cpp index 71459f032..cc9d1e772 100644 --- a/src/characteristics/characteristicwriteprocessor2ad9.cpp +++ b/src/characteristics/characteristicwriteprocessor2ad9.cpp @@ -96,8 +96,6 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra int16_t sincline = a + (((int16_t)b) << 8); double requestIncline = (double)sincline / 10.0; - if (requestIncline < 0) - requestIncline = 0; if (dt == bluetoothdevice::TREADMILL) ((treadmill *)Bike)->changeInclination(requestIncline, requestIncline); diff --git a/src/devices/antbike/antbike.cpp b/src/devices/antbike/antbike.cpp new file mode 100644 index 000000000..238b389c7 --- /dev/null +++ b/src/devices/antbike/antbike.cpp @@ -0,0 +1,197 @@ +#include "antbike.h" +#include "virtualdevices/virtualbike.h" + +#include +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#include +#endif +#include + +using namespace std::chrono_literals; + +antbike::antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) { + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->noVirtualDevice = noVirtualDevice; + initDone = false; + connect(refresh, &QTimer::timeout, this, &antbike::update); + refresh->start(200ms); +} + +void antbike::update() { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + lockscreen hh; + Cadence = hh.getFootCad(); + m_watt = hh.getPower(); + qDebug() << QStringLiteral("Current Garmin Cadence: ") << QString::number(Cadence.value()); + if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = metric::calculateSpeedFromPower( + m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), + speedLimit()); + } else { + Speed = hh.getSpeed(); + } +#endif +#endif + +#ifdef Q_OS_ANDROID + Cadence = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Garmin", "getFootCad", "()I"); + m_watt = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Garmin", "getPower", "()I"); + if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = metric::calculateSpeedFromPower( + m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), + speedLimit()); + } else { + Speed = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Garmin", "getSpeed", "()D"); + } + qDebug() << QStringLiteral("Current Garmin Cadence: ") << QString::number(Cadence.value()); +#endif + + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + + if (requestInclination != -100) { + Inclination = requestInclination; + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); + requestInclination = -100; + } + + update_metrics(false, watts()); + + Distance += ((Speed.value() / (double)3600.0) / + ((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())))); + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + + // ******************************************* virtual bike init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + && !h +#endif +#endif + ) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence) { + qDebug() << "ios_peloton_workaround activated!"; + h = new lockscreen(); + h->virtualbike_ios(); + } else +#endif +#endif + if (virtual_device_enabled) { + emit debug(QStringLiteral("creating virtual bike interface...")); + auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService); + connect(virtualBike, &virtualbike::changeInclination, this, &antbike::changeInclinationRequested); + connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &antbike::ftmsCharacteristicChanged); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + if (!firstStateChanged) + emit connectedAndDiscovered(); + firstStateChanged = 1; + // ******************************************************************************************************** + + if (!noVirtualDevice) { +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { + Heart = (uint8_t)KeepAwakeHelper::heart(); + debug("Current Heart: " + QString::number(Heart.value())); + } +#endif + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); + } +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadence = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadence && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + } + + if (Heart.value()) { + static double lastKcal = 0; + if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator + lastKcal = abs(KCal.value()); + KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal; + } + + if (requestResistance != -1 && requestResistance != currentResistance().value()) { + Resistance = requestResistance; + m_pelotonResistance = requestResistance; + } +} + +void antbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + QByteArray b = newValue; + qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' '); +} + +void antbike::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} + +uint16_t antbike::wattsFromResistance(double resistance) { + return _ergTable.estimateWattage(Cadence.value(), resistance); +} + +resistance_t antbike::resistanceFromPowerRequest(uint16_t power) { + //QSettings settings; + //bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool(); + /*if(toorx_srx_3500)*/ { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value(); + + if (Cadence.value() == 0) + return 1; + + for (resistance_t i = 1; i < maxResistance(); i++) { + if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i) + << wattsFromResistance(i + 1) << power; + return i; + } + } + if (power < wattsFromResistance(1)) + return 1; + else + return maxResistance(); + } /*else { + return power / 10; + }*/ +} + + +uint16_t antbike::watts() { return m_watt.value(); } +bool antbike::connected() { return true; } diff --git a/src/devices/antbike/antbike.h b/src/devices/antbike/antbike.h new file mode 100644 index 000000000..7c9dde21b --- /dev/null +++ b/src/devices/antbike/antbike.h @@ -0,0 +1,81 @@ +#ifndef ANTBIKE_H +#define ANTBIKE_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 "devices/bike.h" +#include "ergtable.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class antbike : public bike { + Q_OBJECT + public: + antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice); + bool connected() override; + uint16_t watts() override; + resistance_t maxResistance() override { return 100; } + resistance_t resistanceFromPowerRequest(uint16_t power) override; + + private: + QTimer *refresh; + + uint8_t sec1Update = 0; + QByteArray lastPacket; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastGoodCadence = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + bool noVirtualDevice = false; + + uint16_t oldLastCrankEventTime = 0; + uint16_t oldCrankRevs = 0; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + uint16_t wattsFromResistance(double resistance); + + signals: + void disconnected(); + void debug(QString string); + + private slots: + void changeInclinationRequested(double grade, double percentage); + void update(); + + void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); +}; +#endif // ANTBIKE_H diff --git a/src/devices/bike.cpp b/src/devices/bike.cpp index c815f6f7a..d4a0e58f9 100644 --- a/src/devices/bike.cpp +++ b/src/devices/bike.cpp @@ -83,10 +83,28 @@ void bike::changePower(int32_t power) { } } -double bike::gears() { return m_gears; } +double bike::gears() { + QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + double gears_offset = settings.value(QZSettings::gears_offset, QZSettings::default_gears_offset).toDouble(); + if(gears_zwift_ratio) { + if(m_gears < 1) + return 1.0; + else if(m_gears > 24) + return 24.0; + } + return m_gears + gears_offset; +} void bike::setGears(double gears) { QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + double gears_offset = settings.value(QZSettings::gears_offset, QZSettings::default_gears_offset).toDouble(); + gears -= gears_offset; qDebug() << "setGears" << gears; + if(gears_zwift_ratio && (gears > 24 || gears < 1)) { + qDebug() << "new gear value ignored because of gears_zwift_ratio setting!"; + return; + } m_gears = gears; settings.setValue(QZSettings::gears_current_value, m_gears); if (lastRawRequestedResistanceValue != -1) { @@ -308,3 +326,61 @@ uint16_t bike::wattFromHR(bool useSpeedAndCadence) { } return watt; } + +double bike::gearsZwiftRatio() { + if(m_gears <= 0) + return 0.65; + else if(m_gears > 24) + return 6; + switch((int)m_gears) { + case 1: + return 0.75; + case 2: + return 0.87; + case 3: + return 0.99; + case 4: + return 1.11; + case 5: + return 1.23; + case 6: + return 1.38; + case 7: + return 1.53; + case 8: + return 1.68; + case 9: + return 1.86; + case 10: + return 2.04; + case 11: + return 2.22; + case 12: + return 2.40; + case 13: + return 2.61; + case 14: + return 2.82; + case 15: + return 3.03; + case 16: + return 3.24; + case 17: + return 3.49; + case 18: + return 3.74; + case 19: + return 3.99; + case 20: + return 4.24; + case 21: + return 4.54; + case 22: + return 4.84; + case 23: + return 5.14; + case 24: + return 5.49; + } + return 1; +} diff --git a/src/devices/bike.h b/src/devices/bike.h index 247cde8c9..c296a9138 100644 --- a/src/devices/bike.h +++ b/src/devices/bike.h @@ -36,6 +36,7 @@ class bike : public bluetoothdevice { uint8_t metrics_override_heartrate() override; void setGears(double d); double gears(); + double gearsZwiftRatio(); void setSpeedLimit(double speed) { m_speedLimit = speed; } double speedLimit() { return m_speedLimit; } virtual bool ifitCompatible() {return false;} @@ -59,10 +60,18 @@ class bike : public bluetoothdevice { void changeInclination(double grade, double percentage) override; virtual void changeSteeringAngle(double angle) { m_steeringAngle = angle; } virtual void resistanceFromFTMSAccessory(resistance_t res) { Q_UNUSED(res); } - void gearUp() {QSettings settings; setGears(gears() + - settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble());} - void gearDown() {QSettings settings; setGears(gears() - - settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble());} + void gearUp() { + QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + setGears(gears() + (gears_zwift_ratio ? 1 : + settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble())); + } + void gearDown() { + QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + setGears(gears() - (gears_zwift_ratio ? 1 : + settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble())); + } Q_SIGNALS: void bikeStarted(); @@ -74,11 +83,9 @@ class bike : public bluetoothdevice { metric RequestedResistance; metric RequestedPelotonResistance; metric RequestedCadence; - metric RequestedPower; resistance_t requestResistance = -1; double requestInclination = -100; - int16_t requestPower = -1; 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 diff --git a/src/devices/bluetooth.cpp b/src/devices/bluetooth.cpp index 512faad44..60989834c 100644 --- a/src/devices/bluetooth.cpp +++ b/src/devices/bluetooth.cpp @@ -107,9 +107,12 @@ void bluetooth::finished() { debug(QStringLiteral("BTLE scanning finished")); QSettings settings; + bool antbike = + settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool(); QString nordictrack_2950_ip = settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString(); QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); + QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString(); bool fake_bike = settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool(); bool fakedevice_elliptical = @@ -118,7 +121,7 @@ void bluetooth::finished() { bool fakedevice_treadmill = settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool(); // wifi devices on windows - if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill) { + if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || antbike) { // faking a bluetooth device qDebug() << "faking a bluetooth device for nordictrack_2950_ip"; deviceDiscovered(QBluetoothDeviceInfo()); @@ -164,10 +167,14 @@ void bluetooth::finished() { bool fitmetriaFanfitFound = !settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool(); + bool zwiftDeviceFound = + !settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool() && !settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool(); + if ((!heartRateBeltFound && !heartRateBeltAvaiable()) || (!ftmsAccessoryFound && !ftmsAccessoryAvaiable()) || (!cscFound && !cscSensorAvaiable()) || (!powerSensorFound && !powerSensorAvaiable()) || (!eliteRizerFound && !eliteRizerAvaiable()) || (!eliteSterzoSmartFound && !eliteSterzoSmartAvaiable()) || - (!fitmetriaFanfitFound && !fitmetriaFanfitAvaiable())) { + (!fitmetriaFanfitFound && !fitmetriaFanfitAvaiable()) || + (!zwiftDeviceFound && !zwiftDeviceAvaiable())) { // force heartRateBelt off forceHeartBeltOffForTimeout = true; @@ -274,6 +281,20 @@ bool bluetooth::fitmetriaFanfitAvaiable() { return false; } +bool bluetooth::zwiftDeviceAvaiable() { + + uint8_t count = 0; + Q_FOREACH (QBluetoothDeviceInfo b, devices) { + if (b.name().toUpper().startsWith("ZWIFT ")) { + count++; + if(count >= 2) + return true; + } + } + return false; +} + + bool bluetooth::powerSensorAvaiable() { QSettings settings; @@ -371,6 +392,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { settings.value(QZSettings::ftms_accessory_name, QZSettings::default_ftms_accessory_name).toString(); bool heartRateBeltFound = heartRateBeltName.startsWith(QStringLiteral("Disabled")); bool ftmsAccessoryFound = ftmsAccessoryName.startsWith(QStringLiteral("Disabled")); + bool zwiftDeviceFound = + !settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool() && !settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool(); bool fitmetriaFanfitFound = !settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool(); bool toorx_ftms = settings.value(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms).toBool(); @@ -423,9 +446,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { QString proformtdf1ip = settings.value(QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip).toString(); QString proformtreadmillip = settings.value(QZSettings::proformtreadmillip, QZSettings::default_proformtreadmillip).toString(); + bool antbike_setting = + settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool(); QString nordictrack_2950_ip = settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString(); QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); + QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_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(); @@ -458,6 +484,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { fitmetriaFanfitFound = fitmetriaFanfitAvaiable(); } + if (!zwiftDeviceFound) { + + zwiftDeviceFound = zwiftDeviceAvaiable(); + } if (!ftmsAccessoryFound) { ftmsAccessoryFound = ftmsAccessoryAvaiable(); @@ -589,7 +619,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { #endif bool searchDevices = (heartRateBeltFound && ftmsAccessoryFound && cscFound && powerSensorFound && eliteRizerFound && - eliteSterzoSmartFound && fitmetriaFanfitFound) || + eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound) || forceHeartBeltOffForTimeout; if (searchDevices) { @@ -623,6 +653,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit deviceConnected(b); connect(fakeBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); connect(fakeBike, &fakebike::inclinationChanged, this, &bluetooth::inclinationChanged); + connect(fakeBike, &fakebike::debug, this, &bluetooth::debug); // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(this, SIGNAL(searchingStop()), fakeBike, SLOT(searchingStop())); //NOTE: Commented due to // #358 @@ -637,6 +668,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(fakeElliptical, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); connect(fakeElliptical, &fakeelliptical::inclinationChanged, this, &bluetooth::inclinationChanged); + connect(fakeElliptical, &fakeelliptical::debug, this, &bluetooth::debug); // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(this, SIGNAL(searchingStop()), fakeBike, SLOT(searchingStop())); //NOTE: Commented due to // #358 @@ -650,6 +682,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit deviceConnected(b); connect(fakeRower, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); connect(fakeRower, &fakerower::inclinationChanged, this, &bluetooth::inclinationChanged); + connect(fakeRower, &fakerower::debug, this, &bluetooth::debug); // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(this, SIGNAL(searchingStop()), fakeBike, SLOT(searchingStop())); //NOTE: Commented due to // #358 @@ -664,6 +697,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { connect(fakeTreadmill, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered); connect(fakeTreadmill, &faketreadmill::inclinationChanged, this, &bluetooth::inclinationChanged); + connect(fakeTreadmill, &faketreadmill::debug, this, &bluetooth::debug); // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); // connect(this, SIGNAL(searchingStop()), fakeBike, SLOT(searchingStop())); //NOTE: Commented due to // #358 @@ -732,6 +766,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { } this->signalBluetoothDeviceConnected(csafeRower); #endif + } else if (antbike_setting && !antBike) { + this->stopDiscovery(); + antBike = new antbike(noWriteResistance, noHeartService, false); + emit deviceConnected(b); + connect(antBike, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart())); + connect(antBike, &antbike::debug, this, &bluetooth::debug); + // connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358 + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + this->signalBluetoothDeviceConnected(antBike); } else if (!proformtreadmillip.isEmpty() && !proformWifiTreadmill) { this->stopDiscovery(); proformWifiTreadmill = new proformwifitreadmill(noWriteResistance, noHeartService, bikeResistanceOffset, @@ -774,6 +821,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit searchingStop(); } this->signalBluetoothDeviceConnected(nordictrackifitadbBike); + } else if (!proform_elliptical_ip.isEmpty() && !nordictrackifitadbElliptical) { + this->stopDiscovery(); + nordictrackifitadbElliptical = new nordictrackifitadbelliptical(noWriteResistance, noHeartService, + bikeResistanceOffset, bikeResistanceGain); + emit deviceConnected(b); + connect(nordictrackifitadbElliptical, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + connect(nordictrackifitadbElliptical, &nordictrackifitadbelliptical::debug, this, &bluetooth::debug); + // nordictrackifitadbTreadmill->deviceDiscovered(b); + // connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358 + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + this->signalBluetoothDeviceConnected(nordictrackifitadbElliptical); } else if (((csc_as_bike && b.name().startsWith(cscName)) || b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) && !cscBike && filter) { @@ -840,7 +901,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit searchingStop(); } this->signalBluetoothDeviceConnected(domyosRower); - } else if ((b.name().startsWith(QStringLiteral("Domyos-Bike")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) && + } else if ((b.name().startsWith(QStringLiteral("Domyos-Bike")) && (!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool())) && !b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1045,7 +1106,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { } else if (b.name().startsWith(QStringLiteral("Domyos")) && !b.name().startsWith(QStringLiteral("DomyosBr")) && !b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKING-")) && !domyos && !domyosElliptical && b.name().compare(ftms_treadmill, Qt::CaseInsensitive) && - !domyosBike && !domyosRower && !ftmsBike && !horizonTreadmill && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && filter) { + !domyosBike && !domyosRower && !ftmsBike && !horizonTreadmill && + (!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts).toBool()) && + filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); domyos = new domyostreadmill(this->pollDeviceTime, noConsole, noHeartService); @@ -1114,6 +1177,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { b.name().toUpper().startsWith(QStringLiteral("KS-BLC")) || // Walkingpad C2 #1672 b.name().toUpper().startsWith( QStringLiteral("KS-BLR"))) && // Treadmill KingSmith WalkingPad R2 Pro KS-HCR1AA + !(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) && // it's an FTMS one !kingsmithR1ProTreadmill && !kingsmithR2Treadmill && filter) { this->setLastBluetoothDevice(b); @@ -1259,17 +1323,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { (b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")))) && !toorx_ftms && toorx_ftms_treadmill) || !b.name().compare(ftms_treadmill, Qt::CaseInsensitive) || - (b.name().toUpper().startsWith(QStringLiteral("DOMYOS-TC")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || + (b.name().toUpper().startsWith(QStringLiteral("DOMYOS-TC")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && !settings.value(QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts).toBool()) || b.name().toUpper().startsWith(QStringLiteral("XT685")) || b.name().toUpper().startsWith(QStringLiteral("XT285")) || b.name().toUpper().startsWith(QStringLiteral("XTERRA TR")) || b.name().toUpper().startsWith(QStringLiteral("T118_")) || - b.name().toUpper().startsWith(QStringLiteral("EW-EP-")) || // Miweba MC700 ellipital Trainer #2419 + b.name().toUpper().startsWith(QStringLiteral("RUNN ")) || b.name().toUpper().startsWith(QStringLiteral("TF04-")) || // Sport Synology Z5 Treadmill #2415 b.name().toUpper().startsWith(QStringLiteral("FIT-")) || // FIT-1596 b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical (b.name().toUpper().startsWith("SCHWINN 810")) || + b.name().toUpper().startsWith(QStringLiteral("KS-MC")) || + (b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1 (b.name().toUpper().startsWith(QStringLiteral("FIT-")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // sports tech f37s treadmill #2412 (b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS (b.name().toUpper().startsWith(QStringLiteral("TT8")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || @@ -1285,8 +1351,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { (b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS (b.name().toUpper().startsWith(QStringLiteral("F89")) && !sole_inclination) || // FMTS (b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS - (b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS - b.name().toUpper().startsWith(QStringLiteral("ESANGLINKER"))) && + (b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) // FTMS + ) && !horizonTreadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -1453,15 +1519,22 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { (b.name().toUpper().startsWith("WATTBIKE")) || (b.name().toUpper().startsWith("ZYCLEZBIKE")) || (b.name().toUpper().startsWith("WAVEFIT-")) || + (b.name().toUpper().startsWith("KETTLERBLE")) || (b.name().toUpper().startsWith("JAS_C3")) || (b.name().toUpper().startsWith("RAVE WHITE")) || (b.name().toUpper().startsWith("DOMYOS-BIKING-")) || - (b.name().startsWith(QStringLiteral("Domyos-Bike")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || + (b.name().startsWith(QStringLiteral("Domyos-Bike")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && !settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool()) || (b.name().toUpper().startsWith("F") && b.name().toUpper().endsWith("ARROW")) || // FI9110 Arrow, https://www.fitnessdigital.it/bicicletta-smart-bike-ion-fitness-arrow-connect/p/10022863/ IO Fitness Arrow (b.name().toUpper().startsWith("ICSE") && b.name().length() == 4) || (b.name().toUpper().startsWith("FLX") && b.name().length() == 10) || (b.name().toUpper().startsWith("CSRB") && b.name().length() == 11) || (b.name().toUpper().startsWith("DU30-")) || // BodyTone du30 + (b.name().toUpper().startsWith("BIKZU_")) || + (b.name().toUpper().startsWith("WLT8828")) || + (b.name().toUpper().startsWith("VANRYSEL-HT")) || + (b.name().toUpper().startsWith("HARISON-X15")) || + (b.name().toUpper().startsWith("FEIVON V2")) || + (b.name().toUpper().startsWith("FELVON V2")) || (b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || (b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1 (b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) || @@ -1484,7 +1557,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { b.name().toUpper().startsWith("KICKR ROLLR") || (b.name().toUpper().startsWith("HAMMER ") && saris_trainer) || (b.name().toUpper().startsWith("WAHOO KICKR"))) && - !wahooKickrSnapBike && filter) { + !wahooKickrSnapBike && !ftmsBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); wahooKickrSnapBike = @@ -1512,6 +1585,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { this->signalBluetoothDeviceConnected(horizonGr7Bike); } else if ((b.name().toUpper().startsWith(QStringLiteral("STAGES ")) || (b.name().toUpper().startsWith("TACX SATORI")) || + (b.name().toUpper().startsWith("RACER S")) || + (b.name().toUpper().startsWith("ELITETRAINER")) || ((b.name().toUpper().startsWith("KICKR CORE")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818))) || (b.name().toUpper().startsWith(QStringLiteral("QD")) && b.name().length() == 2) || (b.name().toUpper().startsWith(QStringLiteral("DFC")) && b.name().length() == 3) || @@ -1589,7 +1664,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { } else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) || b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) || b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) || - b.name().toUpper().startsWith(QLatin1String("STRIDE-")) || + b.name().toUpper().startsWith(QLatin1String("STRIDE")) || + b.name().toUpper().startsWith(QLatin1String("STRIDE6S-")) || b.name().toUpper().startsWith(QLatin1String("ECH-SD-SPT"))) && !echelonStride && filter) { this->setLastBluetoothDevice(b); @@ -1765,6 +1841,22 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // SLOT(inclinationChanged(double))); sportsTechBike->deviceDiscovered(b); this->signalBluetoothDeviceConnected(sportsTechBike); + } else if (b.name().toUpper().startsWith(QStringLiteral("EW-EP-")) && !sportsTechElliptical && !horizonTreadmill && filter) { + this->setLastBluetoothDevice(b); + this->stopDiscovery(); + sportsTechElliptical = new sportstechelliptical(noWriteResistance, noHeartService, bikeResistanceOffset, + bikeResistanceGain); + // stateFileRead(); + emit deviceConnected(b); + connect(sportsTechElliptical, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + // connect(echelonConnectSport, SIGNAL(disconnected()), this, SLOT(restart())); + connect(sportsTechElliptical, &sportstechelliptical::debug, this, &bluetooth::debug); + // connect(echelonConnectSport, SIGNAL(speedChanged(double)), this, SLOT(speedChanged(double))); + // connect(echelonConnectSport, SIGNAL(inclinationChanged(double)), this, + // SLOT(inclinationChanged(double))); + sportsTechElliptical->deviceDiscovered(b); + this->signalBluetoothDeviceConnected(sportsTechElliptical); } else if ((b.name().toUpper().startsWith(QStringLiteral("CARDIOFIT")) || (b.name().toUpper().contains(QStringLiteral("CARE")) && b.name().length() == 11)) // CARE9040177 - Carefitness CV-351 @@ -1832,7 +1924,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // SLOT(inclinationChanged(double))); proformTreadmill->deviceDiscovered(b); this->signalBluetoothDeviceConnected(proformTreadmill); - } else if (b.name().toUpper().startsWith(QStringLiteral("ESLINKER")) && !eslinkerTreadmill && filter) { + } else if ((b.name().toUpper().startsWith(QStringLiteral("ESANGLINKER")) || + b.name().toUpper().startsWith(QStringLiteral("ESLINKER"))) && !eslinkerTreadmill && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); eslinkerTreadmill = new eslinkertreadmill(this->pollDeviceTime, noConsole, noHeartService); @@ -2066,7 +2159,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { // connect(ultraSportBike, &solebike::debug, this, &bluetooth::debug); ultraSportBike->deviceDiscovered(b); this->signalBluetoothDeviceConnected(ultraSportBike); - } else if ((b.name().toUpper().startsWith(QStringLiteral("KEEP_BIKE_"))) && !keepBike && filter) { + } else if ((b.name().toUpper().startsWith(QStringLiteral("KEEP_BIKE_")) || + b.name().toUpper().startsWith(QStringLiteral("KEEP_CC_"))) && !keepBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); keepBike = new keepbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain); @@ -2089,7 +2183,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) { soleBike->deviceDiscovered(b); this->signalBluetoothDeviceConnected(soleBike); } else if ((b.name().toUpper().startsWith(QStringLiteral("BFCP")) || - (b.name().toUpper().startsWith(QStringLiteral("HT")) && b.name().length() == 11)) && + (b.name().toUpper().startsWith(QStringLiteral("HT")) && (b.name().length() == 11 || b.name().length() == 12))) && !skandikaWiriBike && filter) { this->setLastBluetoothDevice(b); this->stopDiscovery(); @@ -2332,7 +2426,8 @@ void bluetooth::connectedAndDiscovered() { connect(heartRateBelt, &heartratebelt::debug, this, &bluetooth::debug); connect(heartRateBelt, &heartratebelt::heartRate, this->device(), &bluetoothdevice::heartRate); heartRateBelt->deviceDiscovered(b); - + if(homeform::singleton()) + homeform::singleton()->setToastRequested(b.name() + " (HR sensor) connected!"); break; } } @@ -2419,6 +2514,8 @@ void bluetooth::connectedAndDiscovered() { connect(cadenceSensor, &bluetoothdevice::cadenceChanged, this->device(), &bluetoothdevice::cadenceSensor); cadenceSensor->deviceDiscovered(b); + if(homeform::singleton()) + homeform::singleton()->setToastRequested(b.name() + " (cadence sensor) connected!"); break; } } @@ -2464,6 +2561,10 @@ void bluetooth::connectedAndDiscovered() { &bluetoothdevice::verticalOscillationSensor); powerSensorRun->deviceDiscovered(b); } + + if(homeform::singleton()) + homeform::singleton()->setToastRequested(b.name() + " (power sensor) connected!"); + break; } } @@ -2531,6 +2632,8 @@ void bluetooth::connectedAndDiscovered() { connect(zwiftClickRemote->playDevice, &ZwiftPlayDevice::plus, (bike*)this->device(), &bike::gearUp); connect(zwiftClickRemote->playDevice, &ZwiftPlayDevice::minus, (bike*)this->device(), &bike::gearDown); zwiftClickRemote->deviceDiscovered(b); + if(homeform::singleton()) + homeform::singleton()->setToastRequested("Zwift Click Connected!"); break; } } @@ -2538,22 +2641,27 @@ void bluetooth::connectedAndDiscovered() { if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) { for (const QBluetoothDeviceInfo &b : qAsConst(devices)) { - if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE")) && zwiftPlayDevice.size() < 2 && this->device() && + if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE") || b.name().toUpper().startsWith("ZWIFT SF2")) && zwiftPlayDevice.size() < 2 && this->device() && this->device()->deviceType() == bluetoothdevice::BIKE) { if(b.manufacturerData(2378).size() > 0) { qDebug() << "this should be 3 or 2. is it? " << int(b.manufacturerData(2378).at(0)); + zwiftPlayDevice.append(new zwiftclickremote(this->device(), + int(b.manufacturerData(2378).at(0)) == 3 ? AbstractZapDevice::ZWIFT_PLAY_TYPE::LEFT : AbstractZapDevice::ZWIFT_PLAY_TYPE::RIGHT)); } else { qDebug() << "manufacturer not found for ZWIFT CLICK"; + zwiftPlayDevice.append(new zwiftclickremote(this->device(), + zwiftPlayDevice.length() == 0 ? AbstractZapDevice::ZWIFT_PLAY_TYPE::LEFT : AbstractZapDevice::ZWIFT_PLAY_TYPE::RIGHT)); + } - zwiftPlayDevice.append(new zwiftclickremote(this->device(), - int(b.manufacturerData(2378).at(0)) == 3 ? AbstractZapDevice::ZWIFT_PLAY_TYPE::LEFT : AbstractZapDevice::ZWIFT_PLAY_TYPE::RIGHT)); // connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart())); connect(zwiftPlayDevice.last(), &zwiftclickremote::debug, this, &bluetooth::debug); connect(zwiftPlayDevice.last()->playDevice, &ZwiftPlayDevice::plus, (bike*)this->device(), &bike::gearUp); connect(zwiftPlayDevice.last()->playDevice, &ZwiftPlayDevice::minus, (bike*)this->device(), &bike::gearDown); zwiftPlayDevice.last()->deviceDiscovered(b); + if(homeform::singleton()) + homeform::singleton()->setToastRequested("Zwift Play/Ride Connected!"); } } } @@ -2772,6 +2880,11 @@ void bluetooth::restart() { delete proformWifiBike; proformWifiBike = nullptr; } + if (antBike) { + + delete antBike; + antBike = nullptr; + } if (proformTelnetBike) { delete proformTelnetBike; @@ -2792,6 +2905,11 @@ void bluetooth::restart() { delete nordictrackifitadbBike; nordictrackifitadbBike = nullptr; } + if (nordictrackifitadbElliptical) { + + delete nordictrackifitadbElliptical; + nordictrackifitadbElliptical = nullptr; + } if (powerBike) { delete powerBike; @@ -3033,6 +3151,11 @@ void bluetooth::restart() { delete sportsTechBike; sportsTechBike = nullptr; } + if (sportsTechElliptical) { + + delete sportsTechElliptical; + sportsTechElliptical = nullptr; + } if (sportsPlusBike) { delete sportsPlusBike; @@ -3206,10 +3329,14 @@ bluetoothdevice *bluetooth::device() { return proformTelnetBike; } else if (proformWifiTreadmill) { return proformWifiTreadmill; + } else if (antBike) { + return antBike; } else if (nordictrackifitadbTreadmill) { return nordictrackifitadbTreadmill; } else if (nordictrackifitadbBike) { return nordictrackifitadbBike; + } else if (nordictrackifitadbElliptical) { + return nordictrackifitadbElliptical; } else if (powerBike) { return powerBike; } else if (powerTreadmill) { @@ -3330,6 +3457,8 @@ bluetoothdevice *bluetooth::device() { return schwinn170Bike; } else if (sportsTechBike) { return sportsTechBike; + } else if (sportsTechElliptical) { + return sportsTechElliptical; } else if (sportsPlusBike) { return sportsPlusBike; } else if (inspireBike) { diff --git a/src/devices/bluetooth.h b/src/devices/bluetooth.h index 68b3239b1..f5df33c07 100644 --- a/src/devices/bluetooth.h +++ b/src/devices/bluetooth.h @@ -21,6 +21,7 @@ #include "qzsettings.h" #include "devices/activiotreadmill/activiotreadmill.h" +#include "devices/antbike/antbike.h" #include "devices/apexbike/apexbike.h" #include "devices/bhfitnesselliptical/bhfitnesselliptical.h" #include "devices/bkoolbike/bkoolbike.h" @@ -35,6 +36,7 @@ #include "devices/concept2skierg/concept2skierg.h" #include "devices/crossrope/crossrope.h" #include "devices/cscbike/cscbike.h" +#include "devices/deeruntreadmill/deerruntreadmill.h" #include "devices/domyosbike/domyosbike.h" #include "devices/domyoselliptical/domyoselliptical.h" #include "devices/domyosrower/domyosrower.h" @@ -76,6 +78,7 @@ #include "devices/nautilustreadmill/nautilustreadmill.h" #include "devices/nordictrackelliptical/nordictrackelliptical.h" #include "devices/nordictrackifitadbbike/nordictrackifitadbbike.h" +#include "devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h" #include "devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h" #include "devices/npecablebike/npecablebike.h" #include "devices/octaneelliptical/octaneelliptical.h" @@ -108,6 +111,7 @@ #include "devices/spirittreadmill/spirittreadmill.h" #include "devices/sportsplusbike/sportsplusbike.h" #include "devices/sportstechbike/sportstechbike.h" +#include "devices/sportstechelliptical/sportstechelliptical.h" #include "devices/stagesbike/stagesbike.h" #include "devices/renphobike/renphobike.h" @@ -159,6 +163,7 @@ class bluetooth : public QObject, public SignalHandler { bool useDiscovery = false; QFile *debugCommsLog = nullptr; QBluetoothDeviceDiscoveryAgent *discoveryAgent = nullptr; + antbike *antBike = nullptr; apexbike *apexBike = nullptr; bkoolbike *bkoolBike = nullptr; bhfitnesselliptical *bhFitnessElliptical = nullptr; @@ -172,6 +177,7 @@ class bluetooth : public QObject, public SignalHandler { csaferower *csafeRower = nullptr; #endif concept2skierg *concept2Skierg = nullptr; + deerruntreadmill *deerrunTreadmill = nullptr; domyostreadmill *domyos = nullptr; domyosbike *domyosBike = nullptr; domyosrower *domyosRower = nullptr; @@ -193,6 +199,7 @@ class bluetooth : public QObject, public SignalHandler { nordictrackelliptical *nordictrackElliptical = nullptr; nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr; nordictrackifitadbbike *nordictrackifitadbBike = nullptr; + nordictrackifitadbelliptical *nordictrackifitadbElliptical = nullptr; octaneelliptical *octaneElliptical = nullptr; octanetreadmill *octaneTreadmill = nullptr; pelotonbike *pelotonBike = nullptr; @@ -213,6 +220,7 @@ class bluetooth : public QObject, public SignalHandler { horizongr7bike *horizonGr7Bike = nullptr; schwinnic4bike *schwinnIC4Bike = nullptr; sportstechbike *sportsTechBike = nullptr; + sportstechelliptical *sportsTechElliptical = nullptr; sportsplusbike *sportsPlusBike = nullptr; inspirebike *inspireBike = nullptr; snodebike *snodeBike = nullptr; @@ -299,6 +307,7 @@ class bluetooth : public QObject, public SignalHandler { bool eliteRizerAvaiable(); bool eliteSterzoSmartAvaiable(); bool fitmetriaFanfitAvaiable(); + bool zwiftDeviceAvaiable(); bool fitmetria_fanfit_isconnected(QString name); #ifdef Q_OS_WIN diff --git a/src/devices/bluetoothdevice.h b/src/devices/bluetoothdevice.h index ab4fd40cf..afba22a76 100644 --- a/src/devices/bluetoothdevice.h +++ b/src/devices/bluetoothdevice.h @@ -534,6 +534,9 @@ class bluetoothdevice : public QObject { int64_t lastStart = 0; int64_t lastStop = 0; + metric RequestedPower; + int16_t requestPower = -1; + /** * @brief m_difficult The current difficulty gain. Units: device dependent */ diff --git a/src/devices/deeruntreadmill/deerruntreadmill.cpp b/src/devices/deeruntreadmill/deerruntreadmill.cpp new file mode 100644 index 000000000..3ca9af850 --- /dev/null +++ b/src/devices/deeruntreadmill/deerruntreadmill.cpp @@ -0,0 +1,495 @@ +#include "deerruntreadmill.h" +#include "virtualdevices/virtualbike.h" + +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif +#include "virtualdevices/virtualtreadmill.h" +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +deerruntreadmill::deerruntreadmill(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, &deerruntreadmill::update); + refresh->start(pollDeviceTime); +} + +void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic characteristic, 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, &deerruntreadmill::packetReceived, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } else { + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } + + if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered || + m_control->state() == QLowEnergyController::UnconnectedState) { + emit debug(QStringLiteral("writeCharacteristic error because the connection is closed")); + + return; + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(characteristic, *writeBuffer); + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + + QStringLiteral(" // ") + info); + } + + loop.exec(); + + if (timeout.isActive() == false) { + emit debug(QStringLiteral(" exit for timeout")); + } +} + +uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) { + uint8_t result = 0; + + if (size < 7) { + qDebug() << QStringLiteral("array too small"); + return 0; + } + + for (size_t i = 5; i <= size - 3; i++) { + result ^= arr[i]; + } + + return result; +} + + +void deerruntreadmill::forceSpeed(double requestSpeed) { + QSettings settings; + uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43}; + + writeSpeed[2] = pollCounter; + writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF; + writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF; + writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed)); + + writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed), + QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false); +} + +void deerruntreadmill::forceIncline(double requestIncline) { + +} + +void deerruntreadmill::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} + +void deerruntreadmill::update() { + if (m_control->state() == QLowEnergyController::UnconnectedState) { + emit disconnected(); + return; + } + + if (initRequest) { + + initRequest = false; + btinit((lastSpeed > 0 ? true : false)); + } else if (/*bluetoothDevice.isValid() &&*/ + m_control->state() == QLowEnergyController::DiscoveredState && gattCommunicationChannelService && + gattWriteCharacteristic.isValid() && gattNotifyCharacteristic.isValid() && initDone) { + + QSettings settings; + // ******************************************* virtual treadmill init ************************************* + if (!firstInit && !this->hasVirtualDevice()) { + bool virtual_device_enabled = + settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool(); + bool virtual_device_force_bike = + settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike) + .toBool(); + if (virtual_device_enabled) { + if (!virtual_device_force_bike) { + debug("creating virtual treadmill interface..."); + auto virtualTreadMill = new virtualtreadmill(this, noHeartService); + connect(virtualTreadMill, &virtualtreadmill::debug, this, &deerruntreadmill::debug); + connect(virtualTreadMill, &virtualtreadmill::changeInclination, this, + &deerruntreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY); + } else { + debug("creating virtual bike interface..."); + auto virtualBike = new virtualbike(this); + connect(virtualBike, &virtualbike::changeInclination, this, + &deerruntreadmill::changeInclinationRequested); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE); + } + firstInit = 1; + } + } + // ******************************************************************************************************** + + // debug("Domyos Treadmill RSSI " + QString::number(bluetoothDevice.rssi())); + + update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())); + + { + if (requestSpeed != -1) { + if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { + emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed)); + forceSpeed(requestSpeed); + } + requestSpeed = -1; + } else 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; + } else if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + if (lastSpeed == 0.0) { + + lastSpeed = 0.5; + } + + // should be: + // 0x49 = inited + // 0x8a = tape stopped after a pause + /*if (lastState == 0x49)*/ { + uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43}; + initData2[2] = pollCounter; + + writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"), + false, true); + } /*else { + uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07}; + + writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false, + true); + }*/ + + requestStart = -1; + emit tapeStarted(); + } else if (requestStop != -1) { + emit debug(QStringLiteral("stopping... ") + paused); + /*if (lastState == PAUSED) { + uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07}; + + writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false, + true); + + } else*/ { + uint8_t stop[] = {0x4d, 0x00, 0x48, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x50, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd6, 0x43}; + stop[2] = pollCounter; + + writeCharacteristic(gattWriteCharacteristic, stop, sizeof(stop), QStringLiteral("stop"), false, + true); + } + + requestStop = -1; + } else { + uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43}; + poll[2] = pollCounter; + + writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false, + true); + } + + pollCounter++; + /*if (requestFanSpeed != -1) { + emit debug(QStringLiteral("changing fan speed...")); + + sendChangeFanSpeed(requestFanSpeed); + requestFanSpeed = -1; + } + if (requestIncreaseFan != -1) { + emit debug(QStringLiteral("increasing fan speed...")); + + sendChangeFanSpeed(FanSpeed + 1); + requestIncreaseFan = -1; + } else if (requestDecreaseFan != -1) { + emit debug(QStringLiteral("decreasing fan speed...")); + + sendChangeFanSpeed(FanSpeed - 1); + requestDecreaseFan = -1; + }*/ + } + } +} + +void deerruntreadmill::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void deerruntreadmill::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); + QByteArray value = newValue; + QDateTime now = QDateTime::currentDateTime(); + + emit debug(QStringLiteral(" << ") + QString::number(value.length()) + QStringLiteral(" ") + value.toHex(' ')); + emit packetReceived(); + + if (newValue.length() < 51) + return; + + lastPacket = value; + // lastState = value.at(0); + + double speed = ((double)(((value[9] << 8) & 0xff) + value[10]) / 100.0); + double incline = 0.0; + +#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 = 0; + if (heart == 0) { + 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) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo( + now)))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 + + Distance += ((speed / (double)3600.0) / + ((double)1000.0 / (double)(lastTimeCharacteristicChanged.msecsTo(now)))); + lastTimeCharacteristicChanged = now; + } + + emit debug(QStringLiteral("Current speed: ") + QString::number(speed)); + emit debug(QStringLiteral("Current incline: ") + QString::number(incline)); + emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value())); + // emit debug(QStringLiteral("Current KCal: ") + QString::number(kcal)); + // emit debug(QStringLiteral("Current Distance: ") + QString::number(distance)); + emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value())); + + if (m_control->error() != QLowEnergyController::NoError) { + qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); + } + + if (Speed.value() != speed) { + + emit speedChanged(speed); + } + Speed = speed; + if (Inclination.value() != incline) { + + emit inclinationChanged(0, incline); + } + Inclination = incline; + + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + + if (speed > 0) { + + lastSpeed = speed; + lastInclination = incline; + } + + firstCharacteristicChanged = false; +} + +void deerruntreadmill::btinit(bool startTape) { + initDone = true; +} + +double deerruntreadmill::minStepInclination() { return 1.0; } + +void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) { + + QBluetoothUuid _gattWriteCharacteristicId((quint16)0xfff1); + QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2); + + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); + if (state == QLowEnergyService::ServiceDiscovered) { + + // qDebug() << gattCommunicationChannelService->characteristics(); + auto characteristics_list = gattCommunicationChannelService->characteristics(); + for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) { + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle() + << c.properties(); + } + + gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId); + gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId); + Q_ASSERT(gattWriteCharacteristic.isValid()); + Q_ASSERT(gattNotifyCharacteristic.isValid()); + + // establish hook into notifications + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this, + &deerruntreadmill::characteristicChanged); + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this, + &deerruntreadmill::characteristicWritten); + connect(gattCommunicationChannelService, + static_cast(&QLowEnergyService::error), + this, &deerruntreadmill::errorService); + connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this, + &deerruntreadmill::descriptorWritten); + + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + gattCommunicationChannelService->writeDescriptor( + gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } +} + +void deerruntreadmill::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' ')); + + initRequest = true; + emit connectedAndDiscovered(); +} + +void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &characteristic, + const QByteArray &newValue) { + Q_UNUSED(characteristic); + emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' ')); +} + +void deerruntreadmill::serviceScanDone(void) { + QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0); + emit debug(QStringLiteral("serviceScanDone")); + + auto services_list = m_control->services(); + emit debug("Services found:"); + for (const QBluetoothUuid &s : qAsConst(services_list)) { + emit debug(s.toString()); + } + + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); + if (gattCommunicationChannelService) { + connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, + &deerruntreadmill::stateChanged); + gattCommunicationChannelService->discoverDetails(); + } else { + emit debug(QStringLiteral("error on find Service")); + } +} + +void deerruntreadmill::errorService(QLowEnergyService::ServiceError err) { + + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("deerruntreadmill::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void deerruntreadmill::error(QLowEnergyController::Error err) { + + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("deerruntreadmill::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void deerruntreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { + { + + bluetoothDevice = device; + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); + connect(m_control, &QLowEnergyController::serviceDiscovered, this, &deerruntreadmill::serviceDiscovered); + connect(m_control, &QLowEnergyController::discoveryFinished, this, &deerruntreadmill::serviceScanDone); + connect(m_control, + static_cast(&QLowEnergyController::error), + this, &deerruntreadmill::error); + connect(m_control, &QLowEnergyController::stateChanged, this, &deerruntreadmill::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.")); + searchStopped = false; + 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")); + searchStopped = false; + emit disconnected(); + }); + + // Connect + m_control->connectToDevice(); + return; + } +} + +void deerruntreadmill::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 deerruntreadmill::connected() { + if (!m_control) { + + return false; + } + return m_control->state() == QLowEnergyController::DiscoveredState; +} + +void deerruntreadmill::searchingStop() { searchStopped = true; } diff --git a/src/devices/deeruntreadmill/deerruntreadmill.h b/src/devices/deeruntreadmill/deerruntreadmill.h new file mode 100644 index 000000000..6789b7383 --- /dev/null +++ b/src/devices/deeruntreadmill/deerruntreadmill.h @@ -0,0 +1,103 @@ +#ifndef DEERRUNTREADMILL_H +#define DEERRUNTREADMILL_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 "devices/treadmill.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class deerruntreadmill : public treadmill { + + Q_OBJECT + public: + deerruntreadmill(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false, + double forceInitSpeed = 0.0, double forceInitInclination = 0.0); + bool connected() override; + double minStepInclination() override; + + private: + void forceSpeed(double requestSpeed); + void forceIncline(double requestIncline); + void btinit(bool startTape); + void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len, + const QString &info, bool disable_log = false, bool wait_for_response = false); + void startDiscover(); + uint8_t calculateXOR(uint8_t arr[], size_t size); + bool noConsole = false; + bool noHeartService = false; + uint32_t pollDeviceTime = 200; + uint8_t pollCounter = 0; + bool searchStopped = false; + uint8_t sec1Update = 0; + uint8_t firstInit = 0; + QByteArray lastPacket; + QDateTime lastTimeCharacteristicChanged; + bool firstCharacteristicChanged = true; + + QTimer *refresh; + + QLowEnergyService *gattCommunicationChannelService = nullptr; + QLowEnergyCharacteristic gattWriteCharacteristic; + QLowEnergyCharacteristic gattNotifyCharacteristic; + + bool initDone = false; + bool initRequest = false; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + Q_SIGNALS: + void disconnected(); + void debug(QString string); + void speedChanged(double speed); + void packetReceived(); + + public slots: + void deviceDiscovered(const QBluetoothDeviceInfo &device); + void searchingStop(); + + private slots: + + void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue); + void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue); + void stateChanged(QLowEnergyService::ServiceState state); + void controllerStateChanged(QLowEnergyController::ControllerState state); + void changeInclinationRequested(double grade, double percentage); + + void serviceDiscovered(const QBluetoothUuid &gatt); + void serviceScanDone(void); + void update(); + void error(QLowEnergyController::Error err); + void errorService(QLowEnergyService::ServiceError); +}; + +#endif // DEERRUNTREADMILL_H diff --git a/src/devices/dircon/dirconmanager.cpp b/src/devices/dircon/dirconmanager.cpp index 1b3d0babb..85270c5e2 100644 --- a/src/devices/dircon/dirconmanager.cpp +++ b/src/devices/dircon/dirconmanager.cpp @@ -106,11 +106,12 @@ enum { DM_SERV_OP(DM_SERV_ENUMI_OP, 0, 0, 0) DM_SERV_I_NUM }; } \ } \ if (P2.size()) { \ + QString dircon_id = QString("%1").arg(settings.value(QZSettings::dircon_id, \ + QZSettings::default_dircon_id).toInt(), 4, 10, QChar('0')); \ DirconProcessor *processor = new DirconProcessor( \ P2, \ QString(QStringLiteral(NAME)) \ - .replace(QStringLiteral("$uuid_hex$"), \ - QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC, 4, 10, QLatin1Char('0'))), \ + .replace(QStringLiteral("$uuid_hex$"), dircon_id), \ server_base_port + DM_MACHINE_##DESC, QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \ this); \ QString servdesc; \ @@ -149,7 +150,7 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset, QSettings settings; DirconProcessorService *service; QList services, proc_services; - bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType(); + bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType(); uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL : DM_MACHINE_TYPE_BIKE; qDebug() << "Building Dircom Manager"; diff --git a/src/devices/domyosbike/domyosbike.cpp b/src/devices/domyosbike/domyosbike.cpp index 40c97c8ce..297349ef9 100644 --- a/src/devices/domyosbike/domyosbike.cpp +++ b/src/devices/domyosbike/domyosbike.cpp @@ -685,7 +685,41 @@ 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) + if (settings.value(QZSettings::domyos_bike_500_profile_v2, QZSettings::default_domyos_bike_500_profile_v2).toBool()) { + switch ((int)resistance) { + case 1: + return (5.0 * Cadence.value()) / 9.5488; + case 2: + return (5.7 * Cadence.value()) / 9.5488; + case 3: + return (6.5 * Cadence.value()) / 9.5488; + case 4: + return (7.5 * Cadence.value()) / 9.5488; + case 5: + return (8.6 * Cadence.value()) / 9.5488; + case 6: + return (9.9 * Cadence.value()) / 9.5488; + case 7: + return (11.4 * Cadence.value()) / 9.5488; + 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 0; + } else 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())))); diff --git a/src/devices/echelonconnectsport/echelonconnectsport.cpp b/src/devices/echelonconnectsport/echelonconnectsport.cpp index 6ad1515f1..fe60bb503 100644 --- a/src/devices/echelonconnectsport/echelonconnectsport.cpp +++ b/src/devices/echelonconnectsport/echelonconnectsport.cpp @@ -1,4 +1,5 @@ #include "echelonconnectsport.h" +#include "homeform.h" #ifdef Q_OS_ANDROID #include "keepawakehelper.h" #endif @@ -469,7 +470,13 @@ void echelonconnectsport::serviceScanDone(void) { gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &echelonconnectsport::stateChanged); - gattCommunicationChannelService->discoverDetails(); + if(gattCommunicationChannelService != nullptr) { + gattCommunicationChannelService->discoverDetails(); + } else { + if(homeform::singleton()) + homeform::singleton()->setToastRequested("Bluetooth Service Error! Restart the bike!"); + m_control->disconnectFromDevice(); + } } void echelonconnectsport::errorService(QLowEnergyService::ServiceError err) { diff --git a/src/devices/echelonstride/echelonstride.cpp b/src/devices/echelonstride/echelonstride.cpp index 356aeaf0e..05978c66a 100644 --- a/src/devices/echelonstride/echelonstride.cpp +++ b/src/devices/echelonstride/echelonstride.cpp @@ -196,12 +196,22 @@ void echelonstride::update() { uint8_t initData3[] = {0xf0, 0xb0, 0x01, 0x01, 0xa2}; writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("start"), false, true); + if(stride4) { + uint8_t initData0[] = {0xf0, 0xa5, 0x00, 0x95}; + writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("start"), false, false); + } + uint8_t initData4[] = {0xf0, 0xd0, 0x01, 0x00, 0xc1}; writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("start"), false, false); uint8_t initData5[] = {0xf0, 0xd0, 0x01, 0x11, 0xd2}; writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("start"), false, false); + if(stride4) { + uint8_t initData0[] = {0xf0, 0xd3, 0x02, 0x01, 0xf4, 0xba}; + writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("start"), false, false); + } + lastStart = QDateTime::currentMSecsSinceEpoch(); requestStart = -1; emit tapeStarted(); @@ -285,6 +295,32 @@ void echelonstride::characteristicChanged(const QLowEnergyCharacteristic &charac } 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); return; + } else if (((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd1 && stride4) { + + 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); + qDebug() << "speed1" << newValue.at(7); + uint16_t convertedData = (uint8_t)newValue.at(7); + qDebug() << "speed2" << convertedData; + convertedData = convertedData << 8; + qDebug() << "speed3" << convertedData; + convertedData = convertedData & 0xFF00; + qDebug() << "speed4" << convertedData; + convertedData = convertedData + (uint8_t)newValue.at(8); + qDebug() << "speed5" << convertedData; + Speed = (((double)convertedData) / 100.0) * miles; + + if (Speed.value() > 0) + lastStart = 0; + else + lastStop = 0; + + qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value()); + return; } /*if (newValue.length() != 21) @@ -344,7 +380,17 @@ void echelonstride::btinit() { uint8_t initData1[] = {0xf0, 0xa1, 0x00, 0x91}; uint8_t initData2[] = {0xf0, 0xa3, 0x00, 0x93}; - writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true); + // stride4 + uint8_t initDataStride4_0[] = {0xf0, 0xe0, 0xfd, 0x3e, 0x65, 0x48, 0xd5, 0x8d}; + + if(stride4) { + writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true); // send a frame to wait the Value: f0e0728518586198 + writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, false); + writeCharacteristic(initDataStride4_0, sizeof(initDataStride4_0), QStringLiteral("init"), false, false); + writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, false); + } else { + writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true); + } writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); @@ -352,7 +398,9 @@ 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); + + if(!stride4) + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); initDone = true; } @@ -432,6 +480,12 @@ void echelonstride::error(QLowEnergyController::Error err) { void echelonstride::deviceDiscovered(const QBluetoothDeviceInfo &device) { { bluetoothDevice = device; + + if(bluetoothDevice.name().toUpper().startsWith("STRIDE4")) { + stride4 = true; + qDebug() << "STRIDE4 workaround enabled!"; + } + m_control = QLowEnergyController::createCentral(bluetoothDevice, this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &echelonstride::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &echelonstride::serviceScanDone); diff --git a/src/devices/echelonstride/echelonstride.h b/src/devices/echelonstride/echelonstride.h index ea2c4c6f4..c7906450a 100644 --- a/src/devices/echelonstride/echelonstride.h +++ b/src/devices/echelonstride/echelonstride.h @@ -78,6 +78,8 @@ class echelonstride : public treadmill { bool initDone = false; bool initRequest = false; + bool stride4 = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/devices/elliptical.cpp b/src/devices/elliptical.cpp index 05299df7c..32aaff779 100644 --- a/src/devices/elliptical.cpp +++ b/src/devices/elliptical.cpp @@ -38,6 +38,40 @@ void elliptical::update_metrics(bool watt_calc, const double watts) { _firstUpdate = false; } +resistance_t elliptical::resistanceFromPowerRequest(uint16_t power) { return power / 10; } // in order to have something + +void elliptical::changePower(int32_t 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 deltaDown = wattsMetric().value() - ((double)power); + double deltaUp = ((double)power) - wattsMetric().value(); + qDebug() << QStringLiteral("filter ") + QString::number(deltaUp) + " " + QString::number(deltaDown) + " " + + QString::number(erg_filter_upper) + " " + QString::number(erg_filter_lower); + if (/*!ergModeSupported &&*/ force_resistance /*&& erg_mode*/ && + (deltaUp > erg_filter_upper || deltaDown > erg_filter_lower)) { + resistance_t r = (resistance_t)resistanceFromPowerRequest(power); + changeResistance(r); // resistance start from 1 + } +} + + uint16_t elliptical::watts() { QSettings settings; diff --git a/src/devices/elliptical.h b/src/devices/elliptical.h index e65dd5723..a90f5bdd0 100644 --- a/src/devices/elliptical.h +++ b/src/devices/elliptical.h @@ -33,12 +33,14 @@ class elliptical : public bluetoothdevice { void setGears(double d); double gears(); virtual double minStepInclination() { return 0.5; } + virtual resistance_t resistanceFromPowerRequest(uint16_t power); public Q_SLOTS: virtual void changeSpeed(double speed); void changeResistance(resistance_t res) override; void changeInclination(double grade, double inclination) override; virtual void changeCadence(int16_t cad); + void changePower(int32_t power) override; virtual void changeRequestedPelotonResistance(int8_t resistance); signals: diff --git a/src/devices/eslinkertreadmill/eslinkertreadmill.cpp b/src/devices/eslinkertreadmill/eslinkertreadmill.cpp index 82ac13fe3..1ac75fb44 100644 --- a/src/devices/eslinkertreadmill/eslinkertreadmill.cpp +++ b/src/devices/eslinkertreadmill/eslinkertreadmill.cpp @@ -1,4 +1,5 @@ #include "eslinkertreadmill.h" +#include "homeform.h" #include "keepawakehelper.h" #include "virtualdevices/virtualtreadmill.h" #include @@ -71,7 +72,8 @@ void eslinkertreadmill::updateDisplay(uint16_t elapsed) { writeCharacteristic(display, sizeof(display), QStringLiteral("updateDisplay elapsed=") + QString::number(elapsed), false, false); - } else { + } else if (treadmill_type == ESANGLINKER){ + } } @@ -116,6 +118,16 @@ void eslinkertreadmill::forceSpeed(double requestSpeed) { writeCharacteristic(display, sizeof(display), QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, true); + } else if(treadmill_type == ESANGLINKER) { + uint8_t display[] = {0xa9, 0x01, 0x01, 0x0b, 0xa2}; + display[3] = (int)(requestSpeed * 10 * 0.621371); + for (int i = 0; i < 4; i++) { + display[4] = display[4] ^ display[i]; + } + + writeCharacteristic(display, sizeof(display), + QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, true); + } } @@ -159,7 +171,7 @@ void eslinkertreadmill::update() { } if (treadmill_type == TYPE::RHYTHM_FUN || treadmill_type == TYPE::YPOO_MINI_CHANGE || - treadmill_type == TYPE::COSTAWAY) { + treadmill_type == TYPE::COSTAWAY || treadmill_type == TYPE::ESANGLINKER) { if (requestSpeed != -1) { if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) { @@ -255,6 +267,14 @@ void eslinkertreadmill::update() { void eslinkertreadmill::serviceDiscovered(const QBluetoothUuid &gatt) { emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); + + if(gatt == QBluetoothUuid((quint16)0x1826)) { + QSettings settings; + settings.setValue(QZSettings::ftms_treadmill, bluetoothDevice.name()); + qDebug() << "forcing FTMS treadmill since it has FTMS"; + if(homeform::singleton()) + homeform::singleton()->setToastRequested("FTMS treadmill found, restart the app to apply the change"); + } } void eslinkertreadmill::characteristicChanged(const QLowEnergyCharacteristic &characteristic, @@ -349,7 +369,7 @@ 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) + else if (newValue.length() != 5 && (treadmill_type == COSTAWAY || treadmill_type == TYPE::ESANGLINKER)) return; if (treadmill_type == RHYTHM_FUN || treadmill_type == YPOO_MINI_CHANGE) { @@ -388,9 +408,12 @@ void eslinkertreadmill::characteristicChanged(const QLowEnergyCharacteristic &ch lastSpeed = speed; lastInclination = incline; } - } else if (treadmill_type == COSTAWAY) { + } else if (treadmill_type == COSTAWAY || treadmill_type == TYPE::ESANGLINKER) { + if(newValue.at(1) != 0x09 && treadmill_type == TYPE::ESANGLINKER) + return; + const double miles = 1.60934; - if(((uint8_t)newValue.at(3)) == 0xFF) + if(((uint8_t)newValue.at(3)) == 0xFF && treadmill_type == COSTAWAY) Speed = 0; else Speed = (double)((uint8_t)newValue.at(3)) / 10.0 * miles; @@ -465,7 +488,58 @@ double eslinkertreadmill::GetInclinationFromPacket(const QByteArray &packet) { void eslinkertreadmill::btinit(bool startTape) { Q_UNUSED(startTape) - if (treadmill_type == COSTAWAY) { + if (treadmill_type == ESANGLINKER) { + uint8_t initData1[] = {0xa9, 0xf2, 0x01, 0x2f, 0x75}; + writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); + + uint8_t initData2[] = {0xa9, 0x0a, 0x01, 0xc6, 0x64}; + writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true); + + uint8_t initData3[] = {0xa9, 0xae, 0x01, 0xfe, 0xf8}; + writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true); + + uint8_t initData4[] = {0xa9, 0xa0, 0x03, 0x00, 0x00, 0x00, 0x0a}; + writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, true); + + uint8_t initData5[] = {0xa9, 0x08, 0x01, 0xad, 0x0d}; + writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, true); + + uint8_t initData6[] = {0xa9, 0x08, 0x04, 0x0c, 0x06, 0x48, 0x12, 0xf5}; + writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, true); + + uint8_t initData7[] = {0xa9, 0x1e, 0x01, 0xfe, 0x48}; + writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, true); + + uint8_t initData8[] = {0xa9, 0xae, 0x01, 0xfe, 0xf8}; + writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, true); + + uint8_t initData9[] = {0xa9, 0xa3, 0x01, 0x01, 0x0a}; + writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, true); + + uint8_t initData10[] = {0xa9, 0x8e, 0x01, 0x09, 0x2f}; + writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, true); + + uint8_t initData11[] = {0xa9, 0xb2, 0x01, 0xfe, 0xe4}; + writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, true); + + uint8_t initData12[] = {0xa9, 0x8e, 0x01, 0x09, 0x2f}; + writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, true); + + uint8_t initData13[] = {0xa9, 0xae, 0x01, 0xfe, 0xf8}; + writeCharacteristic(initData13, sizeof(initData13), QStringLiteral("init"), false, true); + + uint8_t initData14[] = {0xa9, 0x08, 0x01, 0x76, 0xd6}; + writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init"), false, true); + + uint8_t initData15[] = {0xa9, 0x08, 0x04, 0x0a, 0x04, 0x28, 0x0e, 0x8d}; + writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init"), false, true); + + uint8_t initData16[] = {0xa9, 0x08, 0x01, 0x72, 0xd2}; + writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init"), false, true); + + uint8_t initData17[] = {0xa9, 0x08, 0x04, 0x70, 0x16, 0x0e, 0x08, 0xc5}; + writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, true); + } else if (treadmill_type == COSTAWAY) { uint8_t initData1[] = {0xa9, 0xf2, 0x01, 0x2f, 0x75}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true); @@ -633,7 +707,10 @@ void eslinkertreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { 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) { + if(device.name().toUpper().startsWith(QStringLiteral("ESANGLINKER"))) { + treadmill_type = ESANGLINKER; + qDebug() << "ESANGLINKER workaround ENABLED!"; + } else if (eslinker_cadenza) { treadmill_type = CADENZA_FITNESS_T45; } else if (eslinker_ypoo) { treadmill_type = YPOO_MINI_CHANGE; diff --git a/src/devices/eslinkertreadmill/eslinkertreadmill.h b/src/devices/eslinkertreadmill/eslinkertreadmill.h index dfcd7f235..edf1b4b06 100644 --- a/src/devices/eslinkertreadmill/eslinkertreadmill.h +++ b/src/devices/eslinkertreadmill/eslinkertreadmill.h @@ -66,6 +66,7 @@ class eslinkertreadmill : public treadmill { 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 COSTAWAY = 3, + ESANGLINKER = 4, } TYPE; volatile TYPE treadmill_type = RHYTHM_FUN; diff --git a/src/devices/fitshowtreadmill/fitshowtreadmill.cpp b/src/devices/fitshowtreadmill/fitshowtreadmill.cpp index 12424bf3e..bbc534f21 100644 --- a/src/devices/fitshowtreadmill/fitshowtreadmill.cpp +++ b/src/devices/fitshowtreadmill/fitshowtreadmill.cpp @@ -299,11 +299,12 @@ void fitshowtreadmill::serviceDiscovered(const QBluetoothUuid &gatt) { qDebug() << "adding" << gatt.toString() << "as the default service"; serviceId = gatt; // NOTE: clazy-rule-of-tow } - if(gatt == QBluetoothUuid((quint16)0x1826)) { + if(gatt == QBluetoothUuid((quint16)0x1826) && !fs_connected) { QSettings settings; settings.setValue(QZSettings::ftms_treadmill, bluetoothDevice.name()); qDebug() << "forcing FTMS treadmill since it has FTMS"; - homeform::singleton()->setToastRequested("FTMS treadmill found, restart the app to apply the change"); + if(homeform::singleton()) + homeform::singleton()->setToastRequested("FTMS treadmill found, restart the app to apply the change"); } } @@ -837,10 +838,10 @@ void fitshowtreadmill::error(QLowEnergyController::Error err) { void fitshowtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") + device.address().toString() + ')'); - /*if (device.name().startsWith(QStringLiteral("FS-")) || - (device.name().startsWith(QStringLiteral("SW")) && device.name().length() == 14))*/ - - if (device.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT"))) { + if (device.name().toUpper().startsWith(QStringLiteral("FS-"))) { + qDebug() << "FS FIX!"; + fs_connected = true; + } else if (device.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT"))) { qDebug() << "NOBLEPRO FIX!"; minStepInclinationValue = 0.5; noblepro_connected = true; diff --git a/src/devices/fitshowtreadmill/fitshowtreadmill.h b/src/devices/fitshowtreadmill/fitshowtreadmill.h index cba98b7b6..6aa341e3b 100644 --- a/src/devices/fitshowtreadmill/fitshowtreadmill.h +++ b/src/devices/fitshowtreadmill/fitshowtreadmill.h @@ -152,6 +152,7 @@ class fitshowtreadmill : public treadmill { double minStepInclinationValue = 1.0; bool noblepro_connected = false; + bool fs_connected = false; metric rawInclination; diff --git a/src/devices/ftmsbike/ftmsbike.cpp b/src/devices/ftmsbike/ftmsbike.cpp index 0bf1c0c9f..5d22ff74e 100644 --- a/src/devices/ftmsbike/ftmsbike.cpp +++ b/src/devices/ftmsbike/ftmsbike.cpp @@ -24,6 +24,7 @@ using namespace std::chrono_literals; ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset, double bikeResistanceGain) { + QSettings settings; m_watt.setType(metric::METRIC_WATT); Speed.setType(metric::METRIC_SPEED); refresh = new QTimer(this); @@ -33,7 +34,44 @@ ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResis this->bikeResistanceOffset = bikeResistanceOffset; initDone = false; connect(refresh, &QTimer::timeout, this, &ftmsbike::update); - refresh->start(200ms); + refresh->start(settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt()); +} + +void ftmsbike::writeCharacteristicZwiftPlay(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response) { + QEventLoop loop; + QTimer timeout; + + if(!zwiftPlayService) { + qDebug() << QStringLiteral("zwiftPlayService is null!"); + return; + } + + if (wait_for_response) { + connect(zwiftPlayService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } else { + connect(zwiftPlayService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + if (zwiftPlayWriteChar.properties() & QLowEnergyCharacteristic::WriteNoResponse) { + zwiftPlayService->writeCharacteristic(zwiftPlayWriteChar, *writeBuffer, + QLowEnergyService::WriteWithoutResponse); + } else { + zwiftPlayService->writeCharacteristic(zwiftPlayWriteChar, *writeBuffer); + } + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + QStringLiteral(" // ") + info); + } + + loop.exec(); } void ftmsbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, @@ -86,6 +124,48 @@ void ftmsbike::init() { initRequest = false; } +void ftmsbike::zwiftPlayInit() { + QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + + if(zwiftPlayService && gears_zwift_ratio) { + uint8_t rideOn[] = {0x52, 0x69, 0x64, 0x65, 0x4f, 0x6e, 0x02, 0x01}; + writeCharacteristicZwiftPlay(rideOn, sizeof(rideOn), "rideOn", false, true); + + uint8_t init1[] = {0x41, 0x08, 0x05}; + writeCharacteristicZwiftPlay(init1, sizeof(init1), "init1", false, true); + + uint8_t init2[] = {0x04, 0x2a, 0x04, 0x10, 0xc0, 0xbb, 0x01}; + writeCharacteristicZwiftPlay(init2, sizeof(init2), "init2", false, true); + + uint8_t init3[] = {0x00, 0x08, 0x00}; + writeCharacteristicZwiftPlay(init3, sizeof(init3), "init3", false, true); + + writeCharacteristicZwiftPlay(init1, sizeof(init1), "init1", false, true); + + uint8_t init4[] = {0x00, 0x08, 0x88, 0x04}; + writeCharacteristicZwiftPlay(init4, sizeof(init4), "init4", false, true); + + uint8_t init5[] = {0x04, 0x2a, 0x0a, 0x10, 0xc0, 0xbb, 0x01, 0x20, 0xbf, 0x06, 0x28, 0xb4, 0x42}; + writeCharacteristicZwiftPlay(init5, sizeof(init5), "init5", false, true); + + uint8_t init6[] = {0x04, 0x22, 0x0b, 0x08, 0x00, 0x10, 0xda, 0x02, 0x18, 0xec, 0x27, 0x20, 0x90, 0x03}; + writeCharacteristicZwiftPlay(init6, sizeof(init6), "init6", false, true); + + writeCharacteristicZwiftPlay(init2, sizeof(init2), "init2", false, true); + writeCharacteristicZwiftPlay(init4, sizeof(init4), "init4", false, true); + + uint8_t init7[] = {0x04, 0x22, 0x03, 0x10, 0xa9, 0x01}; + writeCharacteristicZwiftPlay(init7, sizeof(init7), "init7", false, true); + + writeCharacteristicZwiftPlay(init2, sizeof(init2), "init2", false, true); + writeCharacteristicZwiftPlay(init4, sizeof(init4), "init4", false, true); + + uint8_t init8[] = {0x04, 0x22, 0x02, 0x10, 0x00}; + writeCharacteristicZwiftPlay(init8, sizeof(init8), "init8", false, true); + } +} + void ftmsbike::forcePower(int16_t requestPower) { uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00}; @@ -156,6 +236,7 @@ void ftmsbike::update() { } if (initRequest) { + zwiftPlayInit(); initRequest = false; } else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState //&& @@ -181,7 +262,7 @@ void ftmsbike::update() { auto virtualBike = this->VirtualBike(); - if (requestResistance != -1) { + if (requestResistance != -1 || lastGearValue != gears()) { if (requestResistance > 100) { requestResistance = 100; } // TODO, use the bluetooth value @@ -189,23 +270,125 @@ void ftmsbike::update() { requestResistance = 1; } - if (requestResistance != currentResistance().value()) { + if (requestResistance != currentResistance().value() || lastGearValue != gears()) { 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 if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && (requestPower == 0 || requestPower == -1)) { init(); - forceResistance(requestResistance); + forceResistance(requestResistance + (gears() * 5)); } } requestResistance = -1; } if((virtualBike && virtualBike->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100 && lastPacketFromFTMS.length() >= 7) { - qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP"; + qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' '); ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS); } + QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + if(zwiftPlayService && gears_zwift_ratio && lastGearValue != gears()) { + uint8_t gear1[] = {0x04, 0x2a, 0x03, 0x10, 0xdc, 0xec}; + uint8_t gear2[] = {0x04, 0x2a, 0x04, 0x10, 0xdc, 0xec, 0x01}; + uint32_t gear_value = 0; + + switch((int)gears()) { + case 1: + gear_value = 0x3acc; + break; + case 2: + gear_value = 0x43fc; + break; + case 3: + gear_value = 0x4dac; + break; + case 4: + gear_value = 0x56d5; + break; + case 5: + gear_value = 0x608c; + break; + case 6: + gear_value = 0x6be8; + break; + case 7: + gear_value = 0x77c4; + break; + case 8: + gear_value = 0x183a0; + break; + case 9: + gear_value = 0x191a8; + break; + case 10: + gear_value = 0x19fb0; + break; + case 11: + gear_value = 0x1adb8; + break; + case 12: + gear_value = 0x1bbc0; + break; + case 13: + gear_value = 0x1cbf3; + break; + case 14: + gear_value = 0x1dca8; + break; + case 15: + gear_value = 0x1ecdc; + break; + case 16: + gear_value = 0x1fd90; + break; + case 17: + gear_value = 0x290d4; + break; + case 18: + gear_value = 0x2a498; + break; + case 19: + gear_value = 0x2b7dc; + break; + case 20: + gear_value = 0x2cb9f; + break; + case 21: + gear_value = 0x2e2d8; + break; + case 22: + gear_value = 0x2fa90; + break; + case 23: + gear_value = 0x391c8; + break; + case 24: + gear_value = 0x3acf3; + break; + default: + // Gestione del caso di default + break; + } + + gear_value = gear_value * settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble(); + + if(gear_value < 0x10000) { + gear1[4] = gear_value & 0xFF; + gear1[5] = ((gear_value & 0xFF00) >> 8) & 0xFF; + writeCharacteristicZwiftPlay(gear1, sizeof(gear1), "gear", false, true); + } else { + gear2[4] = gear_value & 0xFF; + gear2[5] = ((gear_value & 0xFF00) >> 8) & 0xFF; + gear2[6] = ((gear_value & 0xFF0000) >> 16) & 0xFF; + writeCharacteristicZwiftPlay(gear2, sizeof(gear2), "gear", false, true); + } + + uint8_t gearApply[] = {0x00, 0x08, 0x88, 0x04}; + writeCharacteristicZwiftPlay(gearApply, sizeof(gearApply), "gearApply", false, true); + } + lastGearValue = gears(); if (requestPower != -1) { @@ -226,6 +409,13 @@ void ftmsbike::update() { emit debug(QStringLiteral("stopping...")); // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); requestStop = -1; + + QSettings settings; + if (settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool()) { + uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + writeCharacteristic(write, sizeof(write), QStringLiteral("init SS2K")); + } } } } @@ -261,7 +451,8 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris if(newValue.length() > 0) { uint8_t b = (uint8_t)newValue.at(0); if(b != battery_level) - homeform::singleton()->setToastRequested(QStringLiteral("Battery Level ") + QString::number(b) + " %"); + if(homeform::singleton()) + homeform::singleton()->setToastRequested(QStringLiteral("Battery Level ") + QString::number(b) + " %"); battery_level = b; } return; @@ -352,7 +543,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris } Distance += ((Speed.value() / 3600000.0) * - ((double)lastRefreshCharacteristicChanged.msecsTo(now))); + ((double)lastRefreshCharacteristicChanged2AD2.msecsTo(now))); emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); @@ -380,7 +571,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris br) / (2.0 * ar)) * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + - settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); if (!resistance_received && !DU30_bike) { Resistance = m_pelotonResistance; emit resistanceRead(Resistance.value()); @@ -431,7 +622,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / - ((double)lastRefreshCharacteristicChanged.msecsTo( + ((double)lastRefreshCharacteristicChanged2AD2.msecsTo( now)))); //(( (0.048* Output in watts +1.19) * body weight in // kg * 3.5) / 200 ) / 60 @@ -464,6 +655,8 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris if (Flags.remainingTime) { // todo } + + lastRefreshCharacteristicChanged2AD2 = now; } else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACE)) { union flags { struct { @@ -525,7 +718,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris index += 3; } else { Distance += ((Speed.value() / 3600000.0) * - ((double)lastRefreshCharacteristicChanged.msecsTo(now))); + ((double)lastRefreshCharacteristicChanged2ACE.msecsTo(now))); } emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); @@ -620,7 +813,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / - ((double)lastRefreshCharacteristicChanged.msecsTo( + ((double)lastRefreshCharacteristicChanged2ACE.msecsTo( now)))); //(( (0.048* Output in watts +1.19) * body weight in // kg * 3.5) / 200 ) / 60 } @@ -654,6 +847,8 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris if (Flags.remainingTime) { // todo } + + lastRefreshCharacteristicChanged2ACE = now; } else { return; } @@ -663,8 +858,6 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); } - lastRefreshCharacteristicChanged = now; - if (heartRateBeltName.startsWith(QStringLiteral("Disabled")) && (!heart || Heart.value() == 0 || disable_hr_frommachinery)) { update_hr_from_external(); @@ -736,7 +929,7 @@ void ftmsbike::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(); + qDebug() << QStringLiteral("char uuid") << c.uuid() << QStringLiteral("handle") << c.handle() << c.properties(); auto descriptors_list = c.descriptors(); for (const QLowEnergyDescriptor &d : qAsConst(descriptors_list)) { qDebug() << QStringLiteral("descriptor uuid") << d.uuid() << QStringLiteral("handle") << d.handle(); @@ -782,10 +975,23 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) { gattWriteCharControlPointId = c; gattFTMSService = s; } + + QBluetoothUuid _zwiftPlayWriteCharControlPointId(QStringLiteral("00000003-19ca-4651-86e5-fa29dcdd09d1")); + if (c.uuid() == _zwiftPlayWriteCharControlPointId) { + qDebug() << QStringLiteral("Zwift Play service and Control Point found"); + zwiftPlayWriteChar = c; + zwiftPlayService = s; + } } } } + if(gattFTMSService == nullptr && DOMYOS) { + settings.setValue(QZSettings::domyosbike_notfmts, true); + if(homeform::singleton()) + homeform::singleton()->setToastRequested("Domyos bike presents itself like a FTMS but it's not. Restart QZ to apply the fix, thanks."); + } + if (gattFTMSService && gattWriteCharControlPointId.isValid() && settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool()) { init(); @@ -838,27 +1044,50 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact } QByteArray b = newValue; + QSettings settings; + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + if (gattWriteCharControlPointId.isValid()) { qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' '); - lastPacketFromFTMS = newValue; // handling gears - if (b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS) { - qDebug() << "applying gears mod" << m_gears; + if (b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && ((zwiftPlayService == nullptr && gears_zwift_ratio) || !gears_zwift_ratio)) { + lastPacketFromFTMS.clear(); + for(int i=0; i> 8; + if (gears() != 0) { + slope += (gears() * 50); } + b[3] = slope & 0xFF; + b[4] = slope >> 8; + + qDebug() << "applying gears mod" << gears() << slope; + /*} else if(b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && zwiftPlayService != nullptr && gears_zwift_ratio) { + int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8)); + uint8_t gear2[] = {0x04, 0x22, 0x02, 0x10, 0x00}; + int g = (int)(((double)slope / 100.0) + settings.value(QZSettings::gears_offset, QZSettings::default_gears_offset).toDouble()); + if(g < 0) { + g = 0; + } + gear2[4] = g; + writeCharacteristicZwiftPlay(gear2, sizeof(gear2), "gearInclination", false, false);*/ + } else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) { + lastPacketFromFTMS.clear(); + for(int i=0; i> 8; + qDebug() << "applying gears mod" << gears() << gearsZwiftRatio() << power; } - if (writeBuffer) { - delete writeBuffer; - } - writeBuffer = new QByteArray(b); - - gattFTMSService->writeCharacteristic(gattWriteCharControlPointId, *writeBuffer); + writeCharacteristic((uint8_t*)b.data(), b.length(), "injectWrite ", false, true); } } diff --git a/src/devices/ftmsbike/ftmsbike.h b/src/devices/ftmsbike/ftmsbike.h index fbcea0293..481719388 100644 --- a/src/devices/ftmsbike/ftmsbike.h +++ b/src/devices/ftmsbike/ftmsbike.h @@ -77,6 +77,9 @@ class ftmsbike : public bike { private: void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, bool wait_for_response = false); + void writeCharacteristicZwiftPlay(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false, + bool wait_for_response = false); + void zwiftPlayInit(); void startDiscover(); uint16_t watts() override; void init(); @@ -90,10 +93,14 @@ class ftmsbike : public bike { QLowEnergyCharacteristic gattWriteCharControlPointId; QLowEnergyService *gattFTMSService = nullptr; + QLowEnergyCharacteristic zwiftPlayWriteChar; + QLowEnergyService *zwiftPlayService = nullptr; + uint8_t sec1Update = 0; QByteArray lastPacket; QByteArray lastPacketFromFTMS; - QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastRefreshCharacteristicChanged2AD2 = QDateTime::currentDateTime(); + QDateTime lastRefreshCharacteristicChanged2ACE = QDateTime::currentDateTime(); uint8_t firstStateChanged = 0; int8_t bikeResistanceOffset = 4; double bikeResistanceGain = 1.0; diff --git a/src/devices/horizontreadmill/horizontreadmill.cpp b/src/devices/horizontreadmill/horizontreadmill.cpp index 4972cb21f..f97fb83de 100644 --- a/src/devices/horizontreadmill/horizontreadmill.cpp +++ b/src/devices/horizontreadmill/horizontreadmill.cpp @@ -1,5 +1,5 @@ #include "horizontreadmill.h" - +#include "homeform.h" #include "devices/ftmsbike/ftmsbike.h" #include "virtualdevices/virtualbike.h" #include "virtualdevices/virtualtreadmill.h" @@ -897,6 +897,7 @@ void horizontreadmill::update() { if (requestInclination != currentInclination().value() && requestInclination >= minInclination && requestInclination <= 15) { + emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination)); forceIncline(requestInclination); } @@ -1301,6 +1302,56 @@ void horizontreadmill::forceIncline(double requestIncline) { writeS[1] = conversion[r]; writeS[2] = conversion1[r]; + } else if(ICONCEPT_FTMS_treadmill) { + if(requestInclination > 0 && requestInclination < 1) { + writeS[1] = 0x3C; + writeS[2] = 0x00; + } else if(requestInclination > 1 && requestInclination < 2) { + writeS[1] = 0x82; + writeS[2] = 0x00; + } else if(requestInclination > 2 && requestInclination < 3) { + writeS[1] = 0xC8; + writeS[2] = 0x00; + } else if(requestInclination > 3 && requestInclination < 4) { + writeS[1] = 0x04; + writeS[2] = 0x01; + } else if(requestInclination > 4 && requestInclination < 5) { + writeS[1] = 0x4A; + writeS[2] = 0x01; + } else if(requestInclination > 5 && requestInclination < 6) { + writeS[1] = 0x90; + writeS[2] = 0x01; + } else if(requestInclination > 6 && requestInclination < 7) { + writeS[1] = 0xCC; + writeS[2] = 0x01; + } else if(requestInclination > 7 && requestInclination < 8) { + writeS[1] = 0x12; + writeS[2] = 0x02; + } else if(requestInclination > 8 && requestInclination < 9) { + writeS[1] = 0x58; + writeS[2] = 0x02; + } else if(requestInclination > 9 && requestInclination < 10) { + writeS[1] = 0x94; + writeS[2] = 0x02; + } else if(requestInclination > 10 && requestInclination < 11) { + writeS[1] = 0xDA; + writeS[2] = 0x02; + } else if(requestInclination > 11 && requestInclination < 12) { + writeS[1] = 0x20; + writeS[2] = 0x03; + } else if(requestInclination > 12 && requestInclination < 13) { + writeS[1] = 0x5C; + writeS[2] = 0x03; + } else if(requestInclination > 13 && requestInclination < 14) { + writeS[1] = 0xA2; + writeS[2] = 0x03; + } else if(requestInclination > 14 && requestInclination < 15) { + writeS[1] = 0xE8; + writeS[2] = 0x03; + } else { + writeS[1] = 0x00; + writeS[2] = 0x00; + } } else { if(HORIZON_78AT_treadmill) requestIncline = requestIncline / 2.0; @@ -1675,7 +1726,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); if (Flags.inclination) { - if(!tunturi_t60_treadmill) + if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill) Inclination = treadmillInclinationOverride((double)( (int16_t)( ((int16_t)(int8_t)newValue.at(index + 1) << 8) | @@ -1683,6 +1734,43 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha ) ) / 10.0); + else if(ICONCEPT_FTMS_treadmill) { + uint8_t val1 = (uint8_t)newValue.at(index); + uint8_t val2 = (uint8_t)newValue.at(index + 1); + if(val1 == 0x3C && val2 == 0x00) { + Inclination = 1; + } else if(val1 == 0x82 && val2 == 0x00) { + Inclination = 2; + } else if(val1 == 0xC8 && val2 == 0x00) { + Inclination = 3; + } else if(val1 == 0x04 && val2 == 0x01) { + Inclination = 4; + } else if(val1 == 0x4A && val2 == 0x01) { + Inclination = 5; + } else if(val1 == 0x90 && val2 == 0x01) { + Inclination = 6; + } else if(val1 == 0xCC && val2 == 0x01) { + Inclination = 7; + } else if(val1 == 0x12 && val2 == 0x02) { + Inclination = 8; + } else if(val1 == 0x58 && val2 == 0x02) { + Inclination = 9; + } else if(val1 == 0x94 && val2 == 0x02) { + Inclination = 10; + } else if(val1 == 0xDA && val2 == 0x02) { + Inclination = 11; + } else if(val1 == 0x20 && val2 == 0x03) { + Inclination = 12; + } else if(val1 == 0x5C && val2 == 0x03) { + Inclination = 13; + } else if(val1 == 0xA2 && val2 == 0x03) { + Inclination = 14; + } else if(val1 == 0xE8 && val2 == 0x03) { + Inclination = 15; + } else { + Inclination = 0; + } + } index += 4; // the ramo value is useless emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value())); } @@ -1999,10 +2087,21 @@ void horizontreadmill::stateChanged(QLowEnergyService::ServiceState state) { QBluetoothUuid _gattWriteCharControlPointId((quint16)0x2AD9); QBluetoothUuid _gattTreadmillDataId((quint16)0x2ACD); QBluetoothUuid _gattCrossTrainerDataId((quint16)0x2ACE); + QBluetoothUuid _gattInclinationSupported((quint16)0x2AD5); + QBluetoothUuid _DomyosServiceId(QStringLiteral("49535343-fe7d-4ae5-8fa9-9fafd205e455")); emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state))); for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) { qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state(); + + if(s->serviceUuid() == _DomyosServiceId && DOMYOS) { + settings.setValue(QZSettings::domyostreadmill_notfmts, true); + settings.sync(); + if(homeform::singleton()) + homeform::singleton()->setToastRequested("Domyos Treadmill presents itself like a FTMS but it's not. Restart QZ to apply the fix, thanks."); + return; + } + if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) { qDebug() << QStringLiteral("not all services discovered"); return; @@ -2042,6 +2141,9 @@ void horizontreadmill::stateChanged(QLowEnergyService::ServiceState state) { // some treadmills doesn't have the control point and also are Cross Trainer devices so i need // anyway to get the FTMS Service at least gattFTMSService = s; + } else if (c.uuid() == _gattInclinationSupported) { + s->readCharacteristic(c); + qDebug() << s->serviceUuid() << c.uuid() << "reading!"; } if (c.properties() & QLowEnergyCharacteristic::Write && c.uuid() == _gattWriteCharCustomService && @@ -2157,6 +2259,17 @@ void horizontreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha void horizontreadmill::characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { qDebug() << QStringLiteral("characteristicRead ") << characteristic.uuid() << newValue.toHex(' '); + QBluetoothUuid _gattInclinationSupported((quint16)0x2AD5); + if(characteristic.uuid() == _gattInclinationSupported && newValue.length() > 2) { + minInclination = ((double)( + (int16_t)( + ((int16_t)(int8_t)newValue.at(1) << 8) | + (uint8_t)newValue.at(0) + ) + ) / + 10.0); + qDebug() << "new minInclination is " << minInclination; + } } void horizontreadmill::serviceScanDone(void) { @@ -2237,6 +2350,12 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) { } else if(device.name().toUpper().startsWith("HORIZON_7.8AT")) { HORIZON_78AT_treadmill = true; qDebug() << QStringLiteral("HORIZON_7.8AT workaround ON!"); + } else if(device.name().toUpper().startsWith("T01_")) { + ICONCEPT_FTMS_treadmill = true; + qDebug() << QStringLiteral("ICONCEPT_FTMS_treadmill workaround ON!"); + } else if ((device.name().toUpper().startsWith("DOMYOS"))) { + qDebug() << QStringLiteral("DOMYOS found"); + DOMYOS = true; } if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) { @@ -2956,7 +3075,7 @@ void horizontreadmill::testProfileCRC() { double horizontreadmill::minStepInclination() { QSettings settings; bool toorx_ftms_treadmill = settings.value(QZSettings::toorx_ftms_treadmill, QZSettings::default_toorx_ftms_treadmill).toBool(); - if (kettler_treadmill || trx3500_treadmill || toorx_ftms_treadmill || sole_tt8_treadmill) + if (kettler_treadmill || trx3500_treadmill || toorx_ftms_treadmill || sole_tt8_treadmill || ICONCEPT_FTMS_treadmill) return 1.0; else return 0.5; diff --git a/src/devices/horizontreadmill/horizontreadmill.h b/src/devices/horizontreadmill/horizontreadmill.h index c7326389b..d216971a0 100644 --- a/src/devices/horizontreadmill/horizontreadmill.h +++ b/src/devices/horizontreadmill/horizontreadmill.h @@ -97,6 +97,8 @@ class horizontreadmill : public treadmill { bool technogymrun = false; bool disableAutoPause = false; bool HORIZON_78AT_treadmill = false; + bool ICONCEPT_FTMS_treadmill = false; + bool DOMYOS = false; void testProfileCRC(); void updateProfileCRC(); diff --git a/src/devices/keepbike/keepbike.cpp b/src/devices/keepbike/keepbike.cpp index 788d6e015..4528d87cc 100644 --- a/src/devices/keepbike/keepbike.cpp +++ b/src/devices/keepbike/keepbike.cpp @@ -355,7 +355,9 @@ void keepbike::stateChanged(QLowEnergyService::ServiceState state) { gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId); gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId); - Q_ASSERT(gattWriteCharacteristic.isValid()); + if(!gattWriteCharacteristic.isValid()) { + gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId); + } Q_ASSERT(gattNotifyCharacteristic.isValid()); // establish hook into notifications diff --git a/src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.cpp b/src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.cpp new file mode 100644 index 000000000..4ce8a42a7 --- /dev/null +++ b/src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.cpp @@ -0,0 +1,655 @@ +#include "nordictrackifitadbelliptical.h" + +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +nordictrackifitadbellipticalLogcatAdbThread::nordictrackifitadbellipticalLogcatAdbThread(QString s) { Q_UNUSED(s) } + +void nordictrackifitadbellipticalLogcatAdbThread::run() { + QSettings settings; + 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 nordictrackifitadbellipticalLogcatAdbThread::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 nordictrackifitadbellipticalLogcatAdbThread::runCommand(QString command) { + if(adbCommandPending.length() == 0) { + adbCommandPending = command; + return true; + } + return false; +} + +void nordictrackifitadbellipticalLogcatAdbThread::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); +#ifdef Q_OS_WINDOWS + if(adbCommandPending.length() != 0) { + runAdbCommand(adbCommandPending); + adbCommandPending = ""; + } +#endif + }); + QObject::connect(process, &QProcess::readyReadStandardError, [process, this]() { + auto output = process->readAllStandardError(); + emit debug("adbLogCat ERROR << " + output); + }); + emit debug("adbLogCat >> " + command); + process->start("adb/adb.exe", QStringList(command.split(' '))); + process->waitForFinished(-1); +#endif +} + +nordictrackifitadbelliptical::nordictrackifitadbelliptical(bool noWriteResistance, bool noHeartService, + int8_t bikeResistanceOffset, double bikeResistanceGain) { + QSettings settings; + bool nordictrack_ifit_adb_remote = + settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote) + .toBool(); + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + initDone = false; + connect(refresh, &QTimer::timeout, this, &nordictrackifitadbelliptical::update); + ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); + refresh->start(200ms); + + socket = new QUdpSocket(this); + bool result = socket->bind(QHostAddress::AnyIPv4, 8002); + qDebug() << result; + processPendingDatagrams(); + connect(socket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams())); + + initRequest = true; + + // ******************************************* virtual treadmill init ************************************* + if (!firstStateChanged && !this->hasVirtualDevice()) { + 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) { + 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, &nordictrackifitadbelliptical::changeInclination); + this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + firstStateChanged = 1; + // ******************************************************************************************************** + + if (nordictrack_ifit_adb_remote) { +#ifdef Q_OS_ANDROID + 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()); +#elif defined Q_OS_WIN + logcatAdbThread = new nordictrackifitadbellipticalLogcatAdbThread("logcatAdbThread"); + /*connect(logcatAdbThread, &nordictrackifitadbellipticalLogcatAdbThread::onSpeedInclination, this, + &nordictrackifitadbelliptical::onSpeedInclination); + connect(logcatAdbThread, &nordictrackifitadbellipticalLogcatAdbThread::onWatt, this, + &nordictrackifitadbelliptical::onWatt);*/ + connect(logcatAdbThread, &nordictrackifitadbellipticalLogcatAdbThread::onHRM, this, &nordictrackifitadbelliptical::onHRM); + connect(logcatAdbThread, &nordictrackifitadbellipticalLogcatAdbThread::debug, this, &nordictrackifitadbelliptical::debug); + logcatAdbThread->start(); +#elif defined Q_OS_IOS +#ifndef IO_UNDER_QT + h->adb_connect(ip.toStdString().c_str()); +#endif +#endif + } +} + +bool nordictrackifitadbelliptical::inclinationAvailableByHardware() { + QSettings settings; + bool proform_studio_NTEX71021 = + settings.value(QZSettings::proform_studio_NTEX71021, QZSettings::default_proform_studio_NTEX71021) + .toBool(); + bool nordictrackadbbike_resistance = settings.value(QZSettings::nordictrackadbbike_resistance, QZSettings::default_nordictrackadbbike_resistance).toBool(); + + if(proform_studio_NTEX71021 || nordictrackadbbike_resistance) + return false; + else + return true; +} + +double nordictrackifitadbelliptical::getDouble(QString v) { + QChar d = QLocale().decimalPoint(); + if (d == ',') { + v = v.replace('.', ','); + } + return QLocale().toDouble(v); +} + +void nordictrackifitadbelliptical::processPendingDatagrams() { + qDebug() << "in !"; + QHostAddress sender; + QSettings settings; + uint16_t port; + while (socket->hasPendingDatagrams()) { + QByteArray datagram; + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &sender, &port); + lastSender = sender; + qDebug() << "Message From :: " << sender.toString(); + qDebug() << "Port From :: " << port; + qDebug() << "Message :: " << datagram; + + QString ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString(); + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat(); + bool nordictrackadbbike_resistance = settings.value(QZSettings::nordictrackadbbike_resistance, QZSettings::default_nordictrackadbbike_resistance).toBool(); + + double speed = 0; + double cadence = 0; + double resistance = 0; + double gear = 0; + double watt = 0; + double grade = 0; + QStringList lines = QString::fromLocal8Bit(datagram.data()).split("\n"); + foreach (QString line, lines) { + qDebug() << line; + + if (line.contains(QStringLiteral("Changed KPH")) && !settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + QStringList aValues = line.split(" "); + if (aValues.length()) { + speed = getDouble(aValues.last()); + Speed = speed; + } + } else if (line.contains(QStringLiteral("Changed RPM"))) { + QStringList aValues = line.split(" "); + if (aValues.length()) { + cadence = getDouble(aValues.last()); + Cadence = cadence; + Speed = Cadence.value() * + settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio) + .toDouble(); + } + } else if (line.contains(QStringLiteral("Changed CurrentGear"))) { + QStringList aValues = line.split(" "); + if (aValues.length()) { + gear = getDouble(aValues.last()); + if(!nordictrackadbbike_resistance) + Resistance = gear; + gearsAvailable = true; + } + } else if (line.contains(QStringLiteral("Changed Resistance"))) { + QStringList aValues = line.split(" "); + if (aValues.length()) { + resistance = getDouble(aValues.last()); + m_pelotonResistance = (100 / 32) * resistance; + qDebug() << QStringLiteral("Current Peloton Resistance: ") << m_pelotonResistance.value() + << resistance; + if(!gearsAvailable && !nordictrackadbbike_resistance) { + Resistance = resistance; + } + } + } else if (line.contains(QStringLiteral("Changed Watts"))) { + QStringList aValues = line.split(" "); + if (aValues.length()) { + watt = getDouble(aValues.last()); + m_watt = watt; + } + } else if (line.contains(QStringLiteral("Changed Grade"))) { + QStringList aValues = line.split(" "); + if (aValues.length()) { + grade = getDouble(aValues.last()); + Inclination = grade; + } + } + } + + 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), 20); + } + + bool proform_studio_NTEX71021 = + settings.value(QZSettings::proform_studio_NTEX71021, QZSettings::default_proform_studio_NTEX71021).toBool(); + bool nordictrack_ifit_adb_remote = + settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote) + .toBool(); + double inclination_delay_seconds = settings.value(QZSettings::inclination_delay_seconds, QZSettings::default_inclination_delay_seconds).toDouble(); + + // only resistance + if(proform_studio_NTEX71021 || nordictrackadbbike_resistance) { + if (nordictrack_ifit_adb_remote) { + if (requestResistance != -1) { + if (requestResistance != currentResistance().value()) { + int x1 = 950; + int y2 = (int)(493 - (13.57 * (requestResistance - 1))); + int y1Resistance = (int)(493 - (13.57 * currentResistance().value())); + + if(!proform_studio_NTEX71021) { // s22i default + x1 = 1920 - 75; + y2 = (int)(803 - (23.777 * requestResistance)); + y1Resistance = (int)(803 - (23.777 * currentResistance().value())); + Resistance = requestResistance; + } + + 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()); +#elif defined(Q_OS_WIN) + if (logcatAdbThread) + logcatAdbThread->runCommand("shell " + lastCommand); +#elif defined Q_OS_IOS +#ifndef IO_UNDER_QT + h->adb_sendcommand(lastCommand.toStdString().c_str()); +#endif +#endif + } + } + + requestResistance = -1; + } + QByteArray message = (QString::number(requestResistance).toLocal8Bit()) + ";"; + requestResistance = -1; + int ret = socket->writeDatagram(message, message.size(), sender, 8003); + qDebug() << QString::number(ret) + " >> " + message; + } + // since the motor of the bike is slow, let's filter the inclination changes to more than 4 seconds + else if (lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > inclination_delay_seconds) { + lastInclinationChanged = QDateTime::currentDateTime(); + if (nordictrack_ifit_adb_remote) { + bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); + if (requestInclination != -100 && erg_mode && requestResistance != -100) { + qDebug() << "forcing inclination based on the erg mode resistance request of" << requestResistance; + requestInclination = requestResistance; + requestResistance = -100; + } + if (requestInclination != -100) { + double inc = qRound(requestInclination / 0.5) * 0.5; + if (inc != currentInclination().value()) { + bool proform_studio = settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool(); + bool proform_tdf_10_0 = settings.value(QZSettings::proform_tdf_10_0, QZSettings::default_proform_tdf_10_0).toBool(); + int x1 = 75; + int y2 = (int)(616.18 - (17.223 * (inc + gears()))); + int y1Resistance = (int)(616.18 - (17.223 * currentInclination().value())); + + if(proform_studio) { + x1 = 1827; + y2 = (int)(806 - (21.375 * (inc + gears()))); + y1Resistance = (int)(806 - (21.375 * currentInclination().value())); + } else if(proform_tdf_10_0) { + x1 = 75; + y2 = (int)(477 - (12.5 * (inc + gears()))); + y1Resistance = (int)(477 - (12.5 * currentInclination().value())); + } + + lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " + + QString::number(x1) + " " + QString::number(y2) + " 200"; + qDebug() << " >> " + lastCommand; +#ifdef Q_OS_ANDROID + QAndroidJniObject command = QAndroidJniObject::fromString(lastCommand).object(); + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/QZAdbRemote", + "sendCommand", "(Ljava/lang/String;)V", + command.object()); +#elif defined(Q_OS_WIN) + if (logcatAdbThread) + logcatAdbThread->runCommand("shell " + lastCommand); +#elif defined Q_OS_IOS +#ifndef IO_UNDER_QT + h->adb_sendcommand(lastCommand.toStdString().c_str()); +#endif +#endif + // this bike has both inclination and resistance, let's try to handle both + // the Original Poster doesn't want anymore, but maybe it will be useful in the future + /* + if(freemotion_coachbike_b22_7) { + int x1 = 75; + int y2 = (int)(616.18 - (17.223 * (inc + gears()))); + int y1Resistance = (int)(616.18 - (17.223 * currentInclination().value())); + + lastCommand = "input swipe " + QString::number(x1) + " " + QString::number(y1Resistance) + " " + + QString::number(x1) + " " + QString::number(y2) + " 200"; + qDebug() << " >> " + lastCommand; + #ifdef Q_OS_ANDROID + QAndroidJniObject command = QAndroidJniObject::fromString(lastCommand).object(); + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/QZAdbRemote", + "sendCommand", "(Ljava/lang/String;)V", + command.object()); + #elif defined(Q_OS_WIN) + if (logcatAdbThread) + logcatAdbThread->runCommand("shell " + lastCommand); + #elif defined Q_OS_IOS + #ifndef IO_UNDER_QT + h->adb_sendcommand(lastCommand.toStdString().c_str()); + #endif + #endif + } + */ + } + } + + requestInclination = -100; + } + + double r = currentResistance().value() + difficult() + gears(); // the inclination here is like the resistance for the other bikes + QByteArray message = (QString::number(requestInclination).toLocal8Bit()) + ";" + QString::number(r).toLocal8Bit(); + requestInclination = -100; + int ret = socket->writeDatagram(message, message.size(), sender, 8003); + qDebug() << QString::number(ret) + " >> " + message; + } + + if (watts()) + KCal += + ((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) / + (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( + QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in + // kg * 3.5) / 200 ) / 60 + // KCal = (((uint16_t)((uint8_t)newValue.at(15)) << 8) + (uint16_t)((uint8_t) newValue.at(14))); + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))); + + if (Cadence.value() > 0) { + CrankRevs++; + LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0)); + } + + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + +#ifdef Q_OS_ANDROID + if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) + Heart = (uint8_t)KeepAwakeHelper::heart(); + else +#endif + { + if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { + update_hr_from_external(); + } + } + +#ifdef Q_OS_IOS +#ifndef IO_UNDER_QT + bool cadencep = + settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool(); + bool ios_peloton_workaround = + settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool(); + if (ios_peloton_workaround && cadencep && h && firstStateChanged) { + h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime()); + h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate()); + } +#endif +#endif + + emit debug(QStringLiteral("Current Watt: ") + QString::number(watts())); + emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value())); + emit debug(QStringLiteral("Current Gear: ") + QString::number(gear)); + emit debug(QStringLiteral("Current 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)); + } +} + +void nordictrackifitadbelliptical::onHRM(int hrm) { + QSettings settings; + QString heartRateBeltName = + settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString(); + bool disable_hr_frommachinery = + settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool(); + + if ( +#ifdef Q_OS_ANDROID + (!settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) && +#endif + heartRateBeltName.startsWith(QStringLiteral("Disabled")) && !disable_hr_frommachinery) { + + Heart = hrm; + emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value())); + } +} + +void nordictrackifitadbelliptical::forceResistance(double resistance) {} + +void nordictrackifitadbelliptical::update() { + + QSettings settings; + update_metrics(false, 0); + + if (initRequest) { + initRequest = false; + emit connectedAndDiscovered(); + } + + // updating the treadmill console every second + if (sec1Update++ == (500 / refresh->interval())) { + sec1Update = 0; + // updateDisplay(elapsed); + } + + if (requestStart != -1) { + emit debug(QStringLiteral("starting...")); + + // btinit(); + + requestStart = -1; + } + if (requestStop != -1) { + emit debug(QStringLiteral("stopping...")); + // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); + requestStop = -1; + } +} + +uint16_t nordictrackifitadbelliptical::watts() { return m_watt.value(); } + +void nordictrackifitadbelliptical::changeInclinationRequested(double grade, double percentage) { + if (percentage < 0) + percentage = 0; + changeInclination(grade, percentage); +} + +bool nordictrackifitadbelliptical::connected() { return true; } + +uint16_t nordictrackifitadbelliptical::wattsFromResistance(double inclination, double cadence) { + // this is for the s22i + double power = 0.0; + + if (inclination == 0.0) { + power = 0.01 * cadence * cadence + -0.25 * cadence + 8.00; + } + else if (inclination == 0.5) { + power = 0.01 * cadence * cadence + -0.15 * cadence + 4.00; + } + else if (inclination == 1.0) { + power = 0.01 * cadence * cadence + -0.50 * cadence + 11.00; + } + else if (inclination == 1.5) { + power = 0.01 * cadence * cadence + -0.50 * cadence + 11.00; + } + else if (inclination == 2.0) { + power = 0.01 * cadence * cadence + -0.40 * cadence + 8.00; + } + else if (inclination == 2.5) { + power = 0.01 * cadence * cadence + 0.15 * cadence + -6.00; + } + else if (inclination == 3.0) { + power = 0.02 * cadence * cadence + -1.10 * cadence + 21.00; + } + else if (inclination == 3.5) { + power = 0.03 * cadence * cadence + -2.10 * cadence + 47.00; + } + else if (inclination == 4.0) { + power = 0.04 * cadence * cadence + -2.45 * cadence + 56.00; + } + else if (inclination == 4.5) { + power = 0.03 * cadence * cadence + -1.90 * cadence + 42.00; + } + else if (inclination == 5.0) { + power = 0.05 * cadence * cadence + -3.70 * cadence + 86.00; + } + else if (inclination == 5.5) { + power = 0.05 * cadence * cadence + -3.80 * cadence + 92.00; + } + else if (inclination == 6.0) { + power = 0.07 * cadence * cadence + -5.10 * cadence + 112.00; + } + else if (inclination == 6.5) { + power = 0.06 * cadence * cadence + -4.20 * cadence + 94.00; + } + else if (inclination == 7.0) { + power = 0.03 * cadence * cadence + -0.40 * cadence + -10.00; + } + else if (inclination == 7.5) { + power = 0.07 * cadence * cadence + -4.60 * cadence + 100.00; + } + else if (inclination == 8.0) { + power = 0.11 * cadence * cadence + -7.75 * cadence + 180.00; + } + else if (inclination == 8.5) { + power = 0.09 * cadence * cadence + -5.75 * cadence + 132.00; + } + else if (inclination == 9.0) { + power = 0.08 * cadence * cadence + -4.40 * cadence + 90.00; + } + else if (inclination == 9.5) { + power = 0.08 * cadence * cadence + -4.60 * cadence + 102.00; + } + else if (inclination == 10.0) { + power = 0.11 * cadence * cadence + -7.30 * cadence + 180.00; + } + else if (inclination == 10.5) { + power = 0.08 * cadence * cadence + -4.00 * cadence + 90.00; + } + else if (inclination == 11.0) { + power = 0.12 * cadence * cadence + -7.40 * cadence + 174.00; + } + else if (inclination == 11.5) { + power = 0.12 * cadence * cadence + -7.40 * cadence + 174.00; + } + else if (inclination == 12.0) { + power = 0.20 * cadence * cadence + -14.70 * cadence + 351.00; + } + else if (inclination == 12.5) { + power = 0.20 * cadence * cadence + -14.75 * cadence + 372.00; + } + else if (inclination == 13.0) { + power = 0.12 * cadence * cadence + -6.30 * cadence + 159.00; + } + else if (inclination == 13.5) { + power = 0.15 * cadence * cadence + -9.00 * cadence + 219.00; + } + else if (inclination == 14.0) { + power = 0.37 * cadence * cadence + -30.60 * cadence + 753.00; + } + else if (inclination == 14.5) { + power = 0.14 * cadence * cadence + -7.30 * cadence + 183.00; + } + else if (inclination == 15.0) { + power = 0.17 * cadence * cadence + -8.85 * cadence + 222.00; + } + else if (inclination == 15.5) { + power = 0.17 * cadence * cadence + -8.85 * cadence + 222.00; + } + else if (inclination == 16.0) { + power = 0.19 * cadence * cadence + -9.75 * cadence + 245.00; + } + else if (inclination == 16.5) { + power = 0.26 * cadence * cadence + -17.45 * cadence + 455.00; + } + else if (inclination == 17.0) { + power = 0.27 * cadence * cadence + -17.90 * cadence + 470.00; + } + else if (inclination == 17.5) { + power = 0.27 * cadence * cadence + -17.90 * cadence + 470.00; + } + else { + qDebug() << "Inclination level not supported"; + } + + return power; +} diff --git a/src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h b/src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h new file mode 100644 index 000000000..79d38a8d3 --- /dev/null +++ b/src/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h @@ -0,0 +1,117 @@ +#ifndef NORDICTRACKIFITADBELLIPTICAL_H +#define NORDICTRACKIFITADBELLIPTICAL_H + +#include + +#ifndef Q_OS_ANDROID +#include +#else +#include +#endif +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "devices/elliptical.h" +#include "virtualdevices/virtualbike.h" + +#ifdef Q_OS_IOS +#include "ios/lockscreen.h" +#endif + +class nordictrackifitadbellipticalLogcatAdbThread : public QThread { + Q_OBJECT + + public: + explicit nordictrackifitadbellipticalLogcatAdbThread(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); +}; + +class nordictrackifitadbelliptical : public elliptical { + Q_OBJECT + public: + nordictrackifitadbelliptical(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset, + double bikeResistanceGain); + bool connected() override; + bool inclinationAvailableByHardware() override; + + private: + const resistance_t max_resistance = 17; // max inclination for s22i + void forceResistance(double resistance); + uint16_t watts() override; + double getDouble(QString v); + uint16_t wattsFromResistance(double inclination, double cadence); + + QTimer *refresh; + + uint8_t sec1Update = 0; + QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastInclinationChanged = QDateTime::currentDateTime(); + uint8_t firstStateChanged = 0; + uint16_t m_watts = 0; + + bool initDone = false; + bool initRequest = false; + + bool noWriteResistance = false; + bool noHeartService = false; + + bool gearsAvailable = false; + + QUdpSocket *socket = nullptr; + QHostAddress lastSender; + + nordictrackifitadbellipticalLogcatAdbThread *logcatAdbThread = nullptr; + + QString lastCommand; + + QString ip; + +#ifdef Q_OS_IOS + lockscreen *h = 0; +#endif + + signals: + void disconnected(); + void debug(QString string); + + private slots: + + void processPendingDatagrams(); + void changeInclinationRequested(double grade, double percentage); + void onHRM(int hrm); + + void update(); +}; + +#endif // NORDICTRACKIFITADBELLIPTICAL_H diff --git a/src/devices/proformtreadmill/proformtreadmill.cpp b/src/devices/proformtreadmill/proformtreadmill.cpp index e936d43c5..0c43eed88 100644 --- a/src/devices/proformtreadmill/proformtreadmill.cpp +++ b/src/devices/proformtreadmill/proformtreadmill.cpp @@ -73,9 +73,9 @@ void proformtreadmill::forceIncline(double incline) { if (norditrack_s25i_treadmill) { write[14] = write[11] + write[12] + 0x11; } else if (proform_treadmill_8_0 || proform_treadmill_705_cst || proform_treadmill_705_cst_V78_239 || proform_treadmill_9_0 || proform_treadmill_se || - proform_treadmill_z1300i || proform_treadmill_l6_0s || norditrack_s25_treadmill || proform_8_5_treadmill || proform_2000_treadmill || + proform_treadmill_z1300i || proform_treadmill_l6_0s || norditrack_s25_treadmill || proform_8_5_treadmill || nordictrack_treadmill_exp_5i || proform_2000_treadmill || proform_treadmill_sport_8_5 || proform_treadmill_505_cst || proform_carbon_tl || proform_proshox2 || nordictrack_s20i_treadmill || proform_595i_proshox2 || - proform_treadmill_8_7) { + proform_treadmill_8_7 || proform_carbon_tl_PFTL59720) { write[14] = write[11] + write[12] + 0x12; } else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_s20_treadmill && !nordictrack_t65s_83_treadmill) { for (uint8_t i = 0; i < 7; i++) { @@ -100,9 +100,9 @@ void proformtreadmill::forceSpeed(double speed) { if (norditrack_s25i_treadmill) { write[14] = write[11] + write[12] + 0x11; } else if (proform_treadmill_8_0 || proform_treadmill_9_0 || proform_treadmill_se || proform_cadence_lt || - proform_treadmill_z1300i || proform_treadmill_l6_0s || norditrack_s25_treadmill || proform_8_5_treadmill || proform_2000_treadmill || + proform_treadmill_z1300i || proform_treadmill_l6_0s || norditrack_s25_treadmill || proform_8_5_treadmill || nordictrack_treadmill_exp_5i || proform_2000_treadmill || proform_treadmill_sport_8_5 || proform_treadmill_505_cst || proform_treadmill_705_cst || proform_treadmill_705_cst_V78_239 || proform_carbon_tl || proform_proshox2 || nordictrack_s20i_treadmill || proform_595i_proshox2 || - proform_treadmill_8_7) { + proform_treadmill_8_7 || proform_carbon_tl_PFTL59720) { write[14] = write[11] + write[12] + 0x11; } else if (!nordictrack_t65s_treadmill && !nordictrack_s30_treadmill && !nordictrack_s20_treadmill && !nordictrack_t65s_83_treadmill) { for (uint8_t i = 0; i < 7; i++) { @@ -545,7 +545,7 @@ void proformtreadmill::update() { if (counterPoll > 5) { counterPoll = 0; } - } else if (proform_8_5_treadmill) { + } else if (proform_8_5_treadmill || nordictrack_treadmill_exp_5i) { uint8_t noOpData1[] = {0xfe, 0x02, 0x19, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00, 0x0f, 0x80, 0x08, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t noOpData3[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x80, 0x00, 0x10, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -1542,6 +1542,68 @@ void proformtreadmill::update() { if (counterPoll > 5) { counterPoll = 0; } + } else if (proform_carbon_tl_PFTL59720) { + 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, 0x81, 0x00, 0x10, 0x86, 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 if (proform_treadmill_8_7) { uint8_t noOpData1[] = {0xfe, 0x02, 0x17, 0x03}; uint8_t noOpData2[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x04, 0x13, 0x02, 0x00, 0x0d, 0x13, 0x96, 0x31, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80}; @@ -1887,7 +1949,8 @@ void proformtreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha ((norditrack_s25i_treadmill) && (newValue.at(4) != 0x02 || (newValue.at(5) != 0x2f))) || ((nordictrack_t65s_treadmill || proform_pro_1000_treadmill || nordictrack_t65s_83_treadmill || nordictrack_s30_treadmill || - nordictrack_s20_treadmill || proform_treadmill_se || proform_cadence_lt || proform_8_5_treadmill || proform_carbon_tl || nordictrack_s20i_treadmill || proform_treadmill_8_7) && + nordictrack_s20_treadmill || proform_treadmill_se || proform_cadence_lt || proform_8_5_treadmill || nordictrack_treadmill_exp_5i || proform_carbon_tl || + nordictrack_s20i_treadmill || proform_treadmill_8_7 || proform_carbon_tl_PFTL59720) && (newValue.at(4) != 0x02 || newValue.at(5) != 0x2e)) || (((uint8_t)newValue.at(12)) == 0xFF && ((uint8_t)newValue.at(13)) == 0xFF && @@ -1988,6 +2051,7 @@ void proformtreadmill::btinit() { .toBool(); proform_treadmill_z1300i = settings.value(QZSettings::proform_treadmill_z1300i, QZSettings::default_proform_treadmill_z1300i).toBool(); + nordictrack_treadmill_exp_5i = settings.value(QZSettings::nordictrack_treadmill_exp_5i, QZSettings::default_nordictrack_treadmill_exp_5i).toBool(); proform_pro_1000_treadmill = settings.value(QZSettings::proform_pro_1000_treadmill, QZSettings::default_proform_pro_1000_treadmill).toBool(); nordictrack_s20_treadmill = settings.value(QZSettings::nordictrack_s20_treadmill, @@ -2005,6 +2069,7 @@ void proformtreadmill::btinit() { proform_proshox2 = settings.value(QZSettings::proform_proshox2, QZSettings::default_proform_proshox2).toBool(); proform_595i_proshox2 = settings.value(QZSettings::proform_595i_proshox2, QZSettings::default_proform_595i_proshox2).toBool(); proform_treadmill_8_7 = settings.value(QZSettings::proform_treadmill_8_7, QZSettings::default_proform_treadmill_8_7).toBool(); + proform_carbon_tl_PFTL59720 = settings.value(QZSettings::proform_carbon_tl_PFTL59720, QZSettings::default_proform_carbon_tl_PFTL59720).toBool(); // bool proform_treadmill_995i = settings.value(QZSettings::proform_treadmill_995i, @@ -2236,6 +2301,78 @@ void proformtreadmill::btinit() { QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); QThread::msleep(sleepms); +} else if (proform_carbon_tl_PFTL59720) { + 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, 0xdd, 0x28, 0x79, 0xc8, 0x25, 0x70, 0xd9, 0x20, 0x8d}; + uint8_t initData11[] = {0x01, 0x12, 0xe8, 0x49, 0xa8, 0x05, 0x60, 0xc9, 0x50, 0xbd, 0x08, 0x99, 0xe8, 0x65, 0xf0, 0x79, 0xc0, 0x4d, 0xc8, 0x49}; + uint8_t initData12[] = {0xff, 0x08, 0xc8, 0x45, 0xc0, 0x98, 0x02, 0x00, 0x00, 0x15, 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, 0x00, 0x0f, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0}; + uint8_t noOpData3[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x0e, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3a, 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_8_7) { uint8_t initData1[] = {0xfe, 0x02, 0x08, 0x02}; writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false); @@ -3071,6 +3208,84 @@ void proformtreadmill::btinit() { QThread::msleep(sleepms); writeCharacteristic(noOpData6, sizeof(noOpData6), QStringLiteral("init"), false, false); QThread::msleep(sleepms); + } else if (nordictrack_treadmill_exp_5i) { + 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, 0xe0, 0xd5, 0xc8, 0xc1, 0xb8, 0xbd, 0xb0, 0xb1, 0xb0, 0xb5}; + uint8_t initData11[] = {0x01, 0x12, 0xc8, 0xc1, 0xd8, 0xed, 0xe0, 0x11, 0x00, 0x35, 0x28, 0x41, 0x78, 0x9d, 0xb0, 0xd1, 0xf0, 0x15, 0x28, 0x41}; + uint8_t initData12[] = {0xff, 0x08, 0x98, 0xad, 0xc0, 0xa0, 0x02, 0x00, 0x00, 0xc4, 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, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData3[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData4[] = {0xfe, 0x02, 0x19, 0x03}; + uint8_t noOpData5[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData6[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t noOpData7[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x04, 0x15, 0x02, 0x00, 0x0f, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x48, 0x00, 0x00, 0xe0}; + uint8_t noOpData8[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x08, 0x6e, 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); + writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("init"), false, false); + QThread::msleep(sleepms); + writeCharacteristic(noOpData8, sizeof(noOpData8), 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, diff --git a/src/devices/proformtreadmill/proformtreadmill.h b/src/devices/proformtreadmill/proformtreadmill.h index 83bcb274f..f94d4bf38 100644 --- a/src/devices/proformtreadmill/proformtreadmill.h +++ b/src/devices/proformtreadmill/proformtreadmill.h @@ -96,6 +96,8 @@ class proformtreadmill : public treadmill { bool proform_595i_proshox2 = false; bool proform_treadmill_8_7 = false; bool proform_treadmill_705_cst_V78_239 = false; + bool nordictrack_treadmill_exp_5i = false; + bool proform_carbon_tl_PFTL59720 = false; #ifdef Q_OS_IOS lockscreen *h = 0; diff --git a/src/devices/proformwifibike/proformwifibike.cpp b/src/devices/proformwifibike/proformwifibike.cpp index 56f722823..4cee24713 100644 --- a/src/devices/proformwifibike/proformwifibike.cpp +++ b/src/devices/proformwifibike/proformwifibike.cpp @@ -34,6 +34,7 @@ proformwifibike::proformwifibike(bool noWriteResistance, bool noHeartService, in ok = connect(&websocket, &QWebSocket::connected, [&]() { qDebug() << "connected!"; }); ok = connect(&websocket, &QWebSocket::disconnected, [&]() { qDebug() << "disconnected!"; + lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); connectToDevice(); }); diff --git a/src/devices/renphobike/renphobike.cpp b/src/devices/renphobike/renphobike.cpp index 252ef7c7e..5e5bf9b4d 100644 --- a/src/devices/renphobike/renphobike.cpp +++ b/src/devices/renphobike/renphobike.cpp @@ -830,10 +830,10 @@ void renphobike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &chara qDebug() << QStringLiteral("sending") << lastFTMSPacketReceived.toHex(' '); // handling gears } else if (lastFTMSPacketReceived.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS) { - qDebug() << "applying gears mod" << m_gears; + qDebug() << "applying gears mod" << gears(); int16_t slope = (((uint8_t)lastFTMSPacketReceived.at(3)) + (lastFTMSPacketReceived.at(4) << 8)); - if (m_gears != 0) { - slope += (m_gears * 50); + if (gears() != 0) { + slope += (gears() * 50); lastFTMSPacketReceived[3] = slope & 0xFF; lastFTMSPacketReceived[4] = slope >> 8; } diff --git a/src/devices/rower.h b/src/devices/rower.h index 048bee07e..e65655b4c 100644 --- a/src/devices/rower.h +++ b/src/devices/rower.h @@ -59,7 +59,6 @@ class rower : public bluetoothdevice { metric RequestedPelotonResistance; double requestInclination = -100; metric RequestedCadence; - metric RequestedPower; metric RequestedSpeed; volatile double requestSpeed = -1; metric StrokesLength; diff --git a/src/devices/skandikawiribike/skandikawiribike.cpp b/src/devices/skandikawiribike/skandikawiribike.cpp index 01022e743..9aca3a16a 100644 --- a/src/devices/skandikawiribike/skandikawiribike.cpp +++ b/src/devices/skandikawiribike/skandikawiribike.cpp @@ -247,8 +247,8 @@ void skandikawiribike::characteristicChanged(const QLowEnergyCharacteristic &cha #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) { - if (X2000) { - Heart = newValue.at(8); + if (X2000 || delightechBike) { + Heart = newValue.at(8); // X-2000 or delightech app/protocol compatible bike (e.g. Skandika Morpheus) } else { Heart = 0; } @@ -427,8 +427,14 @@ void skandikawiribike::deviceDiscovered(const QBluetoothDeviceInfo &device) { bluetoothDevice = device; if (device.name().toUpper().startsWith(QLatin1String("HT"))) { - X2000 = true; - qDebug() << "X-2000 WORKAROUND!"; + if (device.name().length() == 11) { // Bikes like the Skandika X-2000 Foldaway Bike + X2000 = true; + qDebug() << "X-2000 WORKAROUND!"; + } else if (device.name().length() == 12) // Bikes compatible with delightech app/protocol, for example Skandika Morpheus + { + qDebug() << "deligthechbike WORKAROUND!"; + delightechBike = true; + } } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); @@ -478,7 +484,7 @@ uint16_t skandikawiribike::watts() { // ref // https://translate.google.com/translate?hl=it&sl=en&u=https://support.wattbike.com/hc/en-us/articles/115001881825-Power-Resistance-and-Cadence-Tables&prev=search&pto=aue - if (currentSpeed().value() <= 0) { + if (currentCadence().value() == 0) { // only update watts if pedaling return 0; } diff --git a/src/devices/skandikawiribike/skandikawiribike.h b/src/devices/skandikawiribike/skandikawiribike.h index caf910d05..e68bbb988 100644 --- a/src/devices/skandikawiribike/skandikawiribike.h +++ b/src/devices/skandikawiribike/skandikawiribike.h @@ -75,6 +75,7 @@ class skandikawiribike : public bike { bool noHeartService = false; bool X2000 = false; + bool delightechBike = false; #ifdef Q_OS_IOS lockscreen *h = 0; diff --git a/src/devices/sportstechelliptical/sportstechelliptical.cpp b/src/devices/sportstechelliptical/sportstechelliptical.cpp new file mode 100644 index 000000000..a0c37ff1f --- /dev/null +++ b/src/devices/sportstechelliptical/sportstechelliptical.cpp @@ -0,0 +1,454 @@ +#include "sportstechelliptical.h" +#ifdef Q_OS_ANDROID +#include "keepawakehelper.h" +#endif +#include "virtualdevices/virtualtreadmill.h".h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +sportstechelliptical::sportstechelliptical(bool noWriteResistance, bool noHeartService, int8_t ellipticalResistanceOffset, + double ellipticalResistanceGain) { + m_watt.setType(metric::METRIC_WATT); + Speed.setType(metric::METRIC_SPEED); + refresh = new QTimer(this); + this->noWriteResistance = noWriteResistance; + this->noHeartService = noHeartService; + this->ellipticalResistanceGain = ellipticalResistanceGain; + this->ellipticalResistanceOffset = ellipticalResistanceOffset; + initDone = false; + connect(refresh, &QTimer::timeout, this, &sportstechelliptical::update); + refresh->start(200ms); +} + +void sportstechelliptical::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, &sportstechelliptical::packetReceived, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } else { + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit); + timeout.singleShot(300ms, &loop, &QEventLoop::quit); + } + + if (writeBuffer) { + delete writeBuffer; + } + writeBuffer = new QByteArray((const char *)data, data_len); + + gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer); + + if (!disable_log) { + emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') + " // " + info); + } + + loop.exec(); + + if (timeout.isActive() == false) { + emit debug(QStringLiteral(" exit for timeout")); + } +} + +void sportstechelliptical::forceResistance(resistance_t requestResistance) { + Q_UNUSED(requestResistance) + /* + uint8_t resistance[] = { 0xf0, 0xa6, 0x01, 0x01, 0x00, 0x00 }; + resistance[4] = requestResistance + 1; + for(uint8_t i=0; istate() << gattCommunicationChannelService << + // gattWriteCharacteristic.isValid() << gattNotifyCharacteristic.isValid() << initDone; + + if (!m_control) { + return; + } + + if (m_control->state() == QLowEnergyController::UnconnectedState) { + emit disconnected(); + return; + } + + if (initRequest) { + initRequest = false; + btinit(false); + } else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState && + gattCommunicationChannelService && gattWriteCharacteristic.isValid() && + gattNotify1Characteristic.isValid() && initDone) { + update_metrics(false, 0); + + // updating the elliptical console every second + if (sec1update++ == (1000 / refresh->interval())) { + sec1update = 0; + // updateDisplay(elapsed); + } + + QSettings settings; + uint8_t noOpData[] = {0xf2, 0xc3, 0x07, 0x04, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbe}; + if (requestResistance < 0) { + requestResistance = 0; + } + if (requestResistance > 23) { + requestResistance = 23; + } + noOpData[4] = requestResistance; + noOpData[10] += requestResistance; + writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true); + } +} + +void sportstechelliptical::serviceDiscovered(const QBluetoothUuid &gatt) { + emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString()); +} + +void sportstechelliptical::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + QDateTime now = QDateTime::currentDateTime(); + // 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(); + emit packetReceived(); + + emit debug(QStringLiteral(" << ") + newValue.toHex(' ')); + + lastPacket = newValue; + if (newValue.length() != 20) { + return; + } + + double speed = GetSpeedFromPacket(newValue); + double cadence = GetCadenceFromPacket(newValue); + double resistance = GetResistanceFromPacket(newValue); + double kcal = GetKcalFromPacket(newValue); + double watt = GetWattFromPacket(newValue); + 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)newValue.at(11)); + if (heart == 0 || disable_hr_frommachinery) { + update_hr_from_external(); + } else { + Heart = heart; + } + } + } + + if (!firstCharChanged) { + Distance += ((speed / 3600.0) / (1000.0 / (lastTimeCharChanged.msecsTo(QTime::currentTime())))); + } + + emit debug(QStringLiteral("Current speed: ") + QString::number(speed)); + emit debug(QStringLiteral("Current cadence: ") + QString::number(cadence)); + emit debug(QStringLiteral("Current resistance: ") + QString::number(resistance)); + emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value())); + emit debug(QStringLiteral("Current KCal: ") + QString::number(kcal)); + emit debug(QStringLiteral("Current watt: ") + QString::number(watt)); + emit debug(QStringLiteral("Current Elapsed from the elliptical (not used): ") + + QString::number(GetElapsedFromPacket(newValue))); + emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value())); + + if (m_control->error() != QLowEnergyController::NoError) { + qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString(); + } + + Speed = speed; + Resistance = requestResistance; + KCal = kcal; + if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) { + Cadence = cadence; + } + if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) + m_watt = watt; + + lastTimeCharChanged = QTime::currentTime(); + firstCharChanged = false; +} + +uint16_t sportstechelliptical::GetElapsedFromPacket(const QByteArray &packet) { + uint16_t convertedDataSec = (packet.at(4)); + uint16_t convertedDataMin = (packet.at(3)); + uint16_t convertedData = convertedDataMin * 60.f + convertedDataSec; + return convertedData; +} + +double sportstechelliptical::GetSpeedFromPacket(const QByteArray &packet) { + uint16_t convertedData = (packet.at(12) << 8) | ((uint8_t)packet.at(13)); + double data = (double)(convertedData) / 10.0f; + return data; +} + +double sportstechelliptical::GetKcalFromPacket(const QByteArray &packet) { + uint16_t convertedData = (packet.at(7) << 8) | ((uint8_t)packet.at(8)); + return (double)(convertedData); +} + +double sportstechelliptical::GetWattFromPacket(const QByteArray &packet) { + uint16_t convertedData = (packet.at(9) << 8) | ((uint8_t)packet.at(10)); + double data = ((double)(convertedData)); + return data; +} + +double sportstechelliptical::GetCadenceFromPacket(const QByteArray &packet) { + uint16_t convertedData = packet.at(17); + double data = (convertedData); + if (data < 0) { + return 0; + } + return data; +} + +double sportstechelliptical::GetResistanceFromPacket(const QByteArray &packet) { + uint16_t convertedData = packet.at(15); + double data = (convertedData); + if (data < 0) { + return 0; + } + return data; +} + +void sportstechelliptical::btinit(bool startTape) { + Q_UNUSED(startTape); + QSettings settings; + + const uint8_t initData1[] = {0xf2, 0xc0, 0x00, 0xb2}; + const uint8_t initData2[] = {0xf2, 0xc1, 0x05, 0x01, 0xff, 0xff, 0xff, 0xff, 0xb5}; + const uint8_t initData3[] = {0xf2, 0xc4, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc0}; + const uint8_t initData4[] = {0xf2, 0xc3, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbb}; + + writeCharacteristic((uint8_t *)initData1, sizeof(initData1), QStringLiteral("init"), false, true); + writeCharacteristic((uint8_t *)initData2, sizeof(initData2), QStringLiteral("init"), false, true); + writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true); + writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true); + + initDone = true; +} + +void sportstechelliptical::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()); + } + + // QString uuidWrite = "0000fff2-0000-1000-8000-00805f9b34fb"; + // QString uuidNotify1 = "0000fff1-0000-1000-8000-00805f9b34fb"; + + QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0000fff2-0000-1000-8000-00805f9b34fb")); + QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb")); + + 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, + &sportstechelliptical::characteristicChanged); + connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this, + &sportstechelliptical::characteristicWritten); + connect(gattCommunicationChannelService, + static_cast(&QLowEnergyService::error), + this, &sportstechelliptical::errorService); + connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this, + &sportstechelliptical::descriptorWritten); + + // ******************************************* virtual elliptical init ************************************* + if (!firstVirtualelliptical && !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 elliptical interface...")); + auto virtualTreadmill = new virtualtreadmill(this, noHeartService); + // connect(virtualelliptical,&virtualelliptical::debug ,this,&sportstechelliptical::debug); + connect(virtualTreadmill, &virtualtreadmill::changeInclination, this, &sportstechelliptical::changeInclination); + this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY); + } + } + firstVirtualelliptical = 1; + // ******************************************************************************************************** + + QByteArray descriptor; + descriptor.append((char)0x01); + descriptor.append((char)0x00); + gattCommunicationChannelService->writeDescriptor( + gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor); + } +} + +void sportstechelliptical::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) { + emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' ')); + + initRequest = true; + emit connectedAndDiscovered(); +} + +void sportstechelliptical::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { + Q_UNUSED(characteristic); + emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' ')); +} + +void sportstechelliptical::serviceScanDone(void) { + emit debug(QStringLiteral("serviceScanDone")); + + // QString uuid = "0000fff0-0000-1000-8000-00805f9b34fb"; + + QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb")); + gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId); + + if (gattCommunicationChannelService == nullptr) { + qDebug() << QStringLiteral("invalid service") << _gattCommunicationChannelServiceId.toString(); + return; + } + + connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &sportstechelliptical::stateChanged); + gattCommunicationChannelService->discoverDetails(); +} + +void sportstechelliptical::errorService(QLowEnergyService::ServiceError err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("sportstechelliptical::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void sportstechelliptical::error(QLowEnergyController::Error err) { + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit debug(QStringLiteral("sportstechelliptical::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + + m_control->errorString()); +} + +void sportstechelliptical::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, &sportstechelliptical::serviceDiscovered); + connect(m_control, &QLowEnergyController::discoveryFinished, this, &sportstechelliptical::serviceScanDone); + connect(m_control, + static_cast(&QLowEnergyController::error), + this, &sportstechelliptical::error); + connect(m_control, &QLowEnergyController::stateChanged, this, &sportstechelliptical::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; + } +} + +uint16_t sportstechelliptical::watts() { + if (currentCadence().value() == 0) { + return 0; + } + + return m_watt.value(); +} + +bool sportstechelliptical::connected() { + if (!m_control) { + return false; + } + return m_control->state() == QLowEnergyController::DiscoveredState; +} + +void sportstechelliptical::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(); + } +} + +uint16_t sportstechelliptical::wattsFromResistance(double resistance) { + // Coefficients from the polynomial regression + double intercept = 14.4968; + double b1 = -4.1878; + double b2 = -0.5051; + double b3 = 0.00387; + double b4 = 0.2392; + double b5 = 0.01108; + double cadence = Cadence.value(); + + // Calculate power using the polynomial equation + double power = intercept + + (b1 * resistance) + + (b2 * cadence) + + (b3 * resistance * resistance) + + (b4 * resistance * cadence) + + (b5 * cadence * cadence); + + return power; +} + +resistance_t sportstechelliptical::resistanceFromPowerRequest(uint16_t power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value(); + + if (Cadence.value() == 0) + return 1; + + for (resistance_t i = 1; i < maxResistance(); i++) { + if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) { + qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i) + << wattsFromResistance(i + 1) << power; + return i; + } + } + if (power < wattsFromResistance(1)) + return 1; + else + return maxResistance(); +} diff --git a/src/devices/sportstechelliptical/sportstechelliptical.h b/src/devices/sportstechelliptical/sportstechelliptical.h new file mode 100644 index 000000000..1c6e4128e --- /dev/null +++ b/src/devices/sportstechelliptical/sportstechelliptical.h @@ -0,0 +1,100 @@ +#ifndef SPORTSTECHelliptical_H +#define SPORTSTECHelliptical_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 "devices/elliptical.h" + +class sportstechelliptical : public elliptical { + Q_OBJECT + public: + sportstechelliptical(bool noWriteResistance, bool noHeartService, int8_t ellipticalResistanceOffset, + double ellipticalResistanceGain); + bool connected() override; + resistance_t maxResistance() override { return 24; } + resistance_t resistanceFromPowerRequest(uint16_t power) override; + + private: + double GetSpeedFromPacket(const QByteArray &packet); + double GetResistanceFromPacket(const QByteArray &packet); + double GetKcalFromPacket(const QByteArray &packet); + double GetDistanceFromPacket(QByteArray packet); + uint16_t GetElapsedFromPacket(const QByteArray &packet); + uint16_t wattsFromResistance(double resistance); + void forceResistance(resistance_t requestResistance); + void updateDisplay(uint16_t elapsed); + void btinit(bool startTape); + void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log, + bool wait_for_response); + void startDiscover(); + uint16_t watts() override; + double GetWattFromPacket(const QByteArray &packet); + double GetCadenceFromPacket(const QByteArray &packet); + + QTimer *refresh; + + bool noWriteResistance = false; + bool noHeartService = false; + int8_t ellipticalResistanceOffset = 4; + double ellipticalResistanceGain = 1.0; + + uint8_t firstVirtualelliptical = 0; + bool firstCharChanged = true; + QTime lastTimeCharChanged; + uint8_t sec1update = 0; + QByteArray lastPacket; + + QLowEnergyService *gattCommunicationChannelService = nullptr; + QLowEnergyCharacteristic gattWriteCharacteristic; + QLowEnergyCharacteristic gattNotify1Characteristic; + + bool initDone = false; + bool initRequest = false; + bool readyToStart = false; + + signals: + void disconnected(); + void debug(QString string); + 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); +}; + +#endif // SPORTSTECHelliptical_H diff --git a/src/devices/stagesbike/stagesbike.cpp b/src/devices/stagesbike/stagesbike.cpp index 5fcc5eb8c..d7de9aff4 100644 --- a/src/devices/stagesbike/stagesbike.cpp +++ b/src/devices/stagesbike/stagesbike.cpp @@ -120,7 +120,8 @@ void stagesbike::serviceDiscovered(const QBluetoothUuid &gatt) { QSettings settings; settings.setValue(QZSettings::ftms_bike, bluetoothDevice.name()); qDebug() << "forcing FTMS bike since it has FTMS"; - homeform::singleton()->setToastRequested("FTMS bike found, restart the app to apply the change!"); + if(homeform::singleton()) + homeform::singleton()->setToastRequested("FTMS bike found, restart the app to apply the change!"); } } diff --git a/src/devices/tacxneo2/tacxneo2.cpp b/src/devices/tacxneo2/tacxneo2.cpp index 4189ed1d2..c180aa3d0 100644 --- a/src/devices/tacxneo2/tacxneo2.cpp +++ b/src/devices/tacxneo2/tacxneo2.cpp @@ -141,7 +141,7 @@ void tacxneo2::update() { } else if((virtualBike && virtualBike->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100) { // in order to send the new gear value ASAP forceInclination(lastRawRequestedInclinationValue + gears()); // since this bike doesn't have the concept of resistance, - // i'm using the gears in the inclination + // i'm using the gears in the inclination } lastGearValue = gears(); @@ -262,7 +262,7 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris .toDouble(); Distance += ((Speed.value() / 3600000.0) * - ((double)lastRefreshCharacteristicChanged.msecsTo(now))); + ((double)lastRefreshCharacteristicChanged2A5B.msecsTo(now))); // Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | // (uint16_t)((uint8_t)newValue.at(index)))); debug("Current Resistance: " + @@ -296,10 +296,10 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris ((((0.048 * ((double)watts()) + 1.19) * settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / - (60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo( + (60000.0 / ((double)lastRefreshCharacteristicChanged2A5B.msecsTo( now)))); //(( (0.048* Output in watts +1.19) * body weight in // kg * 3.5) / 200 ) / 60 - lastRefreshCharacteristicChanged = now; + lastRefreshCharacteristicChanged2A5B = now; emit debug(QStringLiteral("Current CrankRevsRead: ") + QString::number(CrankRevsRead)); emit debug(QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime)); @@ -315,11 +315,175 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris 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; + bool crank_rev_present = 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(THINK_X) { + + if ((flags & 0x1) == 0x01) // Pedal Power Balance Present + { + index += 1; + } + if ((flags & 0x2) == 0x02) // Pedal Power Balance Reference + { + } + if ((flags & 0x4) == 0x04) // Accumulated Torque Present + { + index += 2; + } + if ((flags & 0x8) == 0x08) // Accumulated Torque Source + { + } + + if ((flags & 0x10) == 0x10) // Wheel Revolution Data Present + { + cadence_present = true; + wheel_revs = true; + } + + if ((flags & 0x20) == 0x20) // Crank Revolution Data Present + { + cadence_present = true; + crank_rev_present = true; + } + + if (cadence_present) { + if (wheel_revs && !crank_rev_present) { + time_division = 2048; + CrankRevs = + (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) | + ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | + ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index))); + index += 4; + + LastCrankEventTime = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + + index += 2; // wheel event time + + } else if (wheel_revs && crank_rev_present) { + index += 4; // wheel revs + index += 2; // wheel event time + } + + if (crank_rev_present) { + CrankRevs = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + index += 2; + + LastCrankEventTime = + (((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index))); + index += 2; + } + + int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime; + if (deltaT < 0) { + deltaT = LastCrankEventTime + time_division - oldLastCrankEventTime; + } + + if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name) + .toString() + .startsWith(QStringLiteral("Disabled"))) { + if (CrankRevs != oldCrankRevs && deltaT) { + double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * time_division * 60; + if (!crank_rev_present) + cadence = + cadence / + 2; // I really don't like this, there is no relationship between wheel rev and crank rev + if (cadence >= 0) { + Cadence = cadence; + } + lastGoodCadence = now; + } else if (lastGoodCadence.msecsTo(now) > 2000) { + Cadence = 0; + } + } + + qDebug() << QStringLiteral("Current Cadence: ") << Cadence.value() << CrankRevs << oldCrankRevs << deltaT + << time_division << LastCrankEventTime << oldLastCrankEventTime; + + oldLastCrankEventTime = LastCrankEventTime; + oldCrankRevs = CrankRevs; + + if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) { + Speed = Cadence.value() * settings + .value(QZSettings::cadence_sensor_speed_ratio, + QZSettings::default_cadence_sensor_speed_ratio) + .toDouble(); + } else { + Speed = metric::calculateSpeedFromPower( + watts(), Inclination.value(), Speed.value(), + fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit()); + } + emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value())); + + Distance += ((Speed.value() / 3600000.0) * + ((double)lastRefreshCharacteristicChangedPower.msecsTo(now))); + emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); + + // if we change this, also change the wattsFromResistance function. We can create a standard function in + // order to have all the costants in one place (I WANT MORE TIME!!!) + double ac = 0.01243107769; + double bc = 1.145964912; + double cc = -23.50977444; + + double ar = 0.1469553975; + double br = -5.841344538; + double cr = 97.62165482; + + double res = + (((sqrt(pow(br, 2.0) - 4.0 * ar * + (cr - (m_watt.value() * 132.0 / + (ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) - + br) / + (2.0 * ar)) * + settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) + + settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble(); + + if (isnan(res)) { + if (Cadence.value() > 0) { + // let's keep the last good value + } else { + m_pelotonResistance = 0; + } + } else { + m_pelotonResistance = res; + } + + qDebug() << QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value()); + + if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance) + .toBool()) + Resistance = pelotonToBikeResistance(m_pelotonResistance.value()); + else + Resistance = m_pelotonResistance; + emit resistanceRead(Resistance.value()); + qDebug() << QStringLiteral("Current Resistance Calculated: ") + QString::number(Resistance.value()); + + if (watts()) + KCal += + ((((0.048 * ((double)watts()) + 1.19) * + settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / + 200.0) / + (60000.0 / ((double)lastRefreshCharacteristicChangedPower.msecsTo( + now)))); //(( (0.048* Output in watts +1.19) * body weight + // in kg * 3.5) / 200 ) / 60 + emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value())); + + lastRefreshCharacteristicChangedPower = now; + } + } } else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2AD2)) { union flags { @@ -405,7 +569,7 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris } Distance += ((Speed.value() / 3600000.0) * - ((double)lastRefreshCharacteristicChanged.msecsTo(now))); + ((double)lastRefreshCharacteristicChanged2AD2.msecsTo(now))); emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value())); @@ -477,7 +641,7 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) / 200.0) / (60000.0 / - ((double)lastRefreshCharacteristicChanged.msecsTo( + ((double)lastRefreshCharacteristicChanged2AD2.msecsTo( now)))); //(( (0.048* Output in watts +1.19) * body weight in // kg * 3.5) / 200 ) / 60 } @@ -511,6 +675,8 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris if (Flags.remainingTime) { // todo } + + lastRefreshCharacteristicChanged2AD2 = now; } #ifdef Q_OS_ANDROID @@ -725,6 +891,10 @@ void tacxneo2::deviceDiscovered(const QBluetoothDeviceInfo &device) { device.address().toString() + ')'); { bluetoothDevice = device; + if(device.name().toUpper().startsWith(QStringLiteral("THINK X"))) { + THINK_X = true; + qDebug() << "THINK X workaround enabled!"; + } m_control = QLowEnergyController::createCentral(bluetoothDevice, this); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &tacxneo2::serviceDiscovered); diff --git a/src/devices/tacxneo2/tacxneo2.h b/src/devices/tacxneo2/tacxneo2.h index 35fb2c062..02d56077a 100644 --- a/src/devices/tacxneo2/tacxneo2.h +++ b/src/devices/tacxneo2/tacxneo2.h @@ -61,7 +61,9 @@ class tacxneo2 : public bike { uint8_t sec1Update = 0; QByteArray lastPacket; - QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime(); + QDateTime lastRefreshCharacteristicChanged2A5B = QDateTime::currentDateTime(); + QDateTime lastRefreshCharacteristicChanged2AD2 = QDateTime::currentDateTime(); + QDateTime lastRefreshCharacteristicChangedPower = QDateTime::currentDateTime(); QDateTime lastGoodCadence = QDateTime::currentDateTime(); uint8_t firstStateChanged = 0; @@ -78,6 +80,8 @@ class tacxneo2 : public bike { double lastGearValue = -1; bool resistance_received = false; + bool THINK_X = false; + #ifdef Q_OS_IOS lockscreen *h = 0; #endif diff --git a/src/devices/treadmill.h b/src/devices/treadmill.h index 29aa39cd2..d5dd17af5 100644 --- a/src/devices/treadmill.h +++ b/src/devices/treadmill.h @@ -80,8 +80,6 @@ class treadmill : public bluetoothdevice { double m_lastRawSpeedRequested = -1; double m_lastRawInclinationRequested = -100; bool instantaneousStrideLengthCMAvailableFromDevice = false; - metric RequestedPower; - int16_t requestPower = -1; treadmillErgTable _ergTable; void parseSpeed(double speed); diff --git a/src/devices/trxappgateusbbike/trxappgateusbbike.cpp b/src/devices/trxappgateusbbike/trxappgateusbbike.cpp index 80e585c35..93904e5ac 100644 --- a/src/devices/trxappgateusbbike/trxappgateusbbike.cpp +++ b/src/devices/trxappgateusbbike/trxappgateusbbike.cpp @@ -125,7 +125,7 @@ void trxappgateusbbike::update() { QSettings settings; bool toorx30 = settings.value(QZSettings::toorx_3_0, QZSettings::default_toorx_3_0).toBool(); if (toorx30 == false && - (bike_type == TYPE::IRUNNING || bike_type == TYPE::ICONSOLE || bike_type == TYPE::ICONSOLE_2 || + (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2 || bike_type == TYPE::ICONSOLE || bike_type == TYPE::ICONSOLE_2 || bike_type == TYPE::HERTZ_XR_770 || bike_type == TYPE::HERTZ_XR_770_2)) { const uint8_t noOpData[] = {0xf0, 0xa2, 0x01, 0x01, 0x94}; @@ -453,7 +453,7 @@ void trxappgateusbbike::btinit(bool startTape) { bool toorx30 = settings.value(QZSettings::toorx_3_0, QZSettings::default_toorx_3_0).toBool(); if (toorx30 == false && - (bike_type == TYPE::IRUNNING || bike_type == TYPE::ICONSOLE || bike_type == TYPE::ICONSOLE_2)) { + (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2 || bike_type == TYPE::ICONSOLE || bike_type == TYPE::ICONSOLE_2)) { const uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x01, 0x92}; const uint8_t initData2[] = {0xf0, 0xa1, 0x01, 0x01, 0x93}; @@ -462,23 +462,23 @@ void trxappgateusbbike::btinit(bool startTape) { const uint8_t initData5[] = {0xf0, 0xa6, 0x01, 0x01, 0x06, 0x9e}; writeCharacteristic((uint8_t *)initData1, sizeof(initData1), QStringLiteral("init"), false, true); - if (bike_type == TYPE::IRUNNING) { + if (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2) { QThread::msleep(400); } writeCharacteristic((uint8_t *)initData2, sizeof(initData2), QStringLiteral("init"), false, true); - if (bike_type == TYPE::IRUNNING) { + if (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2) { QThread::msleep(400); } writeCharacteristic((uint8_t *)initData3, sizeof(initData3), QStringLiteral("init"), false, true); - if (bike_type == TYPE::IRUNNING) { + if (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2) { QThread::msleep(400); } writeCharacteristic((uint8_t *)initData4, sizeof(initData4), QStringLiteral("init"), false, true); - if (bike_type == TYPE::IRUNNING) { + if (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2) { QThread::msleep(400); } writeCharacteristic((uint8_t *)initData5, sizeof(initData5), QStringLiteral("init"), false, true); - if (bike_type == TYPE::IRUNNING) { + if (bike_type == TYPE::IRUNNING || bike_type == TYPE::IRUNNING_2) { QThread::msleep(400); } } else if (bike_type == TYPE::HOP_SPORT_HS_090H) { @@ -849,7 +849,10 @@ void trxappgateusbbike::stateChanged(QLowEnergyService::ServiceState state) { Q_ASSERT(gattNotify1Characteristic.isValid()); if (bike_type == TYPE::IRUNNING || bike_type == TYPE::CHANGYOW) { if (!gattNotify2Characteristic.isValid()) { - bike_type = TYPE::ICONSOLE; + if(bike_type == TYPE::IRUNNING_2) + bike_type = TYPE::ICONSOLE_2; + else + bike_type = TYPE::ICONSOLE; qDebug() << QStringLiteral("ICONSOLE bike found - overrode due to characteristics"); } } @@ -1019,6 +1022,20 @@ void trxappgateusbbike::serviceScanDone(void) { bike_type = TUNTURI_2; uuid = uuid3; } + } else if (bike_type == IRUNNING) { + + bool found = false; + foreach (QBluetoothUuid s, m_control->services()) { + + if (s == QBluetoothUuid::fromString(uuid)) { + found = true; + break; + } + } + if (!found) { + bike_type = IRUNNING_2; + uuid = uuid3; + } } QBluetoothUuid _gattCommunicationChannelServiceId(uuid); diff --git a/src/devices/trxappgateusbbike/trxappgateusbbike.h b/src/devices/trxappgateusbbike/trxappgateusbbike.h index f1f9f1aea..e568b070f 100644 --- a/src/devices/trxappgateusbbike/trxappgateusbbike.h +++ b/src/devices/trxappgateusbbike/trxappgateusbbike.h @@ -112,6 +112,7 @@ class trxappgateusbbike : public bike { REEBOK_2 = 23, BIKZU = 24, TOORX_SRX_500 = 25, + IRUNNING_2 = 26, } TYPE; TYPE bike_type = TRXAPPGATE; diff --git a/src/devices/trxappgateusbtreadmill/trxappgateusbtreadmill.cpp b/src/devices/trxappgateusbtreadmill/trxappgateusbtreadmill.cpp index 6f34518e7..f3f1c280a 100644 --- a/src/devices/trxappgateusbtreadmill/trxappgateusbtreadmill.cpp +++ b/src/devices/trxappgateusbtreadmill/trxappgateusbtreadmill.cpp @@ -654,7 +654,7 @@ void trxappgateusbtreadmill::characteristicWritten(const QLowEnergyCharacteristi } void trxappgateusbtreadmill::serviceScanDone(void) { - emit debug(QStringLiteral("serviceScanDone")); + qDebug() << QStringLiteral("serviceScanDone") << treadmill_type; QString uuid = QStringLiteral("0000fff0-0000-1000-8000-00805f9b34fb"); if (treadmill_type == TYPE::IRUNNING || treadmill_type == TYPE::REEBOK || treadmill_type == TYPE::DKN_2) { diff --git a/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp b/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp index a783ae594..caa489acc 100644 --- a/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp +++ b/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp @@ -1,5 +1,5 @@ #include "wahookickrsnapbike.h" - +#include "homeform.h" #include "virtualdevices/virtualbike.h" #include #include @@ -721,6 +721,11 @@ void wahookickrsnapbike::serviceScanDone(void) { connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this, &wahookickrsnapbike::stateChanged); gattCommunicationChannelService.constLast()->discoverDetails(); + if(s == QBluetoothUuid((quint16)0x1826)) { + qDebug() << "if it doesn't change the inclination, set the bike in the FTMS bike setting under the Bike settings."; + if(homeform::singleton()) + homeform::singleton()->setToastRequested("if it doesn't change the inclination, set the bike in the FTMS bike setting under the Bike settings."); + } } } @@ -816,7 +821,10 @@ void wahookickrsnapbike::inclinationChanged(double grade, double percentage) { Q_UNUSED(percentage); lastGrade = grade; emit debug(QStringLiteral("writing inclination ") + QString::number(grade)); - QByteArray a = setSimGrade(grade + gears()); + QSettings settings; + double g = grade; + g += gears(); + QByteArray a = setSimGrade(g); uint8_t b[20]; memcpy(b, a.constData(), a.length()); writeCharacteristic(b, a.length(), "setSimGrade", false, true); @@ -824,4 +832,4 @@ void wahookickrsnapbike::inclinationChanged(double grade, double percentage) { bool wahookickrsnapbike::inclinationAvailableByHardware() { return KICKR_BIKE; -} \ No newline at end of file +} diff --git a/src/ergtable.h b/src/ergtable.h index c4ebd25d0..b47e0aa78 100644 --- a/src/ergtable.h +++ b/src/ergtable.h @@ -160,7 +160,7 @@ class ergTable : public QObject { uint16_t wattage = fields[1].toUInt(); uint16_t resistance = fields[2].toUInt(); - qDebug() << "inputs.append(ergDataPoint(" << cadence << ", " << wattage << ", "<< resistance << "));"; + //qDebug() << "inputs.append(ergDataPoint(" << cadence << ", " << wattage << ", "<< resistance << "));"; dataTable.append(ergDataPoint(cadence, wattage, resistance)); } diff --git a/src/homeform.cpp b/src/homeform.cpp index e16f183a2..7bcf5ab7e 100644 --- a/src/homeform.cpp +++ b/src/homeform.cpp @@ -5,6 +5,7 @@ #include "localipaddress.h" #ifdef Q_OS_ANDROID #include "keepawakehelper.h" +#include #include #endif #include "material.h" @@ -276,6 +277,10 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { QStringLiteral("0"), false, QStringLiteral("strokes_length"), 48, labelFontSize); gears = new DataObject(QStringLiteral("Gears"), QStringLiteral("icons/icons/elevationgain.png"), QStringLiteral("0"), true, QStringLiteral("gears"), 48, labelFontSize); + biggearsPlus = new DataObject(QStringLiteral("GearsPlus"), QStringLiteral("icons/icons/elevationgain.png"), + QStringLiteral("0"), true, QStringLiteral("biggearsplus"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Gear +", QStringLiteral("red")); + biggearsMinus = new DataObject(QStringLiteral("GearsMinus"), QStringLiteral("icons/icons/elevationgain.png"), + QStringLiteral("0"), true, QStringLiteral("biggearsminus"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Gear -", QStringLiteral("green")); pidHR = new DataObject(QStringLiteral("PID Heart"), QStringLiteral("icons/icons/heart_red.png"), QStringLiteral("0"), true, QStringLiteral("pid_hr"), 48, labelFontSize); extIncline = new DataObject(QStringLiteral("Ext.Inclin.(%)"), QStringLiteral("icons/icons/inclination.png"), @@ -560,6 +565,14 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { } } #ifdef Q_OS_ANDROID + + QString bluetoothName = getBluetoothName(); + qDebug() << "getBluetoothName()" << bluetoothName; + + if(bluetoothName.length() > 4) { + setToastRequested("Bluetooth name too long, change it to a 4 letters one in the android settings"); + } + // Android 14 restrics access to /Android/data folder bool android_documents_folder = settings.value(QZSettings::android_documents_folder, QZSettings::default_android_documents_folder).toBool(); if (android_documents_folder || QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::Android, 14)) { @@ -681,11 +694,45 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) { QAndroidJniObject javaPath = QAndroidJniObject::fromString(getWritableAppDir()); QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/Shortcuts", "createShortcutsForFiles", "(Ljava/lang/String;Landroid/content/Context;)V", javaPath.object(), QtAndroid::androidContext().object()); + + QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/MediaButtonReceiver", + "registerReceiver", + "(Landroid/content/Context;)V", + QtAndroid::androidContext().object()); #endif bluetoothManager->homeformLoaded = true; } +#ifdef Q_OS_ANDROID +extern "C" { +JNIEXPORT void JNICALL + Java_org_cagnulen_qdomyoszwift_MediaButtonReceiver_nativeOnMediaButtonEvent(JNIEnv *env, jobject obj, jint prev, jint current, jint max) { + qDebug() << "Media button event: current =" << current << "max =" << max << "prev =" << prev; + static QDateTime volumeLastChange = QDateTime::currentDateTime(); + QSettings settings; + bool gears_volume_debouncing = settings.value(QZSettings::gears_volume_debouncing, QZSettings::default_gears_volume_debouncing).toBool(); + + if (!settings.value(QZSettings::volume_change_gears, QZSettings::default_volume_change_gears).toBool()) { + qDebug() << "volume_change_gears disabled!"; + return; + } + + if(gears_volume_debouncing && volumeLastChange.msecsTo(QDateTime::currentDateTime()) < 500) { + qDebug() << "volume debouncing"; + return; + } + + if(prev > current) + homeform::singleton()->Minus(QStringLiteral("gears")); + else + homeform::singleton()->Plus(QStringLiteral("gears")); + + volumeLastChange = QDateTime::currentDateTime(); + } +} +#endif + void homeform::setActivityDescription(QString desc) { activityDescription = desc; } void homeform::chartSaved(QString fileName) { @@ -856,8 +903,7 @@ void homeform::pelotonLoginState(bool ok) { m_pelotonLoginState = (ok ? 1 : 0); emit pelotonLoginChanged(m_pelotonLoginState); if (!ok) { - setToastRequested("Peloton Login Error!"); - emit toastRequestedChanged(toastRequested()); + setToastRequested("Peloton Login Error!"); } } @@ -867,7 +913,6 @@ void homeform::zwiftLoginState(bool ok) { emit zwiftLoginChanged(m_zwiftLoginState); if (!ok) { setToastRequested("Zwift Login Error!"); - emit toastRequestedChanged(toastRequested()); } } @@ -1004,6 +1049,7 @@ void homeform::trainProgramSignals() { if (bluetoothManager->device()) { disconnect(trainProgram, &trainprogram::start, bluetoothManager->device(), &bluetoothdevice::start); disconnect(trainProgram, &trainprogram::stop, bluetoothManager->device(), &bluetoothdevice::stop); + disconnect(trainProgram, &trainprogram::stop, this, &homeform::StopFromTrainProgram); disconnect(trainProgram, &trainprogram::lap, this, &homeform::Lap); disconnect(trainProgram, &trainprogram::changeSpeed, ((treadmill *)bluetoothManager->device()), &treadmill::changeSpeed); @@ -1053,6 +1099,7 @@ void homeform::trainProgramSignals() { connect(trainProgram, &trainprogram::start, bluetoothManager->device(), &bluetoothdevice::start); connect(trainProgram, &trainprogram::stop, bluetoothManager->device(), &bluetoothdevice::stop); + connect(trainProgram, &trainprogram::stop, this, &homeform::StopFromTrainProgram); connect(trainProgram, &trainprogram::lap, this, &homeform::Lap); connect(trainProgram, &trainprogram::toastRequest, this, &homeform::onToastRequested); if (bluetoothManager->device()->deviceType() == bluetoothdevice::TREADMILL) { @@ -1130,7 +1177,6 @@ void homeform::trainProgramSignals() { void homeform::onToastRequested(QString message) { setToastRequested(message); - emit toastRequestedChanged(message); } QStringList homeform::tile_order() { @@ -1814,6 +1860,18 @@ void homeform::sortTiles() { ergMode->setGridId(i); dataList.append(ergMode); } + + if (settings.value(QZSettings::tile_biggears_enabled, false).toBool() && + settings.value(QZSettings::tile_biggears_order, 54).toInt() == i) { + biggearsPlus->setGridId(i); + dataList.append(biggearsPlus); + } + + if (settings.value(QZSettings::tile_biggears_enabled, false).toBool() && + settings.value(QZSettings::tile_biggears_order, 54).toInt() + 1 == i) { + biggearsMinus->setGridId(i); + dataList.append(biggearsMinus); + } } } else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) { for (int i = 0; i < 100; i++) { @@ -3107,6 +3165,12 @@ void homeform::LargeButton(const QString &name) { .toDouble()); } } + + if(name.contains(QStringLiteral("biggearsplus"))) { + gearUp(); + } else if(name.contains(QStringLiteral("biggearsminus"))) { + gearDown(); + } } void homeform::Plus(const QString &name) { @@ -3716,6 +3780,10 @@ void homeform::StopRequested() { emit stopRequestedChanged(m_stopRequested); } +void homeform::StopFromTrainProgram(bool paused) { + Stop(); +} + void homeform::Stop() { QSettings settings; @@ -4097,16 +4165,28 @@ void homeform::update() { static double volumeLast = -1; double currentVolume = h.getVolume() * 10.0; qDebug() << "volume" << volumeLast << currentVolume; + QSettings settings; + bool gears_volume_debouncing = settings.value(QZSettings::gears_volume_debouncing, QZSettings::default_gears_volume_debouncing).toBool(); if (volumeLast == -1) qDebug() << "volume init"; else if (volumeLast > currentVolume) { double diff = volumeLast - currentVolume; - for (int i = 0; i < diff; i++) + for (int i = 0; i < diff; i++) { Minus(QStringLiteral("gears")); + if(gears_volume_debouncing) { + i = diff; + break; + } + } } else if (volumeLast < currentVolume) { double diff = currentVolume - volumeLast; - for (int i = 0; i < diff; i++) + for (int i = 0; i < diff; i++) { Plus(QStringLiteral("gears")); + if(gears_volume_debouncing) { + i = diff; + break; + } + } } volumeLast = currentVolume; } @@ -4312,7 +4392,8 @@ void homeform::update() { this->target_power->setValue( QString::number(((bike *)bluetoothManager->device())->lastRequestedPower().value(), 'f', 0)); this->resistance->setValue(QString::number(resistance, 'f', 0)); - if (settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble() == 1.0) + bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool(); + if (settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble() == 1.0 || gears_zwift_ratio) this->gears->setValue(QString::number(((bike *)bluetoothManager->device())->gears())); else this->gears->setValue(QString::number(((bike *)bluetoothManager->device())->gears(), 'f', 1)); @@ -5453,7 +5534,6 @@ void homeform::update() { qDebug() << QStringLiteral("Autolap based on distance"); Lap(); setToastRequested("AutoLap " + QString::number(settings.value(QZSettings::autolap_distance, QZSettings::default_autolap_distance).toDouble(), 'f', 1)); - emit toastRequestedChanged(toastRequested()); } } @@ -6164,7 +6244,6 @@ void homeform::strava_refreshtoken() { if (reply->error() != 0) { qDebug() << QStringLiteral("Got error") << reply->errorString().toStdString().c_str(); setToastRequested("Strava Auth Failed!"); - emit toastRequestedChanged(toastRequested()); return; } @@ -6188,7 +6267,6 @@ void homeform::strava_refreshtoken() { 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) { @@ -6306,7 +6384,6 @@ 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() { @@ -6321,7 +6398,6 @@ void homeform::writeFileCompleted() { qDebug() << "reply:" << response; setToastRequested("Strava Upload Completed!"); - emit toastRequestedChanged(toastRequested()); } void homeform::onStravaGranted() { @@ -6929,6 +7005,27 @@ void homeform::sendMail() { } #if defined(Q_OS_ANDROID) + +QString homeform::getBluetoothName() +{ + QAndroidJniObject bluetoothAdapter = QAndroidJniObject::callStaticObjectMethod( + "android/bluetooth/BluetoothAdapter", + "getDefaultAdapter", + "()Landroid/bluetooth/BluetoothAdapter;"); + + if (bluetoothAdapter.isValid()) { + QAndroidJniObject name = bluetoothAdapter.callObjectMethod( + "getName", + "()Ljava/lang/String;"); + + if (name.isValid()) { + return name.toString(); + } + } + + return QString(); +} + QString homeform::getAndroidDataAppDir() { static QString path = ""; diff --git a/src/homeform.h b/src/homeform.h index b3dc11eb6..ca1c572fa 100644 --- a/src/homeform.h +++ b/src/homeform.h @@ -350,7 +350,22 @@ class homeform : public QObject { Q_INVOKABLE bool firstRun() { QSettings settings; - return settings.value(QZSettings::bluetooth_lastdevice_name, QZSettings::default_bluetooth_lastdevice_name).toString().isEmpty(); + 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(); + QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString(); + bool fake_bike = + 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 antbike = + settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool(); + + return settings.value(QZSettings::bluetooth_lastdevice_name, QZSettings::default_bluetooth_lastdevice_name).toString().isEmpty() && + nordictrack_2950_ip.isEmpty() && tdf_10_ip.isEmpty() && !fake_bike && !fakedevice_elliptical && !fakedevice_rower && !fakedevice_treadmill && !antbike && proform_elliptical_ip.isEmpty(); } @@ -474,7 +489,7 @@ class homeform : public QObject { void videoSeekPosition(int ms); // in realtime void setVideoRate(double rate); void setMapsVisible(bool value); - void setToastRequested(QString value) { m_toastRequested = value; } + void setToastRequested(QString value) { m_toastRequested = value; emit toastRequestedChanged(value); } void setStravaUploadRequested(bool value) { m_stravaUploadRequested = value; } @@ -483,6 +498,7 @@ class homeform : public QObject { int preview_workout_points(); #if defined(Q_OS_ANDROID) + QString getBluetoothName(); static QString getAndroidDataAppDir(); #endif Q_INVOKABLE static QString getWritableAppDir(); @@ -623,6 +639,8 @@ class homeform : public QObject { DataObject *strokesCount; DataObject *wattKg; DataObject *gears; + DataObject *biggearsPlus; + DataObject *biggearsMinus; DataObject *remaningTimeTrainingProgramCurrentRow; DataObject *nextRows; DataObject *mets; @@ -782,15 +800,16 @@ class homeform : public QObject { void saveProfile(QString profilename); void restart(); bool pelotonAskStart() { return m_pelotonAskStart; } + void Minus(const QString &); + void Plus(const QString &); private slots: void Start(); void Stop(); + void StopFromTrainProgram(bool paused); void StartRequested(); void StopRequested(); void Lap(); - void Minus(const QString &); - void Plus(const QString &); void LargeButton(const QString &); void volumeDown(); void volumeUp(); diff --git a/src/ios/GarminConnect.swift b/src/ios/GarminConnect.swift index eedf5de4a..54bbe4600 100644 --- a/src/ios/GarminConnect.swift +++ b/src/ios/GarminConnect.swift @@ -26,6 +26,14 @@ extension ConnectIQ { return v.FootCad; } + @objc public func getPower() -> Int { + return v.Power; + } + + @objc public func getSpeed() -> Double { + return v.Speed; + } + @objc public func urlParser(_ url: URL) { v.urlParser(url) } @@ -45,6 +53,8 @@ class GarminConnectSwift: NSObject, IQDeviceEventDelegate, IQAppMessageDelegate public var HR: Int = 0 public var FootCad: Int = 0 + public var Power: Int = 0 + public var Speed: Double = 0 let SwiftDebug = swiftDebug() @@ -126,8 +136,12 @@ class GarminConnectSwift: NSObject, IQDeviceEventDelegate, IQAppMessageDelegate print(dictionary) HR = dictionary[0] as? Int ?? 0 FootCad = dictionary[1] as? Int ?? 0 - print("Garmin HR: \(HR)") - print("Garmin Foot Cadence: \(FootCad)") + Power = dictionary[2] as? Int ?? 0 + Speed = dictionary[3] as? Double ?? 0 + SwiftDebug.qtDebug("Garmin HR: \(HR)") + SwiftDebug.qtDebug("Garmin Foot Cadence: \(FootCad)") + SwiftDebug.qtDebug("Garmin Power: \(Power)") + SwiftDebug.qtDebug("Garmin Speed: \(Speed)") } } diff --git a/src/ios/lockscreen.h b/src/ios/lockscreen.h index c8dc15e83..4b96c561d 100644 --- a/src/ios/lockscreen.h +++ b/src/ios/lockscreen.h @@ -56,6 +56,8 @@ class lockscreen { void garminconnect_init(); int getHR(); int getFootCad(); + int getPower(); + double getSpeed(); // debug static void debug(const char* debugstring); diff --git a/src/ios/lockscreen.mm b/src/ios/lockscreen.mm index 9120f2a29..ba942f125 100644 --- a/src/ios/lockscreen.mm +++ b/src/ios/lockscreen.mm @@ -259,6 +259,14 @@ return [Garmin getFootCad]; } +int lockscreen::getPower() { + return [Garmin getPower]; +} + +double lockscreen::getSpeed() { + return [Garmin getSpeed]; +} + // getVolume double lockscreen::getVolume() diff --git a/src/main.cpp b/src/main.cpp index 987aa291e..ce25af309 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #ifdef Q_OS_LINUX #ifndef Q_OS_ANDROID #include // getuid +#include "EventHandler.h" #endif #endif #include @@ -67,6 +68,7 @@ bool bike_wheel_revs = false; bool run_cadence_sensor = false; bool nordictrack_10_treadmill = false; bool reebok_fr30_treadmill = false; +QString eventGearDevice = QStringLiteral(""); QString trainProgram; QString deviceName = QLatin1String(""); uint32_t pollDeviceTime = 200; @@ -148,6 +150,10 @@ QCoreApplication *createApplication(int &argc, char *argv[]) { deviceName = argv[++i]; } + if (!qstrcmp(argv[i], "-bluetooth-event-gear-device")) { + + eventGearDevice = argv[++i]; + } if (!qstrcmp(argv[i], "-peloton-username")) { peloton_username = argv[++i]; @@ -399,7 +405,7 @@ int main(int argc, char *argv[]) { qInstallMessageHandler(myMessageOutput); qDebug() << QStringLiteral("version ") << app->applicationVersion(); foreach (QString s, settings.allKeys()) { - if (!s.contains(QStringLiteral("password")) && !s.contains("user_email")) { + if (!s.contains(QStringLiteral("password")) && !s.contains("user_email") && !s.contains("username")) { qDebug() << s << settings.value(s); } @@ -642,6 +648,13 @@ int main(int argc, char *argv[]) { } else { // start non-GUI version... } + +#ifdef Q_OS_LINUX +#ifndef Q_OS_ANDROID + if(eventGearDevice.length()) + new BluetoothHandler(&bl, eventGearDevice); +#endif +#endif return app->exec(); #endif } diff --git a/src/main.qml b/src/main.qml index 7c258f052..bfa7ee5be 100644 --- a/src/main.qml +++ b/src/main.qml @@ -777,7 +777,7 @@ ApplicationWindow { } ItemDelegate { - text: "version 2.16.67" + text: "version 2.17.0" width: parent.width } diff --git a/src/peloton.cpp b/src/peloton.cpp index 10a505c85..253e5ae4c 100644 --- a/src/peloton.cpp +++ b/src/peloton.cpp @@ -1013,6 +1013,73 @@ void peloton::ride_onfinish(QNetworkReply *reply) { } } + QJsonObject target_metrics_data_list = ride[QStringLiteral("target_metrics_data")].toObject(); + if (trainrows.empty() && !target_metrics_data_list.isEmpty() && + bluetoothManager->device()->deviceType() != bluetoothdevice::ROWING && + bluetoothManager->device()->deviceType() != bluetoothdevice::TREADMILL) { + QJsonArray target_metrics = target_metrics_data_list["target_metrics"].toArray(); + for (const QJsonValue& segment : target_metrics) { + QJsonObject segmentObj = segment.toObject(); + QJsonObject offsets = segmentObj["offsets"].toObject(); + QJsonArray metrics = segmentObj["metrics"].toArray(); + + trainrow r; + int start = offsets["start"].toInt(); + int end = offsets["end"].toInt(); + r.duration = QTime(0, 0, 0).addSecs(end - start); + if (!metrics.isEmpty()) { + QJsonObject metric = metrics[0].toObject(); + int lower = metric["lower"].toInt(); + int upper = metric["upper"].toInt(); + int avg = (upper - lower) / 2; + + int p = lower; + + if (difficulty == QStringLiteral("average")) { + p = avg; + } else if (difficulty == QStringLiteral("upper")) { + p = upper; + } else { // lower + p = lower; + } + + switch(p) { + case 1: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.50; + break; + case 2: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.66; + break; + case 3: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.83; + break; + case 4: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 0.98; + break; + case 5: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 1.13; + break; + case 6: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 1.35; + break; + case 7: + r.power = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble() * 1.5; + break; + default: + qDebug() << "ERROR not handled!" << p; + break; + } + atLeastOnePower = true; + trainrows.append(r); + qDebug() << r.duration << "power" << r.power; + } + } + // this list doesn't have nothing useful for this session + if (!atLeastOnePower) { + trainrows.clear(); + } + } + if (log_request) { qDebug() << "peloton::ride_onfinish" << trainrows.length() << ride; } else { diff --git a/src/qdomyos-zwift.pri b/src/qdomyos-zwift.pri index 45beb3982..95f84d91f 100755 --- a/src/qdomyos-zwift.pri +++ b/src/qdomyos-zwift.pri @@ -75,9 +75,13 @@ DEFINES += QT_DEPRECATED_WARNINGS IO_UNDER_QT SMTP_BUILD NOMINMAX # include(../qtzeroconf/qtzeroconf.pri) SOURCES += \ + $$PWD/devices/antbike/antbike.cpp \ $$PWD/devices/crossrope/crossrope.cpp \ + $$PWD/devices/deeruntreadmill/deerruntreadmill.cpp \ $$PWD/devices/focustreadmill/focustreadmill.cpp \ $$PWD/devices/jumprope.cpp \ + $$PWD/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.cpp \ + $$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \ $$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.cpp \ QTelnet.cpp \ devices/bkoolbike/bkoolbike.cpp \ @@ -298,9 +302,14 @@ INCLUDEPATH += fit-sdk/ devices/ HEADERS += \ $$PWD/CRC16IBM.h \ + $$PWD/EventHandler.h \ + $$PWD/devices/antbike/antbike.h \ $$PWD/devices/crossrope/crossrope.h \ + $$PWD/devices/deeruntreadmill/deerruntreadmill.h \ $$PWD/devices/focustreadmill/focustreadmill.h \ $$PWD/devices/jumprope.h \ + $$PWD/devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h \ + $$PWD/devices/sportstechelliptical/sportstechelliptical.h \ $$PWD/devices/trxappgateusbelliptical/trxappgateusbelliptical.h \ $$PWD/ergtable.h \ $$PWD/treadmillErgTable.h \ @@ -755,6 +764,7 @@ DISTFILES += \ $$PWD/android/src/HidBridge.java \ $$PWD/android/src/IQMessageReceiverWrapper.java \ $$PWD/android/src/LocationHelper.java \ + $$PWD/android/src/MediaButtonReceiver.java \ $$PWD/android/src/MediaProjection.java \ $$PWD/android/src/NotificationUtils.java \ $$PWD/android/src/ScreenCaptureService.java \ @@ -858,4 +868,4 @@ INCLUDEPATH += purchasing/inapp WINRT_MANIFEST = AppxManifest.xml -VERSION = 2.16.67 +VERSION = 2.17.0 diff --git a/src/qzsettings.cpp b/src/qzsettings.cpp index 0277cd570..34ec895e0 100644 --- a/src/qzsettings.cpp +++ b/src/qzsettings.cpp @@ -748,8 +748,23 @@ const QString QZSettings::atletica_lightspeed_treadmill = QStringLiteral("atleti const QString QZSettings::peloton_treadmill_level = QStringLiteral("peloton_treadmill_level"); const QString QZSettings::nordictrackadbbike_resistance = QStringLiteral("nordictrackadbbike_resistance"); const QString QZSettings::proform_treadmill_carbon_t7 = QStringLiteral("proform_treadmill_carbon_t7"); +const QString QZSettings::nordictrack_treadmill_exp_5i = QStringLiteral("nordictrack_treadmill_exp_5i"); +const QString QZSettings::dircon_id = QStringLiteral("dircon_id"); +const QString QZSettings::proform_elliptical_ip = QStringLiteral("proform_elliptical_ip"); +const QString QZSettings::default_proform_elliptical_ip = QStringLiteral(""); +const QString QZSettings::antbike = QStringLiteral("antbike"); +const QString QZSettings::domyosbike_notfmts = QStringLiteral("domyosbike_notfmts"); +const QString QZSettings::gears_volume_debouncing = QStringLiteral("gears_volume_debouncing"); +const QString QZSettings::tile_biggears_enabled = QStringLiteral("tile_biggears_enabled"); +const QString QZSettings::tile_biggears_order = QStringLiteral("tile_biggears_order"); +const QString QZSettings::domyostreadmill_notfmts = QStringLiteral("domyostreadmill_notfmts"); +const QString QZSettings::zwiftplay_swap = QStringLiteral("zwiftplay_swap"); +const QString QZSettings::gears_zwift_ratio = QStringLiteral("gears_zwift_ratio"); +const QString QZSettings::domyos_bike_500_profile_v2 = QStringLiteral("domyos_bike_500_profile_v2"); +const QString QZSettings::gears_offset = QStringLiteral("gears_offset"); +const QString QZSettings::proform_carbon_tl_PFTL59720 = QStringLiteral("proform_carbon_tl_PFTL59720"); -const uint32_t allSettingsCount = 632; +const uint32_t allSettingsCount = 646; QVariant allSettings[allSettingsCount][2] = { {QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles}, @@ -1388,6 +1403,20 @@ QVariant allSettings[allSettingsCount][2] = { {QZSettings::peloton_treadmill_level, QZSettings::default_peloton_treadmill_level}, {QZSettings::nordictrackadbbike_resistance, QZSettings::default_nordictrackadbbike_resistance}, {QZSettings::proform_treadmill_carbon_t7, QZSettings::default_proform_treadmill_carbon_t7}, + {QZSettings::nordictrack_treadmill_exp_5i, QZSettings::default_nordictrack_treadmill_exp_5i}, + {QZSettings::dircon_id, QZSettings::default_dircon_id}, + {QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip}, + {QZSettings::antbike, QZSettings::default_antbike}, + {QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts}, + {QZSettings::gears_volume_debouncing, QZSettings::default_gears_volume_debouncing}, + {QZSettings::tile_biggears_enabled, QZSettings::default_tile_biggears_enabled}, + {QZSettings::tile_biggears_order, QZSettings::default_tile_biggears_order}, + {QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts}, + {QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap}, + {QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio}, + {QZSettings::domyos_bike_500_profile_v2, QZSettings::default_domyos_bike_500_profile_v2}, + {QZSettings::gears_offset, QZSettings::default_gears_offset}, + {QZSettings::proform_carbon_tl_PFTL59720, QZSettings::default_proform_carbon_tl_PFTL59720}, }; void QZSettings::qDebugAllSettings(bool showDefaults) { diff --git a/src/qzsettings.h b/src/qzsettings.h index 33645dedc..d420f6503 100644 --- a/src/qzsettings.h +++ b/src/qzsettings.h @@ -2096,6 +2096,48 @@ class QZSettings { static const QString proform_treadmill_carbon_t7; static constexpr bool default_proform_treadmill_carbon_t7 = false; + static const QString nordictrack_treadmill_exp_5i; + static constexpr bool default_nordictrack_treadmill_exp_5i = false; + + static const QString dircon_id; + static constexpr int default_dircon_id = 0; + + static const QString proform_elliptical_ip; + static const QString default_proform_elliptical_ip; + + static const QString antbike; + static constexpr bool default_antbike = false; + + static const QString domyosbike_notfmts; + static constexpr bool default_domyosbike_notfmts = false; + + static const QString gears_volume_debouncing; + static constexpr bool default_gears_volume_debouncing = false; + + static const QString tile_biggears_enabled; + static constexpr bool default_tile_biggears_enabled = false; + + static const QString tile_biggears_order; + static constexpr int default_tile_biggears_order = 54; + + static const QString domyostreadmill_notfmts; + static constexpr bool default_domyostreadmill_notfmts = false; + + static const QString zwiftplay_swap; + static constexpr bool default_zwiftplay_swap = false; + + static const QString gears_zwift_ratio; + static constexpr bool default_gears_zwift_ratio = false; + + static const QString domyos_bike_500_profile_v2; + static constexpr bool default_domyos_bike_500_profile_v2 = false; + + static const QString gears_offset; + static constexpr double default_gears_offset = 0.0; + + static const QString proform_carbon_tl_PFTL59720; + static constexpr bool default_proform_carbon_tl_PFTL59720 = 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/settings-tiles.qml b/src/settings-tiles.qml index 6705b8509..31d4c80cc 100644 --- a/src/settings-tiles.qml +++ b/src/settings-tiles.qml @@ -193,6 +193,8 @@ ScrollView { property int tile_erg_mode_order: 52 property bool tile_rss_enabled: false property int tile_rss_order: 53 + property bool tile_biggears_enabled: false + property int tile_biggears_order: 54 } @@ -236,7 +238,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -302,7 +304,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -343,7 +345,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -420,7 +422,7 @@ ScrollView { text: qsTr("Estimated calories burned during session, calculated on weight, age, and watts.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -465,7 +467,7 @@ ScrollView { text: qsTr("Estimated distance traveled during the session.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -510,7 +512,7 @@ ScrollView { text: qsTr("Current pace per mile or kilometer (Treadmill, Elliptical and Rower)") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -555,7 +557,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -600,7 +602,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -645,7 +647,7 @@ ScrollView { text: qsTr("Estimation of weight loss during the session.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -691,7 +693,7 @@ ScrollView { text: qsTr("Average watts produced for the session.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -769,7 +771,7 @@ ScrollView { text: qsTr("Percentage of current FTP and current FTP zone.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -846,7 +848,7 @@ ScrollView { text: qsTr("Built-in treadmill fan speed (Treadmill only)") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -891,7 +893,7 @@ ScrollView { text: qsTr("Cumulative power produced during the session in kilojoules.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -936,7 +938,7 @@ ScrollView { text: qsTr("Total time from start of the session.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -981,7 +983,7 @@ ScrollView { text: qsTr("Total time moving during the session.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1026,7 +1028,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1071,7 +1073,7 @@ ScrollView { text: qsTr("Displays time remaining in Peloton class.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1197,7 +1199,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1242,7 +1244,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1288,7 +1290,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1333,7 +1335,7 @@ ScrollView { text: qsTr("Displays target cadence.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1378,7 +1380,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1424,7 +1426,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1564,7 +1566,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1609,7 +1611,49 @@ 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } + + AccordionCheckElement { + title: qsTr("Gears Big Buttons") + linkedBoolSetting: "tile_biggears_enabled" + settings: settings + accordionContent: RowLayout { + spacing: 10 + Label { + text: qsTr("order index:") + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + ComboBox { + id: biggearsOrderTextField + model: rootItem.tile_order + displayText: settings.tile_biggears_order + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onActivated: { + displayText = biggearsOrderTextField.currentValue + } + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: {settings.tile_biggears_order = biggearsOrderTextField.displayText; toast.show("Setting saved!"); } + } + } + } + + Label { + text: qsTr("It shows 2 big gear buttons on the UI") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1654,7 +1698,7 @@ ScrollView { text: qsTr("Displays the time remaining until the next cadence and/or resistance interval.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1699,7 +1743,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1744,7 +1788,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1821,7 +1865,7 @@ ScrollView { text: qsTr("Displays the current time.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1866,7 +1910,7 @@ ScrollView { text: qsTr("(Rower only) Displays the number of strokes rowed.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1911,7 +1955,7 @@ ScrollView { text: qsTr("(Rower only) Displays the stroke length.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1956,7 +2000,7 @@ ScrollView { text: qsTr("(Elite Rizer only) Displays steering angle.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2001,7 +2045,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2046,7 +2090,7 @@ ScrollView { text: qsTr("(Elite Rizer only) Allows control of the incline of external inclination equipment.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2091,7 +2135,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2136,7 +2180,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2181,7 +2225,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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter diff --git a/src/settings.qml b/src/settings.qml index 319929fbe..8f1055e34 100644 --- a/src/settings.qml +++ b/src/settings.qml @@ -960,6 +960,24 @@ import QtQuick.Dialogs 1.0 property int peloton_treadmill_level: 1 property bool nordictrackadbbike_resistance: false property bool proform_treadmill_carbon_t7: false + property bool nordictrack_treadmill_exp_5i: false + property int dircon_id: 0 + property string proform_elliptical_ip: "" + + // from version 2.16.69 + property bool antbike: false + property bool domyosbike_notfmts: false + + // from version 2.16.70 + property bool gears_volume_debouncing: false + property bool tile_biggears_enabled: false + property int tile_biggears_order: 54 + property bool domyostreadmill_notfmts: false + property bool zwiftplay_swap: false + property bool gears_zwift_ratio: false + property bool domyos_bike_500_profile_v2: false + property double gears_offset: 0.0 + property bool proform_carbon_tl_PFTL59720: false } function paddingZeros(text, limit) { @@ -1031,11 +1049,12 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignLeft | Qt.AlignTop + width: column1.width * 0.8 Layout.fillWidth: true color: Material.color(Material.Lime) } @@ -1068,7 +1087,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1106,7 +1125,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Enter your age so that calories burned can be more accurately calculated.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1146,7 +1165,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Select your gender so that calories burned can be more accurately calculated.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1185,7 +1204,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1221,7 +1240,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("If you train to specific output (or watts) levels, for example with Stryd,and have taken an CP test (Critical Power Test), enter your CP here. This number is used to calculate your RSS.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1258,7 +1277,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1295,7 +1314,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1323,7 +1342,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1351,7 +1370,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1379,7 +1398,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1395,7 +1414,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Zwift users: keep this setting off") font.bold: yes font.italic: yes - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1409,7 +1428,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Grey) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 SwitchDelegate { id: switchDelegate text: qsTr("Heart Rate service outside FTMS") @@ -1429,7 +1448,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1457,7 +1476,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1485,7 +1504,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1527,10 +1546,11 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter + Layout.fillWidth: true color: Material.color(Material.Red) } @@ -1703,7 +1723,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1718,7 +1738,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1840,7 +1860,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1880,7 +1900,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1906,7 +1926,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -1943,7 +1963,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("0.005 = Clinchers\n0.004 = Tubulars\n0.012 = MTB") font.italic: true Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 color: "steelblue" } RowLayout { @@ -1975,7 +1995,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2051,7 +2071,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2089,7 +2109,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2127,7 +2147,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2165,7 +2185,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2203,7 +2223,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("See above. Default is 10.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2241,7 +2261,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2279,7 +2299,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2317,7 +2337,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2350,10 +2370,46 @@ import QtQuick.Dialogs 1.0 } Label { - text: qsTr("Applies a multiplier to the gears tile. Default is 1.") + text: qsTr("Applies a multiplier to the gears. Default is 1.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 + 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 Offset:") + Layout.fillWidth: true + } + TextField { + id: gearsOffsetTextField + text: settings.gears_offset + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + //inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.gears_offset = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.gears_offset = gearsOffsetTextField.text; toast.show("Setting saved!"); } + } + } + + Label { + text: qsTr("Applies an offset to the gears. Default is 0.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2392,7 +2448,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2406,7 +2462,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2492,7 +2548,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -2947,7 +3003,6 @@ import QtQuick.Dialogs 1.0 } } SwitchDelegate { - id: domyosBikeCaloriesDisplayDelegate text: qsTr("Fix Calories/Km to Console") spacing: 0 bottomPadding: 0 @@ -2961,7 +3016,6 @@ import QtQuick.Dialogs 1.0 onClicked: settings.domyos_bike_display_calories = checked } SwitchDelegate { - id: domyosBike500ProfileV1Delegate text: qsTr("Bike 500 wattage profile") spacing: 0 bottomPadding: 0 @@ -2972,8 +3026,21 @@ import QtQuick.Dialogs 1.0 checked: settings.domyos_bike_500_profile_v1 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true - onClicked: settings.domyos_bike_500_profile_v1 = checked + onClicked: { settings.domyos_bike_500_profile_v1 = checked; settings.domyos_bike_500_profile_v2 = false; } } + SwitchDelegate { + text: qsTr("Bike 500 wattage profile v2") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.domyos_bike_500_profile_v2 + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.domyos_bike_500_profile_v2 = checked; settings.domyos_bike_500_profile_v1 = false; } + } } AccordionElement { title: qsTr("Tacx Neo Options") @@ -3474,11 +3541,12 @@ import QtQuick.Dialogs 1.0 text: qsTr("Set 100mm as wheel circumference in settings of ant+ speed sensor") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter color: Material.color(Material.Red) + Layout.fillWidth: true } SwitchDelegate { @@ -3500,7 +3568,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3536,7 +3604,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3573,7 +3641,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3601,7 +3669,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3666,7 +3734,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3703,7 +3771,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Android Only: width of the floating window.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3740,7 +3808,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Android Only: height of the floating window.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3777,7 +3845,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Android Only: transparency percentage of the floating window.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3805,7 +3873,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -3827,7 +3895,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Yellow) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 SwitchDelegate { id: tilesIconsDelegate text: qsTr("Tiles Icons") @@ -4054,7 +4122,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4093,7 +4161,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4133,7 +4201,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4151,7 +4219,7 @@ import QtQuick.Dialogs 1.0 ComboBox { id: pelotonTreadmillLevelTextField model: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ] - displayText: settings.peloton_rower_level + displayText: settings.peloton_treadmill_level Layout.fillHeight: false Layout.alignment: Qt.AlignRight | Qt.AlignVCenter onActivated: { @@ -4171,7 +4239,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Difficulty level for peloton treadmill classes. 1 is easy 10 is hard.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4209,7 +4277,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Difficulty level for peloton rower classes. 1 is easy 10 is hard.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4246,7 +4314,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4285,7 +4353,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4323,7 +4391,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4361,7 +4429,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4389,7 +4457,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Turn this on to send cadence to Peloton over Bluetooth. Default is off.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4457,7 +4525,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4497,7 +4565,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4525,7 +4593,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4552,7 +4620,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4580,7 +4648,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4607,7 +4675,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4635,7 +4703,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4682,7 +4750,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Enter the email address you use to login to Zwift. Ensure there are no spaces before or after your email. Click OK.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4722,7 +4790,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Enter the password you use to login to Zwift. Click OK.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4758,7 +4826,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Define the number of delay seconds between each inclination change from Zwift. This value can't be less than 5. Default: 5") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4785,7 +4853,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Only for Android and iOS: QZ will read the inclination in real time from the Zwift app and will adjust the inclination on your treadmill. It doesn't work on workout") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4812,7 +4880,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4853,7 +4921,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4890,7 +4958,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("If you want to send metrics to your Garmin device from your Mac, enable this. Otherwise leave it disabled.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4917,7 +4985,34 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 + 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("Ant+ Bike Over Garmin Watch") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.antbike + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.antbike = checked; window.settings_restart_to_apply = true; } + } + + Label { + text: qsTr("Use your garmin watch to get the ANT+ metrics from a bike") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -4957,7 +5052,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5000,7 +5095,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5057,7 +5152,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5094,7 +5189,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5131,7 +5226,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5168,7 +5263,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5205,7 +5300,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5242,7 +5337,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5282,7 +5377,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5504,7 +5599,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5543,7 +5638,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5571,7 +5666,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5599,7 +5694,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5627,7 +5722,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5665,7 +5760,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5703,7 +5798,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5739,7 +5834,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("This overrides the minimum inclination value of your treadmill (in order to reduce the inclination movement). Default is -100") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5775,7 +5870,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("This overrides the maximum inclination value of your treadmill (in order to reduce the inclination movement). Default is -100") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5796,7 +5891,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Overrides the default inclination values sent from the treadmill") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5823,7 +5918,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5862,7 +5957,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5875,7 +5970,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -5909,7 +6004,8 @@ import QtQuick.Dialogs 1.0 "Proform/NordicTrack z1300i", "Proform SE", "Proform Cadence LT", "Proform 8.0", "Proform 9.0", "Proform 705 CST", "Nordictrack x14i", "Proform Carbon TL", "Proform Proshox 2", "Nordictrack S20i", "Proform 595i", - "Proform 8.7", "Proform 705 CST V78.239", "Proform Carbon T7" + "Proform 8.7", "Proform 705 CST V78.239", "Proform Carbon T7", + "Nordictrack EXP 5i", "Proform Carbon TL PFTL59720" ] onCurrentIndexChanged: { @@ -5948,6 +6044,8 @@ import QtQuick.Dialogs 1.0 settings.proform_treadmill_8_7 = false; settings.proform_treadmill_705_cst_V78_239 = false; settings.proform_treadmill_carbon_t7 = false; + settings.nordictrack_treadmill_exp_5i = false; + settings.proform_carbon_tl_PFTL59720 = false; // Imposta il setting corrispondente al modello selezionato switch (currentIndex) { @@ -5983,6 +6081,8 @@ import QtQuick.Dialogs 1.0 case 29: settings.proform_treadmill_8_7 = true; break; case 30: settings.proform_treadmill_705_cst_V78_239 = true; break; case 31: settings.proform_treadmill_carbon_t7 = true; break; + case 32: settings.nordictrack_treadmill_exp_5i = true; break; + case 33: settings.proform_carbon_tl_PFTL59720 = true; break; } } @@ -6019,7 +6119,9 @@ import QtQuick.Dialogs 1.0 settings.proform_595i_proshox2 ? 28 : settings.proform_treadmill_8_7 ? 29 : settings.proform_treadmill_705_cst_V78_239 ? 30 : - settings.proform_treadmill_carbon_t7 ? 31 : -1; + settings.proform_treadmill_carbon_t7 ? 31 : + settings.nordictrack_treadmill_exp_5i ? 32 : + settings.proform_carbon_tl_PFTL59720 ? 33 : -1; console.log("treadmillModelComboBox " + "Component.onCompleted " + selectedModel); @@ -6484,7 +6586,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7349,7 +7451,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7483,6 +7585,41 @@ import QtQuick.Dialogs 1.0 Layout.fillWidth: true onClicked: { settings.nordictrack_elliptical_c7_5 = checked; window.settings_restart_to_apply = true; } } + RowLayout { + spacing: 10 + Label { + text: qsTr("Companion IP:") + Layout.fillWidth: true + } + TextField { + id: proformEllipticalCompanionIPTextField + text: settings.proform_elliptical_ip + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + //inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.proform_elliptical_ip = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.proform_elliptical_ip = proformEllipticalCompanionIPTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); } + } + } + SwitchDelegate { + text: qsTr("ADB Remote") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.nordictrack_ifit_adb_remote + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.nordictrack_ifit_adb_remote = checked; window.settings_restart_to_apply = true; } + } } AccordionElement { @@ -7595,7 +7732,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7633,7 +7770,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7671,7 +7808,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7709,7 +7846,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7748,7 +7885,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7786,7 +7923,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7824,7 +7961,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7894,7 +8031,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7921,7 +8058,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7949,7 +8086,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Append the Virtual Tag to the Strava Activity") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7976,7 +8113,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -7987,7 +8124,7 @@ import QtQuick.Dialogs 1.0 SwitchDelegate { id: volumeChangeGearsDelegate - text: qsTr("Volumes buttons change gears") + text: qsTr("Volume buttons change gears") spacing: 0 bottomPadding: 0 topPadding: 0 @@ -8004,7 +8141,34 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 + 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("Volume buttons debouncing") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.gears_volume_debouncing + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.gears_volume_debouncing = checked; } + } + + Label { + text: qsTr("Debounce the volume buttons, so you will only see 1 gear step if there are 2 or more volume near steps. Default is off.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8032,7 +8196,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8060,7 +8224,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8088,7 +8252,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8126,7 +8290,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8164,7 +8328,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8191,7 +8355,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8227,7 +8391,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("You can trigger auto laps in the FIT file based on distance. Unit: "+ (settings.miles_unit?"Mi":"KM") +" Default: 0 (disabled).") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8263,7 +8427,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("This slow down the inclination changes adding a delay between each change. This is not applied to all the model of treadmill/bike. Default is 0.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8281,7 +8445,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Grey) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 AccordionElement { id: cadenceSensorOptionsAccordion @@ -8290,18 +8454,19 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Yellow) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 Label { id: cadenceSensorLabel text: qsTr("Don't touch these settings if your bike works properly!") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter color: Material.color(Material.Red) + Layout.fillWidth: true } SwitchDelegate { @@ -8323,7 +8488,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8371,7 +8536,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8409,7 +8574,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8427,7 +8592,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Yellow) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 SwitchDelegate { id: powerSensorAsBikeDelegate text: qsTr("Power Sensor as a Bike") @@ -8447,7 +8612,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8475,7 +8640,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8503,7 +8668,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8531,7 +8696,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Divide the cadence sent to Strava by 2.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8558,7 +8723,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("If you have a bluetooth treadmill and also a Stryd device connected to QZ and you want to use the speed from the stryd instead of the speed of the treadmill, enable this. Default: disabled.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8585,7 +8750,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("If you have a bluetooth treadmill and also a Stryd device connected to QZ, by default Stryd can't get the inclination from the treadmill. Enabling this and QZ will add a inclination gain to the power read from the Stryd. Default: disabled.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8633,7 +8798,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Leave on Disabled or select from list of found Bluetooth devices.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -8651,7 +8816,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Yellow) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 AccordionElement { id: eliteRizerOptionsAccordion title: qsTr("Elite Rizer Options") @@ -8659,7 +8824,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Blue) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 Label { id: labelEliteRizerName text: qsTr("Elite Rizer:") @@ -8727,7 +8892,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Blue) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 Label { id: labelEliteSterzoSmartName text: qsTr("Elite Sterzo Smart:") @@ -8774,7 +8939,7 @@ import QtQuick.Dialogs 1.0 textColor: Material.color(Material.Yellow) color: Material.backgroundColor accordionContent: ColumnLayout { - spacing: 10 + spacing: 0 Label { id: labelFTMSAccessoryName text: qsTr("SmartSpin2k device:") @@ -9412,7 +9577,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Use it to change the gears on QZ!") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9439,7 +9604,61 @@ import QtQuick.Dialogs 1.0 text: qsTr("Use it to change the gears on QZ!") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 + 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("Buttons debouncing") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.gears_volume_debouncing + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.gears_volume_debouncing = checked; } + } + + Label { + text: qsTr("Debounce the buttons, so you will only see 1 gear step even if you are keep pressing the buttons. Default is off.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 + 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("Swap sides") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.zwiftplay_swap + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.zwiftplay_swap = checked; } + } + + Label { + text: qsTr("You can swap the left to the right controller and viceversa. Default is off.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9447,6 +9666,33 @@ import QtQuick.Dialogs 1.0 Layout.fillWidth: true color: Material.color(Material.Lime) } + + SwitchDelegate { + text: qsTr("Use Zwift app ratio for gears (Experimental)") + spacing: 0 + bottomPadding: 0 + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + clip: false + checked: settings.gears_zwift_ratio + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + onClicked: { settings.gears_zwift_ratio = checked; } + } + + Label { + text: qsTr("Use the zwift gears table instead of the QZ classic gears algorithm. Default is off.") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 + textFormat: Text.PlainText + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + color: Material.color(Material.Lime) + } } } @@ -9584,7 +9830,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9612,7 +9858,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9640,7 +9886,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9695,7 +9941,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9723,7 +9969,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9751,7 +9997,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9779,7 +10025,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9808,7 +10054,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9836,7 +10082,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9872,7 +10118,43 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 + 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("ID:") + Layout.fillWidth: true + } + TextField { + id: dirconIdTextField + text: settings.dircon_id + horizontalAlignment: Text.AlignRight + Layout.fillHeight: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + inputMethodHints: Qt.ImhFormattedNumbersOnly + onAccepted: settings.dircon_id = text + onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length + } + Button { + text: "OK" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + onClicked: { settings.dircon_id = dirconIdTextField.text; toast.show("Setting saved!"); window.settings_restart_to_apply = true; } + } + } + + Label { + text: qsTr("If you have multiple QZ instances, you can change the id of the virtual wahoo device. Default: 0") + font.bold: true + font.italic: true + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9927,7 +10209,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -9955,7 +10237,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10028,7 +10310,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Forces Android devices to remain awake while QZ is running. Default is on.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10056,7 +10338,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10084,7 +10366,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10112,7 +10394,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Same as Fake Device but instead of simulating a bike it simulates a treadmill.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10140,7 +10422,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Same as Fake Device but instead of simulating a bike it simulates an elliptical.") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10167,7 +10449,7 @@ import QtQuick.Dialogs 1.0 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 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10195,7 +10477,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10223,7 +10505,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10250,7 +10532,7 @@ import QtQuick.Dialogs 1.0 text: qsTr("Android Only: force QZ to use the /Documents/QZ folder for debug log and fit files") font.bold: true font.italic: true - font.pixelSize: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10278,7 +10560,7 @@ import QtQuick.Dialogs 1.0 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter @@ -10298,7 +10580,7 @@ import QtQuick.Dialogs 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: 9 + font.pixelSize: Qt.application.font.pixelSize - 2 textFormat: Text.PlainText wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter diff --git a/src/templateinfosenderbuilder.cpp b/src/templateinfosenderbuilder.cpp index d9a169439..68ec79315 100644 --- a/src/templateinfosenderbuilder.cpp +++ b/src/templateinfosenderbuilder.cpp @@ -1090,6 +1090,9 @@ void TemplateInfoSenderBuilder::buildContext(bool forceReinit) { obj.setProperty(QStringLiteral("req_cadence"), (dep = ((bike *)device)->lastRequestedCadence()).value()); obj.setProperty(QStringLiteral("req_resistance"), (dep = ((bike *)device)->lastRequestedResistance()).value()); + obj.setProperty(QStringLiteral("inclination"), + (dep = ((bike *)device)->currentInclination()).value()); + obj.setProperty(QStringLiteral("inclination_avg"), dep.average()); } else if (tp == bluetoothdevice::ROWING) { obj.setProperty(QStringLiteral("gears"), ((rower *)device)->gears()); el = ((rower *)device)->lastRequestedPace(); diff --git a/src/trainprogram.cpp b/src/trainprogram.cpp index f2150c3ba..c489ecb03 100644 --- a/src/trainprogram.cpp +++ b/src/trainprogram.cpp @@ -963,6 +963,12 @@ void trainprogram::scheduler() { currentStepDistance += (odometerFromTheDevice - lastOdometer); lastOdometer = odometerFromTheDevice; + + if(currentStep >= rows.length()) { + qDebug() << "currentStep greater than row.length" << currentStep << rows.length(); + end(); + return; + } bool distanceStep = (rows.at(currentStep).distance > 0); distanceEvaluation = (distanceStep && currentStepDistance >= rows.at(currentStep).distance); qDebug() << qSetRealNumberPrecision(10) << QStringLiteral("currentStepDistance") << currentStepDistance @@ -983,6 +989,12 @@ void trainprogram::scheduler() { else currentStep++; + if(currentStep >= rows.length()) { + qDebug() << "currentStep greater than row.length" << currentStep << rows.length(); + end(); + return; + } + calculatedLine = currentStep; rows[currentStep].started = QDateTime::currentDateTime(); @@ -1127,23 +1139,8 @@ void trainprogram::scheduler() { emit changeGeoPosition(p, rows.at(currentStep).azimuth, avgAzimuthNext300Meters()); } } else { - qDebug() << QStringLiteral("trainprogram ends!"); - - // circuit? - if (!isnan(rows.first().latitude) && !isnan(rows.first().longitude) && - QGeoCoordinate(rows.first().latitude, rows.first().longitude) - .distanceTo(bluetoothManager->device()->currentCordinate()) < 50) { - emit lap(); - restart(); - distanceEvaluation = false; - } else { - started = false; - if (settings - .value(QZSettings::trainprogram_stop_at_end, QZSettings::default_trainprogram_stop_at_end) - .toBool()) - emit stop(false); - distanceEvaluation = false; - } + end(); + distanceEvaluation = false; } } else { if (rows.length() > currentStep && rows.at(currentStep).power != -1) { @@ -1210,6 +1207,25 @@ void trainprogram::scheduler() { } while (distanceEvaluation); } +void trainprogram::end() { + QSettings settings; + qDebug() << QStringLiteral("trainprogram ends!"); + + // circuit? + if (!isnan(rows.first().latitude) && !isnan(rows.first().longitude) && + QGeoCoordinate(rows.first().latitude, rows.first().longitude) + .distanceTo(bluetoothManager->device()->currentCordinate()) < 50) { + emit lap(); + restart(); + } else { + started = false; + if (settings + .value(QZSettings::trainprogram_stop_at_end, QZSettings::default_trainprogram_stop_at_end) + .toBool()) + emit stop(false); + } +} + bool trainprogram::overridePowerForCurrentRow(double power) { if (started && currentStep < rows.length() && currentRow().power != -1) { qDebug() << "overriding power from" << rows.at(currentStep).power << "to" << power; diff --git a/src/trainprogram.h b/src/trainprogram.h index b47896f95..0706a1015 100644 --- a/src/trainprogram.h +++ b/src/trainprogram.h @@ -144,6 +144,7 @@ private slots: void zwiftLoginState(bool ok); private: + void end(); mutable QRecursiveMutex schedulerMutex; double avgAzimuthNext300Meters(); QList inclinationNext300Meters(); diff --git a/src/virtualdevices/virtualbike.cpp b/src/virtualdevices/virtualbike.cpp index 2b4f7b2bf..7e110e620 100644 --- a/src/virtualdevices/virtualbike.cpp +++ b/src/virtualdevices/virtualbike.cpp @@ -709,10 +709,15 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte } else if (newValue.length() > 8 && ((uint8_t)newValue.at(0)) == 0xFF && ((uint8_t)newValue.at(1)) == 0x07 && ((uint8_t)newValue.at(7)) == 0x10) { qDebug() << "ifit ans 12"; - reply1 = QByteArray::fromHex("fe021c0300b4002200580200000000007e0000b4"); + if(iFit_Stop == false) { + reply1 = QByteArray::fromHex("fe021c0300b4002200580200000000007e0000b4"); + } else { + qDebug() << "ifit ans 12 - with stop request"; + reply1 = QByteArray::fromHex("fe021c0302050705020210000000000036000000"); + iFit_Stop = false; + } reply2 = QByteArray::fromHex("001201040218071802020000ffffffffffffffff"); reply3 = QByteArray::fromHex("ff0a00000000302a00000075ffffffffffffffff"); - writeCharacteristic(service, characteristic, reply1); writeCharacteristic(service, characteristic, reply2); writeCharacteristic(service, characteristic, reply3); @@ -778,8 +783,9 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte ((uint8_t)newValue.at(8)) == 0x02) { // ff0f0204020b070b0202041032020a0068000000 qDebug() << "ifit ans 15 stop request"; iFit_timer = 0; - reply1 = QByteArray::fromHex("fe0209020205070502021000ffffffffffffffff"); - reply2 = QByteArray::fromHex("ff0901040205070502021000ffffffffffffffff"); + iFit_Stop = true; + reply1 = QByteArray::fromHex("fe02090200b40000005802000000000038000000"); + reply2 = QByteArray::fromHex("ff09010402050705020210000000000038000000"); writeCharacteristic(service, characteristic, reply1); writeCharacteristic(service, characteristic, reply2); } else if (newValue.length() > 12 && ((uint8_t)newValue.at(0)) == 0xFF && diff --git a/src/virtualdevices/virtualbike.h b/src/virtualdevices/virtualbike.h index 05fc1f90b..1d82892f3 100644 --- a/src/virtualdevices/virtualbike.h +++ b/src/virtualdevices/virtualbike.h @@ -79,6 +79,7 @@ class virtualbike : public virtualdevice { qint64 iFit_TSLastFrame = 0; QByteArray iFit_LastFrameReceived; resistance_t iFit_LastResistanceRequested = 0; + bool iFit_Stop = false; bool echelonInitDone = false; void echelonWriteResistance(); diff --git a/src/virtualdevices/virtualtreadmill.cpp b/src/virtualdevices/virtualtreadmill.cpp index 49c4939da..b51bf0d0f 100644 --- a/src/virtualdevices/virtualtreadmill.cpp +++ b/src/virtualdevices/virtualtreadmill.cpp @@ -281,7 +281,7 @@ virtualtreadmill::virtualtreadmill(bluetoothdevice *t, bool noHeartService) { void virtualtreadmill::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) { qDebug() << QStringLiteral("characteristicChanged ") + QString::number(characteristic.uuid().toUInt16()) + - QStringLiteral(" ") + newValue; + QStringLiteral(" ") << newValue.toHex(' '); QByteArray reply; switch (characteristic.uuid().toUInt16()) { diff --git a/src/zwift_play/abstractZapDevice.h b/src/zwift_play/abstractZapDevice.h index ca9c10d9a..ba429e67e 100755 --- a/src/zwift_play/abstractZapDevice.h +++ b/src/zwift_play/abstractZapDevice.h @@ -4,9 +4,11 @@ #include #include #include +#include //#include "localKeyProvider.h" //#include "zapCrypto.h" #include "zapConstants.h" +#include "qzsettings.h" #ifdef Q_OS_ANDROID #include #include @@ -14,7 +16,7 @@ class AbstractZapDevice: public QObject { Q_OBJECT -public: + public: enum ZWIFT_PLAY_TYPE { NONE, LEFT, @@ -25,7 +27,7 @@ class AbstractZapDevice: public QObject { QByteArray REQUEST_START; QByteArray RESPONSE_START; - //ZapCrypto zapEncryption; + //ZapCrypto zapEncryption; AbstractZapDevice() /*: localKeyProvider(), zapEncryption(localKeyProvider)*/ { RIDE_ON = QByteArray::fromRawData("\x52\x69\x64\x65\x4F\x6E", 6); // "RideOn" REQUEST_START = QByteArray::fromRawData("\x00\x09", 2); // {0, 9} @@ -35,7 +37,13 @@ class AbstractZapDevice: public QObject { int processCharacteristic(const QString& characteristicName, const QByteArray& bytes, ZWIFT_PLAY_TYPE zapType) { if (bytes.isEmpty()) return 0; - qDebug() << zapType << characteristicName << bytes.toHex() ; + QSettings settings; + bool gears_volume_debouncing = settings.value(QZSettings::gears_volume_debouncing, QZSettings::default_gears_volume_debouncing).toBool(); + bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool(); + + qDebug() << zapType << characteristicName << bytes.toHex() << zwiftplay_swap << gears_volume_debouncing << risingEdge; + +#define DEBOUNCE (!gears_volume_debouncing || risingEdge <= 0) #ifdef Q_OS_ANDROID_ENCRYPTION QAndroidJniEnvironment env; @@ -55,51 +63,146 @@ class AbstractZapDevice: public QObject { return button; #else switch(bytes[0]) { - case 0x37: - if(bytes.length() == 5) { - if(bytes[2] == 0) { - emit plus(); - } else if(bytes[4] == 0) { - emit minus(); + case 0x37: + if(bytes.length() == 5) { + if(bytes[2] == 0) { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit plus(); + else + emit minus(); + } + } else if(bytes[4] == 0) { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit minus(); + else + emit plus(); } + } else { + risingEdge--; + if(risingEdge < 0) + risingEdge = 0; } - break; - case 0x07: // zwift play - if(bytes.length() > 5 && bytes[bytes.length() - 5] == 0x40 && ( - (((uint8_t)bytes[bytes.length() - 4]) == 0xc7 && zapType == RIGHT) || - (((uint8_t)bytes[bytes.length() - 4]) == 0xc8 && zapType == LEFT) - ) && bytes[bytes.length() - 3] == 0x01) { - if(zapType == LEFT) { - emit plus(); - } else { - emit minus(); + } + break; + case 0x07: // zwift play + if(bytes.length() > 5 && bytes[bytes.length() - 5] == 0x40 && ( + (((uint8_t)bytes[bytes.length() - 4]) == 0xc7 && zapType == RIGHT) || + (((uint8_t)bytes[bytes.length() - 4]) == 0xc8 && zapType == LEFT) + ) && bytes[bytes.length() - 3] == 0x01) { + if(zapType == LEFT) { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit plus(); + else + emit minus(); + } + } else { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit minus(); + else + emit plus(); + } + } + } else if(bytes.length() > 14 && bytes[11] == 0x30 && bytes[12] == 0x00) { + if(zapType == LEFT) { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit plus(); + else + emit minus(); + } + } else { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit minus(); + else + emit plus(); } - } else if(bytes.length() > 14 && bytes[11] == 0x30 && bytes[12] == 0x00) { - if(zapType == LEFT) { + } + } else { + risingEdge--; + if(risingEdge < 0) + risingEdge = 0; + } + break; + case 0x15: // empty data + qDebug() << "ignoring this frame"; + return 1; + case 0x23: // zwift ride + if(bytes.length() > 12 && + ((((uint8_t)bytes[12]) == 0xc7 && zapType == RIGHT) || + (((uint8_t)bytes[12]) == 0xc8 && zapType == LEFT)) + ) { + if(zapType == LEFT) { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit plus(); + else + emit minus(); + } + } else { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit minus(); + else + emit plus(); + } + } + } else if(bytes.length() > 19 && ((uint8_t)bytes[18]) == 0xc8) { + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) emit plus(); - } else { + else emit minus(); - } } - break; - case 0x23: // zwift ride - if(bytes.length() > 12 && - ((((uint8_t)bytes[12]) == 0xc7 && zapType == RIGHT) || - (((uint8_t)bytes[12]) == 0xc8 && zapType == LEFT)) - ) { - if(zapType == LEFT) { + } else if(bytes.length() > 3 && + ((((uint8_t)bytes[3]) == 0xdf) || // right top button + (((uint8_t)bytes[3]) == 0xbf))) { // right bottom button + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) emit plus(); - } else { + else emit minus(); - } - }/* else if(bytes.length() > 14 && bytes[11] == 0x30 && bytes[12] == 0x00) { - if(zapType == LEFT) { + } + } else if(bytes.length() > 3 && + ((((uint8_t)bytes[3]) == 0xfd) || // left top button + (((uint8_t)bytes[3]) == 0xfb))) { // left bottom button + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) + emit minus(); + else emit plus(); - } else { + } + } else if(bytes.length() > 5 && + ((((uint8_t)bytes[4]) == 0xfd) || // left top button + (((uint8_t)bytes[4]) == 0xfb))) { // left bottom button + if(DEBOUNCE) { + risingEdge = 2; + if(!zwiftplay_swap) emit minus(); - } - }*/ - break; + else + emit plus(); + } + } else { + risingEdge--; + if(risingEdge < 0) + risingEdge = 0; + } + break; } return 1; @@ -115,20 +218,20 @@ class AbstractZapDevice: public QObject { // Ottiene la lunghezza dell'array di byte jsize length = QAndroidJniEnvironment()->GetArrayLength(result.object()); - // Allocare memoria per i byte nativi + // Allocare memoria per i byte nativi jbyte* bytes = QAndroidJniEnvironment()->GetByteArrayElements(result.object(), nullptr); - // Costruire un QByteArray dal buffer di byte nativi + // Costruire un QByteArray dal buffer di byte nativi QByteArray byteArray(reinterpret_cast(bytes), length); - // Rilasciare la memoria dell'array di byte JNI + // Rilasciare la memoria dell'array di byte JNI QAndroidJniEnvironment()->ReleaseByteArrayElements(result.object(), bytes, JNI_ABORT); - // Ora puoi usare byteArray come necessario + // Ora puoi usare byteArray come necessario return byteArray; } #endif - //return RIDE_ON + REQUEST_START + localKeyProvider.getPublicKeyBytes(); + //return RIDE_ON + REQUEST_START + localKeyProvider.getPublicKeyBytes(); QByteArray a; a.append(0x52); a.append(0x69); @@ -139,13 +242,14 @@ class AbstractZapDevice: public QObject { return a; } -protected: + protected: virtual void processEncryptedData(const QByteArray& bytes) = 0; -private: + private: QByteArray devicePublicKeyBytes; + static volatile int8_t risingEdge; -signals: + signals: void plus(); void minus(); }; diff --git a/src/zwift_play/zwiftclickremote.cpp b/src/zwift_play/zwiftclickremote.cpp index a1092f9e0..7ae9aa489 100755 --- a/src/zwift_play/zwiftclickremote.cpp +++ b/src/zwift_play/zwiftclickremote.cpp @@ -1,3 +1,4 @@ +#include "homeform.h" #include "zwiftclickremote.h" #include #include @@ -13,6 +14,8 @@ using namespace std::chrono_literals; extern quint8 QZ_EnableDiscoveryCharsAndDescripttors; #endif +volatile int8_t AbstractZapDevice::risingEdge = 0; + zwiftclickremote::zwiftclickremote(bluetoothdevice *parentDevice, AbstractZapDevice::ZWIFT_PLAY_TYPE typeZap) { #ifdef Q_OS_IOS QZ_EnableDiscoveryCharsAndDescripttors = true; @@ -32,6 +35,12 @@ void zwiftclickremote::update() { QByteArray s = playDevice->buildHandshakeStart(); qDebug() << s.length(); writeCharacteristic(gattWrite1Service, &gattWrite1Characteristic, (uint8_t *) s.data(), s.length(), "handshakeStart"); + } else if(initDone) { + countRxTimeout++; + if(countRxTimeout == 10) { + if(homeform::singleton()) + homeform::singleton()->setToastRequested("Zwift device: UPGRADE THE FIRMWARE!"); + } } } @@ -52,6 +61,8 @@ void zwiftclickremote::characteristicChanged(const QLowEnergyCharacteristic &cha Q_UNUSED(characteristic); emit packetReceived(); + countRxTimeout = 0; + qDebug() << QStringLiteral(" << ") << newValue.toHex(' ') << QString(newValue) << characteristic.uuid().Name << characteristic.uuid().toString() << typeZap; if(characteristic.uuid() == QBluetoothUuid(QStringLiteral("00000002-19CA-4651-86E5-FA29DCDD09D1"))) { diff --git a/src/zwift_play/zwiftclickremote.h b/src/zwift_play/zwiftclickremote.h index 47033dac1..0c19e32db 100755 --- a/src/zwift_play/zwiftclickremote.h +++ b/src/zwift_play/zwiftclickremote.h @@ -53,7 +53,9 @@ class zwiftclickremote : public bluetoothdevice { bool initRequest = false; AbstractZapDevice::ZWIFT_PLAY_TYPE typeZap = AbstractZapDevice::NONE; - QTimer *refresh; + QTimer *refresh; + + uint32_t countRxTimeout = 0; signals: void disconnected(); diff --git a/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h b/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h index d35cfee56..7e0b95e28 100644 --- a/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h +++ b/tst/Devices/HorizonTreadmill/horizontreadmilltestdata.h @@ -22,9 +22,7 @@ class HorizonTreadmillTestData : public TreadmillTestData { 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);