mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
182 Commits
android-no
...
virtualgea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a250fe3a3 | ||
|
|
094d2c88cb | ||
|
|
7f3dda70fd | ||
|
|
79d96ba182 | ||
|
|
5ec861dab3 | ||
|
|
f90edbd632 | ||
|
|
621ed69627 | ||
|
|
b754b7f773 | ||
|
|
efb9dfbdb1 | ||
|
|
a40fec4082 | ||
|
|
f6a9d8ca4e | ||
|
|
dd2bfc4e1b | ||
|
|
06fd78378e | ||
|
|
f28574245c | ||
|
|
b964c523dd | ||
|
|
0721bc3ec5 | ||
|
|
3f783305b2 | ||
|
|
be29180e48 | ||
|
|
19c65d7d90 | ||
|
|
704c7f1f80 | ||
|
|
678ac9d466 | ||
|
|
a8a6c5d736 | ||
|
|
e8408710df | ||
|
|
47825f0783 | ||
|
|
f7ce518812 | ||
|
|
f887a068b9 | ||
|
|
6ecbce4b87 | ||
|
|
9454d75f55 | ||
|
|
4063321418 | ||
|
|
bb88d58e47 | ||
|
|
7bc2f065c0 | ||
|
|
c773b45ddf | ||
|
|
eaf7db7813 | ||
|
|
a29f6350d0 | ||
|
|
65ad925d37 | ||
|
|
8fd486d582 | ||
|
|
8fa5dcadcb | ||
|
|
6abf6c9cfd | ||
|
|
b4603da714 | ||
|
|
b27e84de69 | ||
|
|
49337cbbc6 | ||
|
|
fe2f5e923c | ||
|
|
69f54dbd54 | ||
|
|
bc20ec0d8f | ||
|
|
278add7a11 | ||
|
|
6e90091883 | ||
|
|
ebda22d7b4 | ||
|
|
625ffb3932 | ||
|
|
fe6868911e | ||
|
|
1c73d15377 | ||
|
|
c33ee55efb | ||
|
|
56979a2122 | ||
|
|
3e1db8bfdf | ||
|
|
10fdc52446 | ||
|
|
23d23c40a5 | ||
|
|
90e8eeb983 | ||
|
|
dcf395ec46 | ||
|
|
d55cb553d3 | ||
|
|
b862d26bc3 | ||
|
|
d5e4f11849 | ||
|
|
5e9679f6c3 | ||
|
|
8799c447fb | ||
|
|
bcdb767b7e | ||
|
|
15e208d34c | ||
|
|
f16c41e6dd | ||
|
|
9110c55cb1 | ||
|
|
881e155cbc | ||
|
|
e2d187a7bd | ||
|
|
66821d884a | ||
|
|
73ad1dc46c | ||
|
|
c91a2d3ee5 | ||
|
|
87c0e95b01 | ||
|
|
174da2ac14 | ||
|
|
b61ba37b8f | ||
|
|
27333e7836 | ||
|
|
58a9e81bd8 | ||
|
|
d78e92f42f | ||
|
|
2a5eb7b057 | ||
|
|
ae5f70645a | ||
|
|
d26b14276e | ||
|
|
9166ce7218 | ||
|
|
5f0ec98b0c | ||
|
|
1bc7af0a88 | ||
|
|
df75d33ca6 | ||
|
|
34f7df6bfb | ||
|
|
1208b439fa | ||
|
|
14a9faa2ee | ||
|
|
ca4fb0b35e | ||
|
|
6ea6e6d9b2 | ||
|
|
2e17aa40ec | ||
|
|
098392684f | ||
|
|
6678e225c5 | ||
|
|
ca0bd15e69 | ||
|
|
1675240f13 | ||
|
|
b21c6325bb | ||
|
|
b2f9e3d754 | ||
|
|
6dc5d74de3 | ||
|
|
be560aae89 | ||
|
|
37858ca972 | ||
|
|
d3a1a2aafb | ||
|
|
49b890715e | ||
|
|
f19449107b | ||
|
|
8bbed4fa76 | ||
|
|
efc4950f92 | ||
|
|
23fd13ad0c | ||
|
|
64cd90dfaa | ||
|
|
ec5919d67f | ||
|
|
0fc9d7fb40 | ||
|
|
4f03554fbb | ||
|
|
4a16605f43 | ||
|
|
3ae60e1c41 | ||
|
|
edab888e31 | ||
|
|
2eefcee9b7 | ||
|
|
c1db263dcf | ||
|
|
cdf0d34b86 | ||
|
|
eb0528215b | ||
|
|
30d0940359 | ||
|
|
9f7cdd8b42 | ||
|
|
af00334455 | ||
|
|
4e8af61539 | ||
|
|
a17b78c56b | ||
|
|
8f536f487e | ||
|
|
82cad601bf | ||
|
|
a3579c42fa | ||
|
|
9af0046554 | ||
|
|
d59eabc9b3 | ||
|
|
d8d55cfbf8 | ||
|
|
bce3f3cef3 | ||
|
|
e2d5e602e1 | ||
|
|
054087a3bf | ||
|
|
123d1f9634 | ||
|
|
9130cabc65 | ||
|
|
3c893444e6 | ||
|
|
24935046e9 | ||
|
|
ecf596623e | ||
|
|
620be36635 | ||
|
|
3c98edfb6d | ||
|
|
b0c4690489 | ||
|
|
64f1fce8c8 | ||
|
|
f2df53b94b | ||
|
|
ae4aec68c6 | ||
|
|
68696a585a | ||
|
|
ee0279186a | ||
|
|
60c4747a0e | ||
|
|
23e2202bc0 | ||
|
|
e9c2ed8a76 | ||
|
|
9b9174b45a | ||
|
|
e9451c1c76 | ||
|
|
28bd6423b7 | ||
|
|
083fe13ce3 | ||
|
|
574a78ba0b | ||
|
|
54177f927e | ||
|
|
a9c0a23f0a | ||
|
|
5f92401c98 | ||
|
|
2d959a580f | ||
|
|
cc046278fd | ||
|
|
af82f731cf | ||
|
|
a9ff106e54 | ||
|
|
8e2cf858b9 | ||
|
|
d19eee81b3 | ||
|
|
9f5a2ae120 | ||
|
|
6bb520a0a9 | ||
|
|
5031e01e00 | ||
|
|
4ae0c5c638 | ||
|
|
fb45b52341 | ||
|
|
156ea9e7ae | ||
|
|
1b7e86481b | ||
|
|
e11d6d7f6a | ||
|
|
c72759c70a | ||
|
|
38570d855e | ||
|
|
122ff3e25f | ||
|
|
1d48c42aa4 | ||
|
|
daae5659cf | ||
|
|
7c11ff324f | ||
|
|
e4beee9baf | ||
|
|
815d8758b0 | ||
|
|
43fc6f795d | ||
|
|
075c316bfa | ||
|
|
1ff42c9658 | ||
|
|
2add1a9425 | ||
|
|
0fd7f40412 | ||
|
|
7e0604032a |
321
.github/workflows/main.yml
vendored
321
.github/workflows/main.yml
vendored
@@ -409,7 +409,7 @@ jobs:
|
||||
path: "src/qthttpserver"
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -655,7 +655,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api-level: [24, 26, 28, 29, 30, 31, 33, 34, 35, 36]
|
||||
api-level: [24, 26, 28, 29, 30, 31, 33, 35, 36]
|
||||
include:
|
||||
- api-level: 24
|
||||
target: default
|
||||
@@ -685,10 +685,6 @@ jobs:
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 13"
|
||||
- api-level: 34
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 14"
|
||||
- api-level: 35
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
@@ -741,19 +737,47 @@ jobs:
|
||||
# Install the APK
|
||||
adb install apk-debug/android-debug.apk
|
||||
|
||||
# Grant necessary permissions for API 25
|
||||
echo "Granting permissions..."
|
||||
# Grant necessary permissions - comprehensive list for all Android APIs
|
||||
echo "Granting all required permissions..."
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_FINE_LOCATION || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_COARSE_LOCATION || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADMIN || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADVERTISE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_CONNECT || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_SCAN || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WRITE_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.MANAGE_EXTERNAL_STORAGE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.CAMERA || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.RECORD_AUDIO || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.INTERNET || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_NETWORK_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_WIFI_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.CHANGE_WIFI_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WAKE_LOCK || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.VIBRATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_PHONE_STATE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.FOREGROUND_SERVICE || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS || true
|
||||
|
||||
# Additional permissions for newer Android versions (12+)
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.POST_NOTIFICATIONS || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.SCHEDULE_EXACT_ALARM || true
|
||||
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.USE_EXACT_ALARM || true
|
||||
|
||||
# Enable all app ops permissions
|
||||
adb shell appops set org.cagnulen.qdomyoszwift MANAGE_EXTERNAL_STORAGE allow || true
|
||||
adb shell appops set org.cagnulen.qdomyoszwift SYSTEM_ALERT_WINDOW allow || true
|
||||
adb shell appops set org.cagnulen.qdomyoszwift WRITE_SETTINGS allow || true
|
||||
|
||||
echo "All permissions granted successfully"
|
||||
|
||||
# Start the main activity
|
||||
adb shell am start -n org.cagnulen.qdomyoszwift/org.cagnulen.qdomyoszwift.CustomQtActivity
|
||||
|
||||
# Wait for app to start
|
||||
sleep 60
|
||||
sleep 90
|
||||
|
||||
# Verify the app is running
|
||||
echo "Checking if app is running..."
|
||||
@@ -779,6 +803,18 @@ jobs:
|
||||
adb shell screencap -p /sdcard/screenshot.png
|
||||
adb pull /sdcard/screenshot.png
|
||||
|
||||
# Test orientamento automatico con screenshot
|
||||
echo "Starting orientation test with automatic screenshots..."
|
||||
|
||||
# Screenshot iniziale (orientamento corrente)
|
||||
adb shell screencap -p /sdcard/screenshot_orientation_0.png
|
||||
adb pull /sdcard/screenshot_orientation_0.png
|
||||
|
||||
# Loop per 3 rotazioni aggiuntive (90°, 180°, 270°)
|
||||
for i in 1 2 3; do echo "Rotating to orientation $i (90° * $i)"; adb shell settings put system user_rotation $i; sleep 5; echo "Taking screenshot for orientation $i"; adb shell screencap -p /sdcard/screenshot_orientation_$i.png; adb pull /sdcard/screenshot_orientation_$i.png; done
|
||||
|
||||
echo "Orientation test completed - 4 screenshots captured"
|
||||
|
||||
# Check if the package is installed
|
||||
adb shell pm list packages | grep org.cagnulen.qdomyoszwift
|
||||
|
||||
@@ -794,6 +830,7 @@ jobs:
|
||||
name: android-emulator-test-evidence-api${{ matrix.api-level }}
|
||||
path: |
|
||||
screenshot.png
|
||||
screenshot_orientation_*.png
|
||||
process_list.txt
|
||||
full_logcat.txt
|
||||
qdomyos_logcat.txt
|
||||
@@ -801,7 +838,7 @@ jobs:
|
||||
|
||||
ios-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -864,7 +901,7 @@ jobs:
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
qmake CONFIG+=debug && make -j4
|
||||
qmake CONFIG+=debug CONFIG+=iphonesimulator && make -j4
|
||||
|
||||
# causes iOS build on Mac to fail
|
||||
# - name: Commit moc files
|
||||
@@ -1267,7 +1304,7 @@ jobs:
|
||||
bash -c "
|
||||
set -ex &&
|
||||
apt-get update &&
|
||||
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 &&
|
||||
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
@@ -1326,7 +1363,7 @@ jobs:
|
||||
bash -c "
|
||||
set -ex &&
|
||||
apt-get update &&
|
||||
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 &&
|
||||
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
@@ -1537,6 +1574,16 @@ jobs:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- name: treadmill
|
||||
setting_key: nordictrack_2950_ip
|
||||
setting_value: localhost
|
||||
- name: bike
|
||||
setting_key: tdf_10_ip
|
||||
setting_value: localhost
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
@@ -1590,6 +1637,111 @@ jobs:
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
# Set variant-specific IP setting
|
||||
sed -i 's/property string ${{ matrix.variant.setting_key }}: ""/property string ${{ matrix.variant.setting_key }}: "${{ matrix.variant.setting_value }}"/' settings.qml
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-nordictrack-${{ matrix.variant.name }}"/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-nordictrack-${{ matrix.variant.name }}.apk/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
|
||||
|
||||
- name: Archive nordictrack binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nordictrack-${{ matrix.variant.name }}-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
|
||||
|
||||
peloton-bike-plus-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3632/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Fix qmdnsengine submodule
|
||||
run: |
|
||||
cd src/qmdnsengine
|
||||
git fetch
|
||||
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
@@ -1621,30 +1773,137 @@ jobs:
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-nordictrack"/g' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-nordictrack.apk/g' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-peloton-bike-plus"/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-peloton-bike-plus.apk/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
|
||||
|
||||
- name: Archive nordictrack binary
|
||||
- name: Archive peloton-bike-plus binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nordictrack-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack.apk
|
||||
name: peloton-bike-plus-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
|
||||
|
||||
peloton-bike-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3639/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Fix qmdnsengine submodule
|
||||
run: |
|
||||
cd src/qmdnsengine
|
||||
git fetch
|
||||
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-peloton-bike"/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-peloton-bike.apk/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
|
||||
|
||||
- name: Archive peloton-bike binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: peloton-bike-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
|
||||
|
||||
upload_to_release:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-22.04
|
||||
#if: github.event_name == 'schedule'
|
||||
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
|
||||
if: github.event_name == 'schedule'
|
||||
needs: [window-msvc2019-build, window-msvc2022-build, window-build, android-build, raspberry-pi-build, nordictrack-build, peloton-bike-plus-build, peloton-bike-build, raspberry-pi-build-and-image-64bit]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -1653,9 +1912,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: nightly
|
||||
tag_name: nightly-${{ steps.date.outputs.date }}
|
||||
prerelease: false
|
||||
name: 'QZ nightly build $$'
|
||||
name: 'QZ nightly build - ${{ steps.date.outputs.date }}'
|
||||
body: |
|
||||
This is a nightly build of QZ.
|
||||
|
||||
@@ -1672,7 +1931,10 @@ jobs:
|
||||
|
||||
## Other Platforms:
|
||||
- **fdroid-android-trial**: Android build
|
||||
- **nordictrack-android-trial**: Nordictrack build for iFIT2 Tablets
|
||||
- **nordictrack-treadmill-android-trial**: Nordictrack Treadmill build for iFIT2 Tablets
|
||||
- **nordictrack-bike-android-trial**: Nordictrack Bike build for iFIT2 Tablets
|
||||
- **peloton-bike-plus-android-trial**: Peloton Bike Plus build with Grupetto backend
|
||||
- **peloton-bike-android-trial**: Peloton Bike build with Grupetto backend
|
||||
- **raspberry-pi-binary**: Raspberry Pi build
|
||||
|
||||
__Please help us improve QZ by reporting any issues you encounter!__ :wink:
|
||||
@@ -1685,6 +1947,9 @@ jobs:
|
||||
windows-binary-no-python/*
|
||||
windows-binary/*
|
||||
fdroid-android-trial/android-debug.apk
|
||||
nordictrack-android-trial/android-debug-nordictrack.apk
|
||||
nordictrack-treadmill-android-trial/android-debug-nordictrack-treadmill.apk
|
||||
nordictrack-bike-android-trial/android-debug-nordictrack-bike.apk
|
||||
peloton-bike-plus-android-trial/android-debug-peloton-bike-plus.apk
|
||||
peloton-bike-android-trial/android-debug-peloton-bike.apk
|
||||
raspberry-pi-binary/qdomyos-zwift-32bit
|
||||
raspberry-pi-binary/qdomyos-zwift-64bit
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
src/qdomyos-zwift.pro.user
|
||||
|
||||
.idea/
|
||||
|
||||
src/Makefile
|
||||
|
||||
@@ -371,4 +371,5 @@ The ProForm 995i implementation serves as the reference example:
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
* Add the property at the END of the properties list
|
||||
- #usa le qdebug invece che le emit debug
|
||||
@@ -286,6 +286,8 @@
|
||||
8752C0E92B15D85600C3D1A5 /* ios_eliteariafan.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8752C0E62B15D85600C3D1A5 /* ios_eliteariafan.mm */; };
|
||||
87540FAD2848FD70005E0D44 /* libqtexttospeech_speechios.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */; };
|
||||
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8754D24B27F786F0003D7054 /* virtualrower.swift */; };
|
||||
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */; };
|
||||
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D22E4E260B006A12E4 /* fontmanager.cpp */; };
|
||||
87586A4125B8340E00A243C4 /* proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4025B8340E00A243C4 /* proformbike.cpp */; };
|
||||
87586A4325B8341B00A243C4 /* moc_proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4225B8341B00A243C4 /* moc_proformbike.cpp */; };
|
||||
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */; };
|
||||
@@ -340,6 +342,8 @@
|
||||
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9927BE35C4001D7645 /* bowflext216treadmill.cpp */; };
|
||||
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */; };
|
||||
876BFCA127BE35D8001D7645 /* moc_bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */; };
|
||||
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */; };
|
||||
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */; };
|
||||
876E4E142594748000BD5714 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 876E4E132594748000BD5714 /* Assets.xcassets */; };
|
||||
876E4E1B2594748000BD5714 /* watchkit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
876E4E202594748000BD5714 /* qdomyoszwiftApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876E4E1F2594748000BD5714 /* qdomyoszwiftApp.swift */; };
|
||||
@@ -388,6 +392,9 @@
|
||||
8781908526150C8E0085E656 /* libqtlabsplatformplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 8781908126150B490085E656 /* libqtlabsplatformplugin.a */; };
|
||||
8783153B25E8D81E0007817C /* moc_sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */; };
|
||||
8783153C25E8DAFD0007817C /* sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3EBBA25D2CFED0040EB4C /* sportstechbike.cpp */; };
|
||||
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */; };
|
||||
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */; };
|
||||
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */; };
|
||||
878531642711A3E1004B153D /* fakebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531602711A3E0004B153D /* fakebike.cpp */; };
|
||||
878531652711A3E1004B153D /* activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531612711A3E1004B153D /* activiotreadmill.cpp */; };
|
||||
878531682711A3EC004B153D /* moc_activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531662711A3EB004B153D /* moc_activiotreadmill.cpp */; };
|
||||
@@ -454,6 +461,8 @@
|
||||
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A4B75F25AF27CB0027EF3C /* metric.cpp */; };
|
||||
87A6825A2CE3AB3100586A2A /* moc_sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */; };
|
||||
87A6825D2CE3AB4000586A2A /* sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */; };
|
||||
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */; };
|
||||
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */; };
|
||||
87ADD2BB27634C1500B7A0AB /* technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */; };
|
||||
87ADD2BD27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */; };
|
||||
87AE0CB227760DCB00E547E9 /* virtualtreadmill_zwift.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87AE0CB127760DCB00E547E9 /* virtualtreadmill_zwift.swift */; };
|
||||
@@ -580,6 +589,12 @@
|
||||
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
|
||||
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
|
||||
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
|
||||
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
|
||||
@@ -1196,6 +1211,9 @@
|
||||
8752C0E72B15D85600C3D1A5 /* ios_eliteariafan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ios_eliteariafan.h; path = ../src/ios/ios_eliteariafan.h; sourceTree = "<group>"; };
|
||||
87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtexttospeech_speechios.a; path = ../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios.a; sourceTree = "<group>"; };
|
||||
8754D24B27F786F0003D7054 /* virtualrower.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = virtualrower.swift; path = ../src/ios/virtualrower.swift; sourceTree = "<group>"; };
|
||||
8755E5D12E4E260B006A12E4 /* fontmanager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fontmanager.h; path = ../src/fontmanager.h; sourceTree = SOURCE_ROOT; };
|
||||
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fontmanager.cpp; path = ../src/fontmanager.cpp; sourceTree = SOURCE_ROOT; };
|
||||
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fontmanager.cpp; sourceTree = "<group>"; };
|
||||
87586A3F25B8340D00A243C4 /* proformbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformbike.h; path = ../src/devices/proformbike/proformbike.h; sourceTree = "<group>"; };
|
||||
87586A4025B8340E00A243C4 /* proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformbike.cpp; path = ../src/devices/proformbike/proformbike.cpp; sourceTree = "<group>"; };
|
||||
87586A4225B8341B00A243C4 /* moc_proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformbike.cpp; sourceTree = "<group>"; };
|
||||
@@ -1282,6 +1300,9 @@
|
||||
876BFC9B27BE35C5001D7645 /* proformelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformelliptical.h; path = ../src/devices/proformelliptical/proformelliptical.h; sourceTree = "<group>"; };
|
||||
876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformelliptical.cpp; sourceTree = "<group>"; };
|
||||
876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bowflext216treadmill.cpp; sourceTree = "<group>"; };
|
||||
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitbackupwriter.h; path = ../src/fitbackupwriter.h; sourceTree = SOURCE_ROOT; };
|
||||
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitbackupwriter.cpp; path = ../src/fitbackupwriter.cpp; sourceTree = SOURCE_ROOT; };
|
||||
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitbackupwriter.cpp; sourceTree = "<group>"; };
|
||||
876E4E112594747F00BD5714 /* watchkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watchkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
876E4E132594748000BD5714 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
876E4E152594748000BD5714 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -1365,6 +1386,10 @@
|
||||
878225C234983ACB863D2D29 /* fit_nmea_sentence_mesg.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_nmea_sentence_mesg.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_nmea_sentence_mesg.hpp"; sourceTree = "<absolute>"; };
|
||||
8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechbike.cpp; sourceTree = "<group>"; };
|
||||
87842E7E25AF88FB00321E69 /* secret.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = secret.h; path = ../src/secret.h; sourceTree = "<group>"; };
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtlabscalendarplugin.a; path = ../../Qt/5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a; sourceTree = "<group>"; };
|
||||
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbrower.cpp; sourceTree = "<group>"; };
|
||||
878521D22E44B26600922796 /* nordictrackifitadbrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbrower.h; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h; sourceTree = SOURCE_ROOT; };
|
||||
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbrower.cpp; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp; sourceTree = SOURCE_ROOT; };
|
||||
878531602711A3E0004B153D /* fakebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakebike.cpp; path = ../src/devices/fakebike/fakebike.cpp; sourceTree = "<group>"; };
|
||||
878531612711A3E1004B153D /* activiotreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = activiotreadmill.cpp; path = ../src/devices/activiotreadmill/activiotreadmill.cpp; sourceTree = "<group>"; };
|
||||
878531622711A3E1004B153D /* activiotreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = activiotreadmill.h; path = ../src/devices/activiotreadmill/activiotreadmill.h; sourceTree = "<group>"; };
|
||||
@@ -1471,6 +1496,9 @@
|
||||
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sramAXSController.cpp; sourceTree = "<group>"; };
|
||||
87A6825B2CE3AB4000586A2A /* sramAXSController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sramAXSController.h; path = ../src/devices/sramAXSController/sramAXSController.h; sourceTree = SOURCE_ROOT; };
|
||||
87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sramAXSController.cpp; path = ../src/devices/sramAXSController/sramAXSController.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = androidstatusbar.h; path = ../src/androidstatusbar.h; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = androidstatusbar.cpp; path = ../src/androidstatusbar.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_androidstatusbar.cpp; sourceTree = "<group>"; };
|
||||
87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = technogymmyruntreadmill.cpp; path = ../src/devices/technogymmyruntreadmill/technogymmyruntreadmill.cpp; sourceTree = "<group>"; };
|
||||
87ADD2BA27634C1400B7A0AB /* technogymmyruntreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = technogymmyruntreadmill.h; path = ../src/devices/technogymmyruntreadmill/technogymmyruntreadmill.h; sourceTree = "<group>"; };
|
||||
87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymmyruntreadmill.cpp; sourceTree = "<group>"; };
|
||||
@@ -1653,6 +1681,15 @@
|
||||
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
|
||||
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
|
||||
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
|
||||
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
@@ -1904,6 +1941,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */,
|
||||
8768C9282BBC13220099DBE1 /* libcrypto.a in Link Binary With Libraries */,
|
||||
87FA94672B6B89FD00B6AB9A /* SwiftUI.framework in Link Binary With Libraries */,
|
||||
879F74112893D5B8009A64C8 /* libqavfcamera.a in Link Binary With Libraries */,
|
||||
@@ -2240,6 +2278,27 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
|
||||
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
|
||||
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
|
||||
8755E5D12E4E260B006A12E4 /* fontmanager.h */,
|
||||
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */,
|
||||
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */,
|
||||
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */,
|
||||
878521D22E44B26600922796 /* nordictrackifitadbrower.h */,
|
||||
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */,
|
||||
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */,
|
||||
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */,
|
||||
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */,
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */,
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */,
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */,
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */,
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */,
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */,
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */,
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */,
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */,
|
||||
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */,
|
||||
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */,
|
||||
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */,
|
||||
@@ -2940,6 +2999,7 @@
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */,
|
||||
8768C9262BBC12D10099DBE1 /* libcrypto.a */,
|
||||
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */,
|
||||
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */,
|
||||
@@ -3680,12 +3740,16 @@
|
||||
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */,
|
||||
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */,
|
||||
3015F9B9FF4CA6D653D46CCA /* fit_developer_field_description.cpp in Compile Sources */,
|
||||
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */,
|
||||
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */,
|
||||
87310B22266FBB78008BA0D6 /* moc_homefitnessbuddy.cpp in Compile Sources */,
|
||||
87958F1B27628D5400124B24 /* moc_elitesterzosmart.cpp in Compile Sources */,
|
||||
8768C8D82BBC12890099DBE1 /* centraldir.c in Compile Sources */,
|
||||
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */,
|
||||
87CC3BA425A0885F001EC5A8 /* elliptical.cpp in Compile Sources */,
|
||||
4AD2C93A2B8FD5855E521630 /* fit_encode.cpp in Compile Sources */,
|
||||
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */,
|
||||
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */,
|
||||
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
|
||||
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
|
||||
@@ -3805,6 +3869,12 @@
|
||||
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
|
||||
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
|
||||
27E452D452B62D0948DF0755 /* sessionline.cpp in Compile Sources */,
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */,
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */,
|
||||
E40895A73216AC52D35083D9 /* signalhandler.cpp in Compile Sources */,
|
||||
873CD22427EF8E18000131BC /* inappproductqmltype.cpp in Compile Sources */,
|
||||
87DF68BF25E2675100FCDA46 /* moc_schwinnic4bike.cpp in Compile Sources */,
|
||||
@@ -3864,6 +3934,8 @@
|
||||
87440FBF2640292900E4DC0B /* moc_fitplusbike.cpp in Compile Sources */,
|
||||
8768C8CA2BBC11C80099DBE1 /* sockets.c in Compile Sources */,
|
||||
87B617EC25F25FED0094A1CB /* screencapture.cpp in Compile Sources */,
|
||||
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */,
|
||||
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */,
|
||||
876F9B5F275385C9006AE6FA /* fitmetria_fanfit.cpp in Compile Sources */,
|
||||
FB2566376FE0FB17ED3DE94D /* FitDeveloperField.mm in Compile Sources */,
|
||||
43FA2D5EA73D9C89F1A333B6 /* FitEncode.mm in Compile Sources */,
|
||||
@@ -3880,6 +3952,8 @@
|
||||
2B42755BF45173E11E2110CB /* FitFieldDefinition.mm in Compile Sources */,
|
||||
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
|
||||
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
|
||||
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
|
||||
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
|
||||
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
|
||||
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
|
||||
872261F0289EA887006A6F75 /* moc_nordictrackelliptical.cpp in Compile Sources */,
|
||||
@@ -4381,7 +4455,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1121;
|
||||
CURRENT_PROJECT_VERSION = 1165;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4417,6 +4492,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4462,6 +4538,7 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
|
||||
);
|
||||
MARKETING_VERSION = 2.20;
|
||||
OTHER_CFLAGS = (
|
||||
@@ -4557,6 +4634,9 @@
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = "";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_INSTALL_OBJC_HEADER = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
|
||||
@@ -4575,8 +4655,9 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1121;
|
||||
CURRENT_PROJECT_VERSION = 1165;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
@@ -4613,6 +4694,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4658,6 +4740,7 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
|
||||
);
|
||||
MARKETING_VERSION = 2.20;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -4754,6 +4837,9 @@
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = _debug;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_INSTALL_OBJC_HEADER = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
|
||||
@@ -4805,7 +4891,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1121;
|
||||
CURRENT_PROJECT_VERSION = 1165;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4901,7 +4987,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1121;
|
||||
CURRENT_PROJECT_VERSION = 1165;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -4993,7 +5079,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1121;
|
||||
CURRENT_PROJECT_VERSION = 1166;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5109,7 +5195,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1121;
|
||||
CURRENT_PROJECT_VERSION = 1166;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
|
||||
@@ -23,6 +23,7 @@ class WatchKitConnection: NSObject {
|
||||
static let shared = WatchKitConnection()
|
||||
public static var distance = 0.0
|
||||
public static var kcal = 0.0
|
||||
public static var totalKcal = 0.0
|
||||
public static var stepCadence = 0
|
||||
public static var speed = 0.0
|
||||
public static var cadence = 0.0
|
||||
@@ -70,6 +71,9 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
WatchKitConnection.distance = dDistance
|
||||
let dKcal = Double(result["kcal"] as! Double)
|
||||
WatchKitConnection.kcal = dKcal
|
||||
if let totalKcalDouble = result["totalKcal"] as? Double {
|
||||
WatchKitConnection.totalKcal = totalKcalDouble
|
||||
}
|
||||
|
||||
let dSpeed = Double(result["speed"] as! Double)
|
||||
WatchKitConnection.speed = dSpeed
|
||||
|
||||
@@ -28,6 +28,7 @@ class WorkoutTracking: NSObject {
|
||||
static let shared = WorkoutTracking()
|
||||
public static var distance = Double()
|
||||
public static var kcal = Double()
|
||||
public static var totalKcal = Double()
|
||||
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
|
||||
public static var cadenceLastSteps = Double()
|
||||
public static var cadenceSteps = 0
|
||||
@@ -166,6 +167,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
|
||||
@@ -185,6 +187,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
}
|
||||
@@ -223,25 +226,30 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
workoutSession.stopActivity(with: Date())
|
||||
workoutSession.end()
|
||||
|
||||
guard let quantityType = HKQuantityType.quantityType(
|
||||
// Write active calories
|
||||
guard let activeQuantityType = HKQuantityType.quantityType(
|
||||
forIdentifier: .activeEnergyBurned) else {
|
||||
return
|
||||
}
|
||||
|
||||
let unit = HKUnit.kilocalorie()
|
||||
let totalEnergyBurned = WorkoutTracking.kcal
|
||||
let quantity = HKQuantity(unit: unit,
|
||||
doubleValue: totalEnergyBurned)
|
||||
let activeEnergyBurned = WorkoutTracking.kcal
|
||||
let activeQuantity = HKQuantity(unit: unit,
|
||||
doubleValue: activeEnergyBurned)
|
||||
|
||||
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
|
||||
|
||||
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
|
||||
quantity: quantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
|
||||
quantity: activeQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sample]) {(success, error) in}
|
||||
|
||||
workoutBuilder.add([activeSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
let unitDistance = HKUnit.mile()
|
||||
let miles = WorkoutTracking.distance
|
||||
let quantityMiles = HKQuantity(unit: unitDistance,
|
||||
@@ -273,6 +281,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +346,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,6 +415,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia sql
|
||||
QTPLUGIN += qavfmediaplayer
|
||||
QT+= charts
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ These instructions build the app itself, not the test project.
|
||||
|
||||
```buildoutcfg
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
$ cd qdomyos-zwift
|
||||
$ git submodule update --init src/smtpclient/
|
||||
@@ -106,7 +106,7 @@ This operation takes a moment to complete.
|
||||
#### Install qdomyos-zwift from sources
|
||||
|
||||
```bash
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
cd qdomyos-zwift
|
||||
git submodule update --init src/smtpclient/
|
||||
|
||||
@@ -9,6 +9,7 @@ ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
property int chart_display_mode: 0
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
@@ -19,6 +20,9 @@ ColumnLayout {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
} else if (loadRequest.status === WebView.LoadSucceededStatus) {
|
||||
// Send chart display mode to the web view
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
@@ -28,4 +32,22 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes in chart display mode setting
|
||||
Connections {
|
||||
target: settings
|
||||
function onChart_display_modeChanged() {
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
|
||||
function sendDisplayModeToWebView() {
|
||||
if (webView.loading === false) {
|
||||
webView.runJavaScript("
|
||||
if (window.setChartDisplayMode) {
|
||||
window.setChartDisplayMode(" + settings.chart_display_mode + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,22 +13,32 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +273,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'gpx')
|
||||
fileDialogTrainProgram.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
51
src/PreviewChart.qml
Normal file
51
src/PreviewChart.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal popupclose()
|
||||
id: column1
|
||||
spacing: 10
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/previewchart/chart.htm"
|
||||
visible: true
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: chartJscheckStartFromWeb
|
||||
interval: 200; running: true; repeat: true
|
||||
onTriggered: {if(rootItem.startRequested) {rootItem.startRequested = false; rootItem.stopRequested = false; stackView.pop(); }}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeButton
|
||||
height: 50
|
||||
width: parent.width
|
||||
text: "Close"
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
popupclose();
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
headerToolbar.visible = true;
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,28 @@ import QtQuick.Dialogs 1.0
|
||||
|
||||
ColumnLayout {
|
||||
signal loadSettings(url name)
|
||||
FileDialog {
|
||||
id: fileDialogSettings
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogSettings.fileUrl)
|
||||
loadSettings(fileDialogSettings.fileUrl)
|
||||
fileDialogSettings.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogSettings.close()
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
loadSettings(fileUrl)
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +116,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'settings')
|
||||
fileDialogSettings.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import QtQuick 2.0
|
||||
import AndroidStatusBar 1.0
|
||||
import QtQuick.Window 2.12
|
||||
|
||||
/**
|
||||
* adapted from StackOverflow:
|
||||
@@ -29,7 +31,9 @@ ListView {
|
||||
z: Infinity
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 10
|
||||
anchors.bottomMargin: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ?
|
||||
((Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.navigationBarHeight + 10 : 10) : 10
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
interactive: false
|
||||
|
||||
@@ -11,22 +11,32 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +306,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
|
||||
fileDialogTrainProgram.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
@@ -845,7 +845,6 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 0.5;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
@@ -904,7 +903,6 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 1;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
|
||||
71
src/WorkoutTypeTag.qml
Normal file
71
src/WorkoutTypeTag.qml
Normal file
@@ -0,0 +1,71 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string workoutSource: "QZ"
|
||||
property alias text: tagText.text
|
||||
|
||||
// Auto-size based on text
|
||||
width: tagText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
|
||||
// Color scheme based on workout source
|
||||
color: {
|
||||
switch(workoutSource.toUpperCase()) {
|
||||
case "PELOTON": return "#ff6b35"
|
||||
case "ZWIFT": return "#ff6900"
|
||||
case "ERG": return "#8bc34a"
|
||||
case "QZ": return "#2196f3"
|
||||
case "MANUAL": return "#757575"
|
||||
default: return "#9e9e9e"
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle border for better definition
|
||||
border.color: Qt.darker(color, 1.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: tagText
|
||||
anchors.centerIn: parent
|
||||
text: workoutSource.toUpperCase()
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: "Arial"
|
||||
}
|
||||
|
||||
// Subtle shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 1
|
||||
anchors.leftMargin: 1
|
||||
radius: parent.radius
|
||||
color: "#20000000"
|
||||
z: -1
|
||||
}
|
||||
|
||||
// Hover effect for interactivity feedback
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
910
src/WorkoutsHistory.qml
Normal file
910
src/WorkoutsHistory.qml
Normal file
@@ -0,0 +1,910 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtCharts 2.15
|
||||
import Qt.labs.calendar 1.0
|
||||
|
||||
Page {
|
||||
id: workoutHistoryPage
|
||||
|
||||
|
||||
// Signal for chart preview
|
||||
signal fitfile_preview_clicked(var url)
|
||||
|
||||
// Helper function to wrap text with emoji font only on Android
|
||||
function wrapEmoji(emoji) {
|
||||
return Qt.platform.os === "android" ?
|
||||
'<font face="' + fontManager.emojiFontFamily + '">' + emoji + '</font>' :
|
||||
emoji;
|
||||
}
|
||||
|
||||
// Sport type to icon mapping (using FIT_SPORT values)
|
||||
function getSportIcon(sport) {
|
||||
switch(parseInt(sport)) {
|
||||
case 1: // FIT_SPORT_RUNNING
|
||||
case 11: // FIT_SPORT_WALKING
|
||||
return "🏃"; // Running/Walking
|
||||
case 2: // FIT_SPORT_CYCLING
|
||||
return "🚴"; // Cycling
|
||||
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
|
||||
return "⭕"; // Elliptical
|
||||
case 15: // FIT_SPORT_ROWING
|
||||
return "🚣"; // Rowing
|
||||
case 84: // FIT_SPORT_JUMPROPE
|
||||
return "🪢"; // Jump Rope
|
||||
default:
|
||||
return "💪"; // Generic workout
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 60
|
||||
color: "#f5f5f5"
|
||||
|
||||
// Calendar Icon Button - positioned absolutely on the left
|
||||
Button {
|
||||
id: calendarButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: calendarButton.pressed ? "#e0e0e0" : "#f0f0f0"
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📅") :
|
||||
"📅"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 20
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
calendarPopup.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Title with filter status - centered
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Workout History"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: workoutModel && workoutModel.isDateFiltered ?
|
||||
"Filtered: " + workoutModel.filteredDate.toLocaleDateString() : ""
|
||||
font.pixelSize: 12
|
||||
color: "#666666"
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Filter Button - positioned absolutely on the right
|
||||
Button {
|
||||
id: clearFilterButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 12
|
||||
width: 100
|
||||
height: 36
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
|
||||
background: Rectangle {
|
||||
radius: 6
|
||||
color: clearFilterButton.pressed ? "#ff6666" : "#ff8888"
|
||||
border.color: "#ff4444"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "Clear Filter"
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.clearDateFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
BusyIndicator {
|
||||
id: loadingIndicator
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? (workoutModel.isLoading || workoutModel.isDatabaseProcessing) : false
|
||||
running: visible
|
||||
}
|
||||
|
||||
// Database processing message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? workoutModel.isDatabaseProcessing : false
|
||||
text: "Processing workout files...\nThis may take a few moments on first startup."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#666666"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
// Workout List
|
||||
ListView {
|
||||
id: workoutListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: streakBanner.visible ? streakBanner.height + 10 : 10
|
||||
model: workoutModel
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
onContentYChanged: {
|
||||
// Hide banner when scrolling down, show when at top
|
||||
streakBanner.visible = contentY <= 20
|
||||
}
|
||||
|
||||
delegate: SwipeDelegate {
|
||||
id: swipeDelegate
|
||||
width: parent.width
|
||||
height: 135
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Delegate data:", JSON.stringify({
|
||||
sport: sport,
|
||||
title: title,
|
||||
date: date,
|
||||
duration: duration,
|
||||
distance: distance,
|
||||
calories: calories,
|
||||
id: id
|
||||
}))
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: "#FF4444"
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 20
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🗑️") + " Delete" :
|
||||
"🗑️ Delete"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
color: "white"
|
||||
font.pixelSize: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipe.onCompleted: {
|
||||
// Show confirmation dialog
|
||||
confirmDialog.workoutId = model.id
|
||||
confirmDialog.workoutTitle = model.title
|
||||
confirmDialog.open()
|
||||
}
|
||||
|
||||
// Card-like container
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
radius: 10
|
||||
color: "white"
|
||||
border.color: "#e0e0e0"
|
||||
|
||||
// Workout Type Tag - positioned absolutely in top-right
|
||||
WorkoutTypeTag {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
workoutSource: workoutModel ? workoutModel.getWorkoutSource(model.id) : "QZ"
|
||||
}
|
||||
|
||||
// Action buttons - positioned absolutely in bottom-right
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
// Peloton URL button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.getWorkoutSource(model.id) === "PELOTON" &&
|
||||
workoutModel.getPelotonUrl(model.id) !== ""
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#ff8855" : "#ff6b35"
|
||||
radius: 6
|
||||
border.color: "#cc5529"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🌐") :
|
||||
"🌐"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.openPelotonUrl(model.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.hasTrainingProgram(model.id)
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#1976d2" : "#2196f3"
|
||||
radius: 6
|
||||
border.color: "#1565c0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📋") :
|
||||
"📋"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var success = workoutModel.loadTrainingProgram(model.id)
|
||||
if (success) {
|
||||
trainingProgramDialog.title = "Success"
|
||||
trainingProgramDialog.message = "Training program loaded successfully!"
|
||||
trainingProgramDialog.isSuccess = true
|
||||
} else {
|
||||
trainingProgramDialog.title = "Error"
|
||||
trainingProgramDialog.message = "Failed to load training program. Please check if the file exists."
|
||||
trainingProgramDialog.isSuccess = false
|
||||
}
|
||||
trainingProgramDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
// Sport icon
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji(getSportIcon(sport)) :
|
||||
getSportIcon(sport)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 32
|
||||
}
|
||||
}
|
||||
|
||||
// Workout info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
// Title row (without tag) with auto-scrolling
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 80 // Reserve space for tag
|
||||
Layout.preferredHeight: 24
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
text: title
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Auto-scroll animation for long titles
|
||||
SequentialAnimation on x {
|
||||
running: titleText.contentWidth > titleText.parent.width
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 1500 }
|
||||
NumberAnimation {
|
||||
from: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
to: 0
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 2000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date
|
||||
color: "#666666"
|
||||
}
|
||||
|
||||
// Stats row
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "⏱ " + duration
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "📏 " + distance.toFixed(2) + " km"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " + Math.round(calories) + " kcal" :
|
||||
"🔥 " + Math.round(calories) + " kcal"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
console.log("Workout clicked, ID:", model.id)
|
||||
|
||||
// Get workout details from the model
|
||||
var details = workoutModel.getWorkoutDetails(model.id)
|
||||
console.log("Workout details:", JSON.stringify(details))
|
||||
|
||||
// Emit signal with file URL for chart preview - same pattern as profiles.qml
|
||||
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
|
||||
// Convert to URL like profiles.qml does with FolderListModel
|
||||
var fileUrl = "file://" + details.filePath
|
||||
console.log("Converted to URL:", fileUrl)
|
||||
workoutHistoryPage.fitfile_preview_clicked(fileUrl)
|
||||
|
||||
// Push the ChartJsTest view
|
||||
stackView.push("PreviewChart.qml")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation Dialog
|
||||
Dialog {
|
||||
id: confirmDialog
|
||||
|
||||
property int workoutId
|
||||
property string workoutTitle
|
||||
|
||||
title: "Delete Workout"
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
Text {
|
||||
text: "Are you sure you want to delete '" + confirmDialog.workoutTitle + "'?"
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
workoutModel.deleteWorkout(confirmDialog.workoutId)
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
onRejected: {
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program Loading Dialog
|
||||
Dialog {
|
||||
id: trainingProgramDialog
|
||||
|
||||
property string message: ""
|
||||
property bool isSuccess: true
|
||||
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 8
|
||||
border.color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
border.width: 2
|
||||
}
|
||||
|
||||
header: Rectangle {
|
||||
height: 50
|
||||
color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
radius: 8
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: trainingProgramDialog.title
|
||||
color: "white"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
Layout.margins: 20
|
||||
Layout.preferredWidth: 300
|
||||
Layout.preferredHeight: 120
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " +
|
||||
wrapEmoji(trainingProgramDialog.isSuccess ? '✅' : '❌') +
|
||||
" " + trainingProgramDialog.message :
|
||||
"🔥 " + (trainingProgramDialog.isSuccess ? '✅ ' : '❌ ') + trainingProgramDialog.message
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streak Banner at the bottom
|
||||
Rectangle {
|
||||
id: streakBanner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 80
|
||||
visible: workoutModel
|
||||
|
||||
Behavior on visible {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Special pulsing effect for major milestones
|
||||
SequentialAnimation on opacity {
|
||||
running: workoutModel && workoutModel.currentStreak >= 30
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0.9; to: 1.0; duration: 1500; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { from: 1.0; to: 0.9; duration: 1500; easing.type: Easing.InOutSine }
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFD700" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#9932CC" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#FF1493" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF4500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF6347" : "#FF6B35"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFA500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#8A2BE2" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#DC143C" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF6B35" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF4500" : "#F7931E"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40FFFFFF" }
|
||||
GradientStop { position: 1.0; color: "#00FFFFFF" }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
// Current streak with count
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 15
|
||||
|
||||
// Fire emoji with animation
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("👑🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🎖️🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🦁🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🎊🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🏆🔥") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "👑🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🎖️🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🦁🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🎊🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🏆🔥" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 7 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 365 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Special sparkle effect for year achievement
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: 360; duration: 3000; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
|
||||
// Current streak count
|
||||
Text {
|
||||
text: workoutModel ? workoutModel.currentStreak + " day" + (workoutModel.currentStreak !== 1 ? "s" : "") + " streak" : ""
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: "white"
|
||||
visible: workoutModel
|
||||
}
|
||||
|
||||
// Another fire emoji
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("🔥👑") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🔥🎖️") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🔥🦁") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🔥🎊") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🔥🏆") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "🔥👑" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🔥🎖️" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🔥🦁" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🔥🎊" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🔥🏆" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 365 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Counter-rotation for variety
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: -360; duration: 3500; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Motivational message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? workoutModel.streakMessage : ""
|
||||
font.pixelSize: 14
|
||||
font.italic: true
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.streakMessage !== ""
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
// Best streak (smaller text)
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? "Personal best: " + workoutModel.longestStreak + " day" + (workoutModel.longestStreak !== 1 ? "s" : "") : ""
|
||||
font.pixelSize: 12
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.longestStreak > workoutModel.currentStreak && workoutModel.longestStreak > 0
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle shadow effect at the top
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40000000" }
|
||||
GradientStop { position: 1.0; color: "#00000000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Popup
|
||||
Popup {
|
||||
id: calendarPopup
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.min(parent.width * 0.9, 400)
|
||||
height: Math.min(parent.height * 0.8, 500)
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: {
|
||||
// Refresh workout dates when calendar opens
|
||||
if (workoutModel) {
|
||||
calendar.workoutDates = workoutModel.getWorkoutDates()
|
||||
console.log("Calendar opened, refreshed workout dates:", JSON.stringify(calendar.workoutDates))
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 12
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
|
||||
// Shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 2
|
||||
anchors.leftMargin: 2
|
||||
radius: parent.radius
|
||||
color: "#40000000"
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Calendar Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
text: "<"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() - 1, 1)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: calendar.selectedDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
text: ">"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Grid
|
||||
GridLayout {
|
||||
id: calendar
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
columns: 7
|
||||
|
||||
property date selectedDate: new Date()
|
||||
property var workoutDates: workoutModel ? workoutModel.getWorkoutDates() : []
|
||||
|
||||
// Debug: print workout dates when they change
|
||||
onWorkoutDatesChanged: {
|
||||
console.log("Calendar workout dates updated:", JSON.stringify(workoutDates))
|
||||
}
|
||||
|
||||
// Day headers
|
||||
Repeater {
|
||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
text: modelData
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: "#666666"
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar days
|
||||
Repeater {
|
||||
model: getCalendarDays()
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 40
|
||||
|
||||
property date dayDate: modelData.date
|
||||
property bool isCurrentMonth: modelData.currentMonth
|
||||
property bool hasWorkout: modelData.hasWorkout
|
||||
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
|
||||
color: {
|
||||
if (mouseArea.pressed) return "#e3f2fd"
|
||||
if (isToday) return "#bbdefb"
|
||||
if (!isCurrentMonth) return "#f5f5f5"
|
||||
return "white"
|
||||
}
|
||||
|
||||
border.color: isToday ? "#2196f3" : "#e0e0e0"
|
||||
border.width: isToday ? 2 : 1
|
||||
radius: 4
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: dayDate.getDate()
|
||||
color: isCurrentMonth ? "black" : "#cccccc"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
// Workout indicator dot
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: "#ff6b35"
|
||||
visible: hasWorkout
|
||||
border.width: 1
|
||||
border.color: "#cc5529"
|
||||
|
||||
// Debug: log when a dot should be visible
|
||||
Component.onCompleted: {
|
||||
if (hasWorkout) {
|
||||
console.log("Workout dot visible for date:", dayDate.toDateString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
if (isCurrentMonth) {
|
||||
var year = dayDate.getFullYear();
|
||||
var month = dayDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = dayDate.getDate();
|
||||
var dateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
workoutModel.setDateFilter(dateString);
|
||||
calendarPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "Close"
|
||||
onClicked: calendarPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript functions for calendar
|
||||
function getCalendarDays() {
|
||||
var days = []
|
||||
var firstDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth(), 1)
|
||||
var lastDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 0)
|
||||
var startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay()) // Go back to start of week
|
||||
|
||||
var workoutDates = calendar.workoutDates || []
|
||||
console.log("getCalendarDays: workoutDates received:", JSON.stringify(workoutDates))
|
||||
|
||||
// workoutDates is now a QStringList (array of strings in format "yyyy-MM-dd")
|
||||
var workoutDateStrings = workoutDates || []
|
||||
console.log("Final workout date strings:", JSON.stringify(workoutDateStrings))
|
||||
|
||||
for (var i = 0; i < 42; i++) { // 6 rows x 7 days
|
||||
var currentDate = new Date(startDate)
|
||||
currentDate.setDate(startDate.getDate() + i)
|
||||
|
||||
// Costruisci la stringa YYYY-MM-DD dai componenti della data locale per evitare problemi di fuso orario
|
||||
var year = currentDate.getFullYear();
|
||||
var month = currentDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = currentDate.getDate();
|
||||
var localDateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
var hasWorkout = workoutDateStrings.indexOf(localDateString) !== -1;
|
||||
if (hasWorkout) {
|
||||
// Questo console.log ora utilizza la stringa della data locale corretta per la corrispondenza
|
||||
console.log("Found workout match for:", localDateString);
|
||||
}
|
||||
|
||||
var isCurrentMonth = currentDate.getMonth() === calendar.selectedDate.getMonth()
|
||||
|
||||
days.push({
|
||||
date: currentDate,
|
||||
currentMonth: isCurrentMonth,
|
||||
hasWorkout: hasWorkout
|
||||
})
|
||||
}
|
||||
|
||||
console.log("getCalendarDays: returning", days.length, "days")
|
||||
return days
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.0" android:versionCode="1121" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.11" android:versionCode="1155" android:installLocation="auto">
|
||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default permissions. -->
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
@@ -106,6 +106,16 @@
|
||||
android:name=".ScreenCaptureService"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<service android:name=".VirtualGearingService"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.accessibilityservice"
|
||||
android:resource="@xml/virtual_gearing_service_config" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="ocr" />
|
||||
|
||||
4
src/android/res/values/strings.xml
Normal file
4
src/android/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="virtual_gearing_service_description">Virtual Gearing Service for QZ - Enables touch simulation for virtual shifting in cycling apps</string>
|
||||
</resources>
|
||||
10
src/android/res/xml/virtual_gearing_service_config.xml
Normal file
10
src/android/res/xml/virtual_gearing_service_config.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/virtual_gearing_service_description"
|
||||
android:packageNames="@null"
|
||||
android:accessibilityEventTypes="typeAllMask"
|
||||
android:accessibilityFlags="flagDefault"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:notificationTimeout="100"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canPerformGestures="true" />
|
||||
@@ -35,15 +35,21 @@ public class Ant {
|
||||
static boolean bikeRequest = false; // Added bike request flag
|
||||
static boolean garminKey = false;
|
||||
static boolean treadmill = false;
|
||||
static boolean technoGymGroupCycle = false;
|
||||
static int antBikeDeviceNumber = 0;
|
||||
static int antHeartDeviceNumber = 0;
|
||||
|
||||
// Updated antStart method with BikeRequest parameter at the end
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest) {
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
|
||||
QLog.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
bikeRequest = BikeRequest; // Set bike request flag
|
||||
technoGymGroupCycle = TechnoGymGroupCycle;
|
||||
antBikeDeviceNumber = AntBikeDeviceNumber;
|
||||
antHeartDeviceNumber = AntHeartDeviceNumber;
|
||||
activity = a;
|
||||
if(a != null)
|
||||
QLog.v(TAG, "antStart activity is valid");
|
||||
|
||||
116
src/android/src/AppConfiguration.java
Normal file
116
src/android/src/AppConfiguration.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class AppConfiguration {
|
||||
private static final String TAG = "AppConfiguration";
|
||||
|
||||
public static class TouchCoordinate {
|
||||
public final double xPercent;
|
||||
public final double yPercent;
|
||||
|
||||
public TouchCoordinate(double xPercent, double yPercent) {
|
||||
this.xPercent = xPercent;
|
||||
this.yPercent = yPercent;
|
||||
}
|
||||
|
||||
public int getX(int screenWidth) {
|
||||
return (int) (screenWidth * xPercent);
|
||||
}
|
||||
|
||||
public int getY(int screenHeight) {
|
||||
return (int) (screenHeight * yPercent);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AppConfig {
|
||||
public final String appName;
|
||||
public final String packageName;
|
||||
public final TouchCoordinate shiftUp;
|
||||
public final TouchCoordinate shiftDown;
|
||||
|
||||
public AppConfig(String appName, String packageName, TouchCoordinate shiftUp, TouchCoordinate shiftDown) {
|
||||
this.appName = appName;
|
||||
this.packageName = packageName;
|
||||
this.shiftUp = shiftUp;
|
||||
this.shiftDown = shiftDown;
|
||||
}
|
||||
}
|
||||
|
||||
// Predefined configurations based on SwiftControl
|
||||
private static final AppConfig[] SUPPORTED_APPS = {
|
||||
// MyWhoosh - coordinates from SwiftControl repository
|
||||
new AppConfig(
|
||||
"MyWhoosh",
|
||||
"com.mywhoosh.whooshgame",
|
||||
new TouchCoordinate(0.98, 0.94), // Shift Up - bottom right corner
|
||||
new TouchCoordinate(0.80, 0.94) // Shift Down - more to the left
|
||||
),
|
||||
|
||||
// IndieVelo / TrainingPeaks
|
||||
new AppConfig(
|
||||
"IndieVelo",
|
||||
"com.indieVelo.client",
|
||||
new TouchCoordinate(0.66, 0.74), // Shift Up - center right
|
||||
new TouchCoordinate(0.575, 0.74) // Shift Down - center left
|
||||
),
|
||||
|
||||
// Biketerra.com
|
||||
new AppConfig(
|
||||
"Biketerra",
|
||||
"biketerra",
|
||||
new TouchCoordinate(0.8, 0.5), // Generic coordinates for now
|
||||
new TouchCoordinate(0.2, 0.5)
|
||||
),
|
||||
|
||||
// Default configuration for unrecognized apps
|
||||
new AppConfig(
|
||||
"Default",
|
||||
"*",
|
||||
new TouchCoordinate(0.85, 0.9), // Conservative coordinates
|
||||
new TouchCoordinate(0.15, 0.9)
|
||||
)
|
||||
};
|
||||
|
||||
public static AppConfig getConfigForPackage(String packageName) {
|
||||
// Use custom coordinates from settings instead of hardcoded values
|
||||
return getCurrentConfig();
|
||||
}
|
||||
|
||||
// Get current configuration from user settings
|
||||
public static AppConfig getCurrentConfig() {
|
||||
try {
|
||||
double shiftUpX = VirtualGearingBridge.getVirtualGearingShiftUpX();
|
||||
double shiftUpY = VirtualGearingBridge.getVirtualGearingShiftUpY();
|
||||
double shiftDownX = VirtualGearingBridge.getVirtualGearingShiftDownX();
|
||||
double shiftDownY = VirtualGearingBridge.getVirtualGearingShiftDownY();
|
||||
int appIndex = VirtualGearingBridge.getVirtualGearingApp();
|
||||
|
||||
String appName = "Custom";
|
||||
if (appIndex >= 0 && appIndex < SUPPORTED_APPS.length) {
|
||||
appName = SUPPORTED_APPS[appIndex].appName;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "Using custom coordinates: shiftUp(" + shiftUpX + "," + shiftUpY +
|
||||
") shiftDown(" + shiftDownX + "," + shiftDownY + ") for " + appName);
|
||||
|
||||
return new AppConfig(
|
||||
appName,
|
||||
"*", // Package name not relevant for custom config
|
||||
new TouchCoordinate(shiftUpX, shiftUpY),
|
||||
new TouchCoordinate(shiftDownX, shiftDownY)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error getting custom config, using fallback", e);
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public static AppConfig getDefaultConfig() {
|
||||
return SUPPORTED_APPS[SUPPORTED_APPS.length - 1]; // Last element is the default
|
||||
}
|
||||
|
||||
public static AppConfig[] getAllConfigs() {
|
||||
return SUPPORTED_APPS;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,15 @@ import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnes
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentType;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.HeartRateDataSource;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.IRawPowerOnlyDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.ICalculatedPowerReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedSpeedReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedAccumulatedDistanceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.IRawSpeedAndDistanceDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc.ICalculatedCadenceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
|
||||
@@ -29,9 +38,17 @@ public class BikeChannelController {
|
||||
private Context context;
|
||||
private AntPlusFitnessEquipmentPcc fePcc = null;
|
||||
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
|
||||
private AntPlusBikePowerPcc powerPcc = null;
|
||||
private PccReleaseHandle<AntPlusBikePowerPcc> powerReleaseHandle = null;
|
||||
private AntPlusBikeSpeedDistancePcc speedCadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeSpeedDistancePcc> speedCadenceReleaseHandle = null;
|
||||
private AntPlusBikeCadencePcc cadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeCadencePcc> cadenceReleaseHandle = null;
|
||||
private boolean isConnected = false;
|
||||
private boolean isPowerConnected = false;
|
||||
private boolean isSpeedCadenceConnected = false;
|
||||
|
||||
// Bike data fields
|
||||
// Bike data fields - from fitness equipment
|
||||
public int cadence = 0; // Current cadence in RPM
|
||||
public int power = 0; // Current power in watts
|
||||
public BigDecimal speed = new BigDecimal(0); // Current speed in m/s
|
||||
@@ -42,6 +59,12 @@ public class BikeChannelController {
|
||||
public int heartRate = 0; // Heart rate from equipment
|
||||
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
|
||||
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
|
||||
|
||||
// Bike data fields - from dedicated sensors
|
||||
public int powerSensorPower = 0; // Power from dedicated power sensor
|
||||
public int speedSensorCadence = 0; // Cadence from speed/cadence sensor
|
||||
public BigDecimal speedSensorSpeed = new BigDecimal(0); // Speed from speed/cadence sensor
|
||||
public long speedSensorDistance = 0; // Distance from speed/cadence sensor
|
||||
|
||||
// Fitness equipment state receiver
|
||||
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
|
||||
@@ -63,9 +86,18 @@ public class BikeChannelController {
|
||||
}
|
||||
};
|
||||
|
||||
public BikeChannelController() {
|
||||
public BikeChannelController(boolean technoGymGroupCycle, int antBikeDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
|
||||
if (technoGymGroupCycle) {
|
||||
// For Technogym Group Cycle: disable openChannel, enable openPowerSensorChannel
|
||||
openPowerSensorChannel(antBikeDeviceNumber);
|
||||
} else {
|
||||
// Standard behavior: enable openChannel, disable openPowerSensorChannel
|
||||
openChannel();
|
||||
}
|
||||
|
||||
//openSpeedCadenceSensorChannel();
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
@@ -123,6 +155,106 @@ public class BikeChannelController {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public boolean openPowerSensorChannel(int deviceNumber) {
|
||||
// Request access to power sensor device (deviceNumber = 0 means first available)
|
||||
powerReleaseHandle = AntPlusBikePowerPcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusBikePowerPcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikePowerPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
powerPcc = result;
|
||||
isPowerConnected = true;
|
||||
QLog.d(TAG, "Connected to power sensor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToPowerSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Power Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Power Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Power Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Power Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Power Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Power Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized power sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Power Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isPowerConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean openSpeedCadenceSensorChannel() {
|
||||
// Request access to first available speed/cadence sensor device
|
||||
speedCadenceReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess((Activity)context, context,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeSpeedDistancePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
speedCadencePcc = result;
|
||||
isSpeedCadenceConnected = true;
|
||||
QLog.d(TAG, "Connected to speed/cadence sensor: " + result.getDeviceName());
|
||||
subscribeToSpeedCadenceSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Speed/Cadence Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Speed/Cadence Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Speed/Cadence Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Speed/Cadence Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized speed/cadence sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Speed/Cadence Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isSpeedCadenceConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
private void subscribeToBikeEvents() {
|
||||
if (fePcc != null) {
|
||||
// General fitness equipment data
|
||||
@@ -181,36 +313,181 @@ public class BikeChannelController {
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToPowerSensorEvents() {
|
||||
if (powerPcc != null) {
|
||||
// Subscribe to raw power-only data events
|
||||
powerPcc.subscribeRawPowerOnlyDataEvent(new IRawPowerOnlyDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawPowerOnlyData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
long powerOnlyUpdateEventCount, int instantaneousPower,
|
||||
long accumulatedPower) {
|
||||
if (instantaneousPower != -1) {
|
||||
powerSensorPower = instantaneousPower;
|
||||
QLog.d(TAG, "Power Sensor Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also subscribe to calculated power events
|
||||
powerPcc.subscribeCalculatedPowerEvent(new ICalculatedPowerReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedPower(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
AntPlusBikePowerPcc.DataSource dataSource,
|
||||
BigDecimal calculatedPower) {
|
||||
if (calculatedPower != null && calculatedPower.intValue() != -1) {
|
||||
powerSensorPower = calculatedPower.intValue();
|
||||
QLog.d(TAG, "Power Sensor Calculated Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToSpeedCadenceSensorEvents() {
|
||||
if (speedCadencePcc != null) {
|
||||
// 2.095m circumference = average 700cx23mm road tire
|
||||
BigDecimal wheelCircumference = new BigDecimal("2.095");
|
||||
|
||||
// Subscribe to calculated speed events
|
||||
speedCadencePcc.subscribeCalculatedSpeedEvent(new CalculatedSpeedReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedSpeed(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedSpeed) {
|
||||
if (calculatedSpeed != null && calculatedSpeed.doubleValue() > 0) {
|
||||
speedSensorSpeed = calculatedSpeed;
|
||||
QLog.d(TAG, "Speed Sensor Data - Speed: " + speedSensorSpeed + "m/s");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to calculated distance events
|
||||
speedCadencePcc.subscribeCalculatedAccumulatedDistanceEvent(new CalculatedAccumulatedDistanceReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedAccumulatedDistance(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedAccumulatedDistance) {
|
||||
if (calculatedAccumulatedDistance != null && calculatedAccumulatedDistance.longValue() > 0) {
|
||||
speedSensorDistance = calculatedAccumulatedDistance.longValue();
|
||||
QLog.d(TAG, "Speed Sensor Data - Distance: " + speedSensorDistance + "m");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to raw speed and distance data
|
||||
speedCadencePcc.subscribeRawSpeedAndDistanceDataEvent(new IRawSpeedAndDistanceDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawSpeedAndDistanceData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal timestampOfLastEvent, long cumulativeRevolutions) {
|
||||
QLog.d(TAG, "Speed/Distance Raw Data - Revs: " + cumulativeRevolutions + ", Time: " + timestampOfLastEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if this is a combined speed/cadence sensor
|
||||
if (speedCadencePcc.isSpeedAndCadenceCombinedSensor()) {
|
||||
// Connect to cadence functionality
|
||||
cadenceReleaseHandle = AntPlusBikeCadencePcc.requestAccess(
|
||||
(Activity)context, speedCadencePcc.getAntDeviceNumber(), 0, true,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeCadencePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeCadencePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
if (resultCode == RequestAccessResult.SUCCESS) {
|
||||
cadencePcc = result;
|
||||
cadencePcc.subscribeCalculatedCadenceEvent(new ICalculatedCadenceReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedCadence(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedCadence) {
|
||||
if (calculatedCadence != null && calculatedCadence.intValue() > 0) {
|
||||
speedSensorCadence = calculatedCadence.intValue();
|
||||
QLog.d(TAG, "Cadence Sensor Data - Cadence: " + speedSensorCadence + "rpm");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Cadence Sensor State Changed to: " + newDeviceState);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (releaseHandle != null) {
|
||||
releaseHandle.close();
|
||||
releaseHandle = null;
|
||||
}
|
||||
if (powerReleaseHandle != null) {
|
||||
powerReleaseHandle.close();
|
||||
powerReleaseHandle = null;
|
||||
}
|
||||
if (speedCadenceReleaseHandle != null) {
|
||||
speedCadenceReleaseHandle.close();
|
||||
speedCadenceReleaseHandle = null;
|
||||
}
|
||||
if (cadenceReleaseHandle != null) {
|
||||
cadenceReleaseHandle.close();
|
||||
cadenceReleaseHandle = null;
|
||||
}
|
||||
fePcc = null;
|
||||
powerPcc = null;
|
||||
speedCadencePcc = null;
|
||||
cadencePcc = null;
|
||||
isConnected = false;
|
||||
QLog.d(TAG, "Channel Closed");
|
||||
isPowerConnected = false;
|
||||
isSpeedCadenceConnected = false;
|
||||
QLog.d(TAG, "All Channels Closed");
|
||||
}
|
||||
|
||||
// Getter methods for bike data
|
||||
// Getter methods for bike data with sensor reconciliation
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
// Priority: 1) Fitness Equipment, 2) Speed/Cadence Sensor, 3) Power Sensor
|
||||
if (isConnected && cadence > 0) {
|
||||
return cadence; // From fitness equipment
|
||||
} else if (isSpeedCadenceConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From dedicated speed/cadence sensor
|
||||
} else if (isPowerConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From power sensor (if it provides cadence)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getPower() {
|
||||
return power;
|
||||
// Priority: 1) Dedicated Power Sensor, 2) Fitness Equipment
|
||||
if (isPowerConnected && powerSensorPower > 0) {
|
||||
return powerSensorPower; // From dedicated power sensor (most accurate)
|
||||
} else if (isConnected && power > 0) {
|
||||
return power; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double getSpeedKph() {
|
||||
// Convert from m/s to km/h
|
||||
return speed.doubleValue() * 3.6;
|
||||
return getSpeedMps() * 3.6;
|
||||
}
|
||||
|
||||
public double getSpeedMps() {
|
||||
return speed.doubleValue();
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorSpeed.doubleValue() > 0) {
|
||||
return speedSensorSpeed.doubleValue(); // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && speed.doubleValue() > 0) {
|
||||
return speed.doubleValue(); // From fitness equipment
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public long getDistance() {
|
||||
return distance;
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorDistance > 0) {
|
||||
return speedSensorDistance; // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && distance > 0) {
|
||||
return distance; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long getCalories() {
|
||||
@@ -236,4 +513,50 @@ public class BikeChannelController {
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
// Additional connection status methods
|
||||
public boolean isPowerSensorConnected() {
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean isSpeedCadenceSensorConnected() {
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
public boolean isAnyDeviceConnected() {
|
||||
return isConnected || isPowerConnected || isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
// Raw sensor data getters (for debugging/advanced use)
|
||||
public int getRawFitnessEquipmentPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public int getRawPowerSensorPower() {
|
||||
return powerSensorPower;
|
||||
}
|
||||
|
||||
public int getRawFitnessEquipmentCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public int getRawSpeedSensorCadence() {
|
||||
return speedSensorCadence;
|
||||
}
|
||||
|
||||
public double getRawFitnessEquipmentSpeed() {
|
||||
return speed.doubleValue();
|
||||
}
|
||||
|
||||
public double getRawSpeedSensorSpeed() {
|
||||
return speedSensorSpeed.doubleValue();
|
||||
}
|
||||
|
||||
public long getRawFitnessEquipmentDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public long getRawSpeedSensorDistance() {
|
||||
return speedSensorDistance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ public class ChannelService extends Service {
|
||||
|
||||
public void openAllChannels() throws ChannelNotAvailableException {
|
||||
if (Ant.heartRequest && heartChannelController == null)
|
||||
heartChannelController = new HeartChannelController();
|
||||
heartChannelController = new HeartChannelController(Ant.antHeartDeviceNumber);
|
||||
|
||||
if (Ant.speedRequest) {
|
||||
if(Ant.treadmill && sdmChannelController == null) {
|
||||
@@ -330,7 +330,7 @@ public class ChannelService extends Service {
|
||||
|
||||
// Add initialization for BikeChannelController (receiver)
|
||||
if (Ant.bikeRequest && bikeChannelController == null) {
|
||||
bikeChannelController = new BikeChannelController();
|
||||
bikeChannelController = new BikeChannelController(Ant.technoGymGroupCycle, Ant.antBikeDeviceNumber);
|
||||
}
|
||||
|
||||
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
|
||||
|
||||
@@ -2,33 +2,90 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.view.WindowManager;
|
||||
import android.view.DisplayCutout;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
|
||||
public class CustomQtActivity extends QtActivity {
|
||||
private static final String TAG = "CustomQtActivity";
|
||||
|
||||
// Declare the native method that will be implemented in C++
|
||||
private static native void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Handle Android 16 API 36 WindowInsetsController for fullscreen support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11 (API 30) and above - use WindowInsetsController
|
||||
getWindow().setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController controller = getWindow().getDecorView().getWindowInsetsController();
|
||||
if (controller != null) {
|
||||
controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
} else {
|
||||
// Fallback for older Android versions (API < 30)
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
);
|
||||
Log.d(TAG, "onCreate: CustomQtActivity initialized");
|
||||
|
||||
// This tells the OS that we want to handle the display cutout area ourselves
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
// This is the core of the new solution. We set a listener on the main view.
|
||||
// The OS will call this listener whenever the insets change (e.g., on rotation).
|
||||
final View decorView = getWindow().getDecorView();
|
||||
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
|
||||
if (density > 0) {
|
||||
// Use system window insets as primary source
|
||||
top = Math.round(insets.getSystemWindowInsetTop() / density);
|
||||
bottom = Math.round(insets.getSystemWindowInsetBottom() / density);
|
||||
left = Math.round(insets.getSystemWindowInsetLeft() / density);
|
||||
right = Math.round(insets.getSystemWindowInsetRight() / density);
|
||||
|
||||
// For API 28+, also check display cutout for additional padding
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
// Use the maximum between system window inset and cutout safe inset
|
||||
left = Math.max(left, Math.round(cutout.getSafeInsetLeft() / density));
|
||||
right = Math.max(right, Math.round(cutout.getSafeInsetRight() / density));
|
||||
top = Math.max(top, Math.round(cutout.getSafeInsetTop() / density));
|
||||
bottom = Math.max(bottom, Math.round(cutout.getSafeInsetBottom() / density));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "onApplyWindowInsets - Top:" + top + " Bottom:" + bottom + " Left:" + left + " Right:" + right);
|
||||
Log.d(TAG, "Raw insets - SystemTop:" + insets.getSystemWindowInsetTop() +
|
||||
" SystemBottom:" + insets.getSystemWindowInsetBottom() +
|
||||
" SystemLeft:" + insets.getSystemWindowInsetLeft() +
|
||||
" SystemRight:" + insets.getSystemWindowInsetRight());
|
||||
Log.d(TAG, "Stable insets - StableTop:" + insets.getStableInsetTop() +
|
||||
" StableBottom:" + insets.getStableInsetBottom() +
|
||||
" StableLeft:" + insets.getStableInsetLeft() +
|
||||
" StableRight:" + insets.getStableInsetRight());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
Log.d(TAG, "Cutout insets - Top:" + cutout.getSafeInsetTop() +
|
||||
" Bottom:" + cutout.getSafeInsetBottom() +
|
||||
" Left:" + cutout.getSafeInsetLeft() +
|
||||
" Right:" + cutout.getSafeInsetRight());
|
||||
}
|
||||
}
|
||||
|
||||
// Push the new, correct inset values to the C++ layer
|
||||
onInsetsChanged(top, bottom, left, right);
|
||||
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This method is still needed for the QML check
|
||||
public static int getApiLevel() {
|
||||
return Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
public class FloatingWindowGFG extends Service {
|
||||
|
||||
@@ -37,6 +41,14 @@ public class FloatingWindowGFG extends Service {
|
||||
private WindowManager.LayoutParams floatWindowLayoutParam;
|
||||
private WindowManager windowManager;
|
||||
private Button maximizeBtn;
|
||||
private Handler handler;
|
||||
private Runnable paddingTimeoutRunnable;
|
||||
private boolean isDraggingEnabled = false;
|
||||
private int originalHeight;
|
||||
private boolean isExpanded = false;
|
||||
private WebView webView;
|
||||
private int originalMargin = 20; // in dp, matching the XML layout
|
||||
private int reducedMargin = 2; // minimal margin when not dragging
|
||||
|
||||
// Retrieve the user preference node for the package com.mycompany
|
||||
SharedPreferences sharedPreferences;
|
||||
@@ -56,6 +68,9 @@ public class FloatingWindowGFG extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Initialize handler for timeout operations
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// The screen height and width are calculated, cause
|
||||
// the height and width of the floating window is set depending on this
|
||||
/*DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
|
||||
@@ -73,23 +88,30 @@ public class FloatingWindowGFG extends Service {
|
||||
// inflate a new view hierarchy from the floating_layout xml
|
||||
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
|
||||
|
||||
WebView wv = (WebView)floatView.findViewById(R.id.webview);
|
||||
wv.setWebViewClient(new WebViewClient(){
|
||||
webView = (WebView)floatView.findViewById(R.id.webview);
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
WebSettings settings = wv.getSettings();
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
wv.clearView();
|
||||
wv.measure(100, 100);
|
||||
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
|
||||
// Add JavaScript interface for communication with HTML
|
||||
webView.addJavascriptInterface(new WebAppInterface(), "Android");
|
||||
|
||||
webView.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
webView.clearView();
|
||||
webView.measure(100, 100);
|
||||
webView.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
QLog.d("QZ","loadurl");
|
||||
|
||||
// Initially set reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
|
||||
|
||||
// WindowManager.LayoutParams takes a lot of parameters to set the
|
||||
@@ -116,17 +138,18 @@ public class FloatingWindowGFG extends Service {
|
||||
// 5) Next parameter is Layout_Format. System chooses a format that supports
|
||||
// translucency by PixelFormat.TRANSLUCENT
|
||||
|
||||
originalHeight = FloatingHandler._height;
|
||||
floatWindowLayoutParam = new WindowManager.LayoutParams(
|
||||
(int) (FloatingHandler._width ),
|
||||
(int) (FloatingHandler._height ),
|
||||
(int) (originalHeight ),
|
||||
LAYOUT_TYPE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
// The Gravity of the Floating Window is set.
|
||||
// The Window will appear in the center of the screen
|
||||
floatWindowLayoutParam.gravity = Gravity.CENTER;
|
||||
// Use TOP | LEFT for free positioning without constraints
|
||||
floatWindowLayoutParam.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
|
||||
// X and Y value of the window is set
|
||||
floatWindowLayoutParam.x = 0;
|
||||
@@ -145,48 +168,86 @@ public class FloatingWindowGFG extends Service {
|
||||
// The window can be moved at any position on the screen.
|
||||
floatView.setOnTouchListener(new View.OnTouchListener() {
|
||||
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
|
||||
double x;
|
||||
double y;
|
||||
double px;
|
||||
double py;
|
||||
int initialX;
|
||||
int initialY;
|
||||
float initialTouchX;
|
||||
float initialTouchY;
|
||||
boolean isDragging = false;
|
||||
final int TOUCH_THRESHOLD = 10; // Threshold for distinguishing tap vs drag
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
||||
QLog.d("QZ","onTouch");
|
||||
QLog.d("QZ","onTouch action: " + event.getAction());
|
||||
|
||||
switch (event.getAction()) {
|
||||
// When the window will be touched,
|
||||
// the x and y position of that position
|
||||
// will be retrieved
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
x = floatWindowLayoutUpdateParam.x;
|
||||
y = floatWindowLayoutUpdateParam.y;
|
||||
|
||||
// returns the original raw X
|
||||
// coordinate of this event
|
||||
px = event.getRawX();
|
||||
|
||||
// returns the original raw Y
|
||||
// coordinate of this event
|
||||
py = event.getRawY();
|
||||
// Store initial positions
|
||||
initialX = floatWindowLayoutUpdateParam.x;
|
||||
initialY = floatWindowLayoutUpdateParam.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
isDragging = false;
|
||||
|
||||
// Enable dragging for 5 seconds
|
||||
enableDraggingTemporarily();
|
||||
break;
|
||||
// When the window will be dragged around,
|
||||
// it will update the x, y of the Window Layout Parameter
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
|
||||
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
|
||||
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.commit();
|
||||
|
||||
// updated parameter is applied to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
break;
|
||||
// Calculate distance moved
|
||||
float deltaX = event.getRawX() - initialTouchX;
|
||||
float deltaY = event.getRawY() - initialTouchY;
|
||||
|
||||
// Check if we've moved enough to consider this a drag
|
||||
if (!isDragging && (Math.abs(deltaX) > TOUCH_THRESHOLD || Math.abs(deltaY) > TOUCH_THRESHOLD)) {
|
||||
isDragging = true;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Only allow dragging if it's temporarily enabled
|
||||
if (isDragging && isDraggingEnabled) {
|
||||
// Get screen dimensions for boundary checking
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
// Calculate new position
|
||||
int newX = initialX + (int) deltaX;
|
||||
int newY = initialY + (int) deltaY;
|
||||
|
||||
// Apply boundary constraints
|
||||
// Keep window within screen bounds
|
||||
int windowWidth = FloatingHandler._width;
|
||||
int windowHeight = FloatingHandler._height;
|
||||
|
||||
if (newX < 0) newX = 0;
|
||||
if (newY < 0) newY = 0;
|
||||
if (newX + windowWidth > screenWidth) newX = screenWidth - windowWidth;
|
||||
if (newY + windowHeight > screenHeight) newY = screenHeight - windowHeight;
|
||||
|
||||
// Update position
|
||||
floatWindowLayoutUpdateParam.x = newX;
|
||||
floatWindowLayoutUpdateParam.y = newY;
|
||||
|
||||
// Save position to preferences
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.apply(); // Use apply() instead of commit() for better performance
|
||||
|
||||
// Apply updated parameter to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
// If it wasn't a drag, it's a tap - let the WebView handle it
|
||||
if (!isDragging) {
|
||||
return false; // Let the event propagate to WebView
|
||||
}
|
||||
isDragging = false;
|
||||
break;
|
||||
}
|
||||
return isDragging && isDraggingEnabled; // Consume the event only if we're dragging and dragging is enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -200,4 +261,107 @@ public class FloatingWindowGFG extends Service {
|
||||
// Window is removed from the screen
|
||||
windowManager.removeView(floatView);
|
||||
}
|
||||
|
||||
// Method to enable dragging temporarily for 5 seconds
|
||||
private void enableDraggingTemporarily() {
|
||||
isDraggingEnabled = true;
|
||||
|
||||
// Increase margin for better dragging experience
|
||||
setWebViewMargin(originalMargin);
|
||||
|
||||
// Cancel any existing timeout
|
||||
if (paddingTimeoutRunnable != null) {
|
||||
handler.removeCallbacks(paddingTimeoutRunnable);
|
||||
}
|
||||
|
||||
// Create new timeout runnable
|
||||
paddingTimeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
isDraggingEnabled = false;
|
||||
// Restore reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
QLog.d("QZ", "Dragging disabled after timeout, margin restored");
|
||||
}
|
||||
};
|
||||
|
||||
// Schedule timeout for 5 seconds
|
||||
handler.postDelayed(paddingTimeoutRunnable, 5000);
|
||||
}
|
||||
|
||||
// Method to expand window height dynamically
|
||||
private void expandWindow(int additionalHeight) {
|
||||
if (!isExpanded) {
|
||||
isExpanded = true;
|
||||
floatWindowLayoutParam.height = originalHeight + additionalHeight;
|
||||
|
||||
// Adjust Y position to keep window within screen bounds
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
if (floatWindowLayoutParam.y + floatWindowLayoutParam.height > screenHeight) {
|
||||
floatWindowLayoutParam.y = screenHeight - floatWindowLayoutParam.height;
|
||||
if (floatWindowLayoutParam.y < 0) {
|
||||
floatWindowLayoutParam.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window expanded to height: " + floatWindowLayoutParam.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to restore original window height
|
||||
private void restoreWindow() {
|
||||
if (isExpanded) {
|
||||
isExpanded = false;
|
||||
floatWindowLayoutParam.height = originalHeight;
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window restored to original height: " + originalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set WebView margin dynamically
|
||||
private void setWebViewMargin(int marginDp) {
|
||||
if (webView != null) {
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) webView.getLayoutParams();
|
||||
int marginPx = (int) (marginDp * getResources().getDisplayMetrics().density);
|
||||
params.setMargins(marginPx, marginPx, marginPx, marginPx);
|
||||
webView.setLayoutParams(params);
|
||||
QLog.d("QZ", "WebView margin set to: " + marginDp + "dp (" + marginPx + "px)");
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript interface class
|
||||
public class WebAppInterface {
|
||||
@JavascriptInterface
|
||||
public void expandFloatingWindow(int additionalHeight) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
expandWindow(additionalHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void restoreFloatingWindow() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
restoreWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void enableDraggingMargins() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
enableDraggingTemporarily();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@ public class HeartChannelController {
|
||||
private boolean isConnected = false;
|
||||
public int heart = 0; // Public to be accessible from ChannelService
|
||||
|
||||
public HeartChannelController() {
|
||||
public HeartChannelController(int antHeartDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
openChannel(antHeartDeviceNumber);
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
// Request access to first available heart rate device
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, 0, 0, // 0 means first available device
|
||||
public boolean openChannel(int deviceNumber) {
|
||||
// Request access to heart rate device (deviceNumber = 0 means first available)
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
@@ -57,7 +57,7 @@ public class HeartChannelController {
|
||||
case SUCCESS:
|
||||
hrPcc = result;
|
||||
isConnected = true;
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToHrEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
|
||||
@@ -21,77 +21,133 @@ public class QLog {
|
||||
|
||||
// Debug level methods
|
||||
public static int d(String tag, String msg) {
|
||||
sendToQt(3, tag, msg);
|
||||
try {
|
||||
sendToQt(3, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg);
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Error level methods
|
||||
public static int e(String tag, String msg) {
|
||||
sendToQt(6, tag, msg);
|
||||
try {
|
||||
sendToQt(6, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg);
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Info level methods
|
||||
public static int i(String tag, String msg) {
|
||||
sendToQt(4, tag, msg);
|
||||
try {
|
||||
sendToQt(4, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg);
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Verbose level methods
|
||||
public static int v(String tag, String msg) {
|
||||
sendToQt(2, tag, msg);
|
||||
try {
|
||||
sendToQt(2, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg);
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Warning level methods
|
||||
public static int w(String tag, String msg) {
|
||||
sendToQt(5, tag, msg);
|
||||
try {
|
||||
sendToQt(5, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg);
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg, tr);
|
||||
}
|
||||
|
||||
public static int w(String tag, Throwable tr) {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, tr);
|
||||
}
|
||||
|
||||
// What a Terrible Failure: Report an exception that should never happen
|
||||
public static int wtf(String tag, String msg) {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, tr);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, String msg, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg, tr);
|
||||
}
|
||||
|
||||
@@ -106,7 +162,11 @@ public class QLog {
|
||||
|
||||
// Additional utility methods
|
||||
public static int println(int priority, String tag, String msg) {
|
||||
sendToQt(priority, tag, msg);
|
||||
try {
|
||||
sendToQt(priority, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.println(priority, tag, msg);
|
||||
}
|
||||
|
||||
|
||||
145
src/android/src/VirtualGearingBridge.java
Normal file
145
src/android/src/VirtualGearingBridge.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.WindowManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class VirtualGearingBridge {
|
||||
private static final String TAG = "VirtualGearingBridge";
|
||||
|
||||
public static boolean isAccessibilityServiceEnabled(Context context) {
|
||||
String settingValue = Settings.Secure.getString(
|
||||
context.getContentResolver(),
|
||||
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
|
||||
|
||||
QLog.d(TAG, "Enabled accessibility services: " + settingValue);
|
||||
|
||||
if (settingValue != null) {
|
||||
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(':');
|
||||
splitter.setString(settingValue);
|
||||
while (splitter.hasNext()) {
|
||||
String service = splitter.next();
|
||||
QLog.d(TAG, "Checking service: " + service);
|
||||
if (service.contains("org.cagnulen.qdomyoszwift/.VirtualGearingService") ||
|
||||
service.contains("VirtualGearingService")) {
|
||||
QLog.d(TAG, "VirtualGearingService is enabled");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
QLog.d(TAG, "VirtualGearingService is not enabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void openAccessibilitySettings(Context context) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
QLog.d(TAG, "Opened accessibility settings");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to open accessibility settings", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void simulateShiftUp() {
|
||||
QLog.d(TAG, "Simulating shift up with app-specific coordinates");
|
||||
VirtualGearingService.shiftUpSmart();
|
||||
}
|
||||
|
||||
public static void simulateShiftDown() {
|
||||
QLog.d(TAG, "Simulating shift down with app-specific coordinates");
|
||||
VirtualGearingService.shiftDownSmart();
|
||||
}
|
||||
|
||||
public static String getCurrentAppPackageName(Context context) {
|
||||
try {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (activityManager != null) {
|
||||
ActivityManager.RunningAppProcessInfo myProcess = new ActivityManager.RunningAppProcessInfo();
|
||||
ActivityManager.getMyMemoryState(myProcess);
|
||||
|
||||
// For Android 5.0+ we should use UsageStatsManager, but for simplicity
|
||||
// we use a more direct approach via current foreground process
|
||||
// In a complete implementation we should use UsageStatsManager
|
||||
|
||||
// For now return null and let the service detect the app
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error getting current app package name", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int[] getScreenSize(Context context) {
|
||||
try {
|
||||
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return new int[]{displayMetrics.widthPixels, displayMetrics.heightPixels};
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error getting screen size", e);
|
||||
return new int[]{1080, 1920}; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
public static void simulateTouch(int x, int y) {
|
||||
QLog.d(TAG, "Simulating touch at (" + x + ", " + y + ")");
|
||||
VirtualGearingService.simulateKeypress(x, y);
|
||||
}
|
||||
|
||||
public static boolean isServiceRunning() {
|
||||
boolean running = VirtualGearingService.isServiceEnabled();
|
||||
QLog.d(TAG, "Service running: " + running);
|
||||
return running;
|
||||
}
|
||||
|
||||
// Native methods to get settings from C++ side
|
||||
public static native double getVirtualGearingShiftUpX();
|
||||
public static native double getVirtualGearingShiftUpY();
|
||||
public static native double getVirtualGearingShiftDownX();
|
||||
public static native double getVirtualGearingShiftDownY();
|
||||
public static native int getVirtualGearingApp();
|
||||
|
||||
// Methods to get coordinates that will be/were sent
|
||||
public static String getShiftUpCoordinates() {
|
||||
try {
|
||||
AppConfiguration.AppConfig config = AppConfiguration.getCurrentConfig();
|
||||
// Use VirtualGearingService to get screen size (it has access to service context)
|
||||
int[] screenSize = VirtualGearingService.getScreenSize();
|
||||
int x = config.shiftUp.getX(screenSize[0]);
|
||||
int y = config.shiftUp.getY(screenSize[1]);
|
||||
return x + "," + y;
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error getting shift up coordinates", e);
|
||||
return "0,0";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getShiftDownCoordinates() {
|
||||
try {
|
||||
AppConfiguration.AppConfig config = AppConfiguration.getCurrentConfig();
|
||||
// Use VirtualGearingService to get screen size (it has access to service context)
|
||||
int[] screenSize = VirtualGearingService.getScreenSize();
|
||||
int x = config.shiftDown.getX(screenSize[0]);
|
||||
int y = config.shiftDown.getY(screenSize[1]);
|
||||
return x + "," + y;
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error getting shift down coordinates", e);
|
||||
return "0,0";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLastTouchCoordinates() {
|
||||
// For now, return the last coordinates that would be sent for shift up
|
||||
// This could be enhanced to track actual last touch
|
||||
return getShiftUpCoordinates();
|
||||
}
|
||||
}
|
||||
152
src/android/src/VirtualGearingService.java
Normal file
152
src/android/src/VirtualGearingService.java
Normal file
@@ -0,0 +1,152 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.accessibilityservice.AccessibilityService;
|
||||
import android.accessibilityservice.GestureDescription;
|
||||
import android.graphics.Path;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class VirtualGearingService extends AccessibilityService {
|
||||
private static final String TAG = "VirtualGearingService";
|
||||
private static VirtualGearingService instance;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
QLog.d(TAG, "VirtualGearingService created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
instance = null;
|
||||
QLog.d(TAG, "VirtualGearingService destroyed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||
// Capture foreground app package name for smart coordinates
|
||||
if (event != null && event.getPackageName() != null) {
|
||||
String packageName = event.getPackageName().toString();
|
||||
if (!packageName.equals(currentPackageName)) {
|
||||
currentPackageName = packageName;
|
||||
QLog.d(TAG, "App changed to: " + packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt() {
|
||||
QLog.d(TAG, "VirtualGearingService interrupted");
|
||||
}
|
||||
|
||||
public static boolean isServiceEnabled() {
|
||||
return instance != null;
|
||||
}
|
||||
|
||||
public static void simulateKeypress(int x, int y) {
|
||||
if (instance == null) {
|
||||
QLog.w(TAG, "Service not enabled, cannot simulate keypress");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
|
||||
Path path = new Path();
|
||||
path.moveTo(x, y);
|
||||
path.lineTo(x + 1, y);
|
||||
|
||||
GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(
|
||||
path, 0, ViewConfiguration.getTapTimeout(), false);
|
||||
gestureBuilder.addStroke(stroke);
|
||||
|
||||
instance.dispatchGesture(gestureBuilder.build(), null, null);
|
||||
QLog.d(TAG, "Simulated keypress at (" + x + ", " + y + ")");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error simulating keypress", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility
|
||||
public static void shiftUp() {
|
||||
QLog.d(TAG, "Using legacy shiftUp - consider using shiftUpSmart()");
|
||||
simulateKeypress(100, 200);
|
||||
}
|
||||
|
||||
public static void shiftDown() {
|
||||
QLog.d(TAG, "Using legacy shiftDown - consider using shiftDownSmart()");
|
||||
simulateKeypress(100, 300);
|
||||
}
|
||||
|
||||
// New smart methods with app-specific coordinates
|
||||
public static void shiftUpSmart() {
|
||||
if (instance == null) {
|
||||
QLog.w(TAG, "Service not enabled, cannot simulate smart shift up");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to detect app from package name of last AccessibilityEvent
|
||||
String currentPackage = getCurrentPackageName();
|
||||
AppConfiguration.AppConfig config = AppConfiguration.getConfigForPackage(currentPackage);
|
||||
|
||||
// Calculate coordinates based on screen dimensions
|
||||
int[] screenSize = getScreenSize();
|
||||
int x = config.shiftUp.getX(screenSize[0]);
|
||||
int y = config.shiftUp.getY(screenSize[1]);
|
||||
|
||||
QLog.d(TAG, "Smart shift up for " + config.appName + " at (" + x + ", " + y + ")");
|
||||
simulateKeypress(x, y);
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error in shiftUpSmart, falling back to legacy", e);
|
||||
shiftUp();
|
||||
}
|
||||
}
|
||||
|
||||
public static void shiftDownSmart() {
|
||||
if (instance == null) {
|
||||
QLog.w(TAG, "Service not enabled, cannot simulate smart shift down");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String currentPackage = getCurrentPackageName();
|
||||
AppConfiguration.AppConfig config = AppConfiguration.getConfigForPackage(currentPackage);
|
||||
|
||||
int[] screenSize = getScreenSize();
|
||||
int x = config.shiftDown.getX(screenSize[0]);
|
||||
int y = config.shiftDown.getY(screenSize[1]);
|
||||
|
||||
QLog.d(TAG, "Smart shift down for " + config.appName + " at (" + x + ", " + y + ")");
|
||||
simulateKeypress(x, y);
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error in shiftDownSmart, falling back to legacy", e);
|
||||
shiftDown();
|
||||
}
|
||||
}
|
||||
|
||||
private static String currentPackageName = null;
|
||||
|
||||
private static String getCurrentPackageName() {
|
||||
return currentPackageName != null ? currentPackageName : "unknown";
|
||||
}
|
||||
|
||||
public static int[] getScreenSize() {
|
||||
if (instance != null) {
|
||||
try {
|
||||
android.content.res.Resources resources = instance.getResources();
|
||||
android.util.DisplayMetrics displayMetrics = resources.getDisplayMetrics();
|
||||
int width = displayMetrics.widthPixels;
|
||||
int height = displayMetrics.heightPixels;
|
||||
QLog.d(TAG, "Screen size: " + width + "x" + height + " (density=" + displayMetrics.density + ")");
|
||||
return new int[]{width, height};
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error getting screen size from service", e);
|
||||
}
|
||||
}
|
||||
QLog.w(TAG, "Using fallback screen size");
|
||||
return new int[]{1080, 1920}; // Default fallback
|
||||
}
|
||||
}
|
||||
64
src/androidstatusbar.cpp
Normal file
64
src/androidstatusbar.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "androidstatusbar.h"
|
||||
#include <QQmlEngine>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QtAndroid>
|
||||
#include <QAndroidJniEnvironment>
|
||||
#endif
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::m_instance = nullptr;
|
||||
|
||||
AndroidStatusBar::AndroidStatusBar(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_instance = this;
|
||||
}
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::instance()
|
||||
{
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void AndroidStatusBar::registerQmlType()
|
||||
{
|
||||
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
|
||||
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
|
||||
Q_UNUSED(engine)
|
||||
Q_UNUSED(scriptEngine)
|
||||
return new AndroidStatusBar();
|
||||
});
|
||||
}
|
||||
|
||||
int AndroidStatusBar::apiLevel() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
return QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/CustomQtActivity", "getApiLevel", "()I");
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right)
|
||||
{
|
||||
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right) {
|
||||
m_top = top;
|
||||
m_bottom = bottom;
|
||||
m_left = left;
|
||||
m_right = right;
|
||||
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left << "Right:" << m_right;
|
||||
emit insetsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// JNI method with standard naming convention
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top, jint bottom, jint left, jint right)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
if (AndroidStatusBar::instance()) {
|
||||
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
43
src/androidstatusbar.h
Normal file
43
src/androidstatusbar.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef ANDROIDSTATUSBAR_H
|
||||
#define ANDROIDSTATUSBAR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class AndroidStatusBar : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int height READ height NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int leftInset READ leftInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int rightInset READ rightInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit AndroidStatusBar(QObject *parent = nullptr);
|
||||
|
||||
static void registerQmlType();
|
||||
static AndroidStatusBar* instance();
|
||||
|
||||
int height() const { return m_top; }
|
||||
int navigationBarHeight() const { return m_bottom; }
|
||||
int leftInset() const { return m_left; }
|
||||
int rightInset() const { return m_right; }
|
||||
int apiLevel() const;
|
||||
|
||||
public slots:
|
||||
void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
signals:
|
||||
void insetsChanged();
|
||||
|
||||
private:
|
||||
int m_top = 0;
|
||||
int m_bottom = 0;
|
||||
int m_left = 0;
|
||||
int m_right = 0;
|
||||
|
||||
static AndroidStatusBar* m_instance;
|
||||
};
|
||||
|
||||
#endif // ANDROIDSTATUSBAR_H
|
||||
@@ -14,7 +14,7 @@ class CharacteristicWriteProcessor : public QObject {
|
||||
public:
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
double bikeResistanceGain = 1.0;
|
||||
bluetoothdevice *Bike;
|
||||
bluetoothdevice *Bike = nullptr;
|
||||
|
||||
explicit CharacteristicWriteProcessor(double bikeResistanceGain, int8_t bikeResistanceOffset,
|
||||
bluetoothdevice *bike, QObject *parent = nullptr);
|
||||
|
||||
@@ -31,7 +31,7 @@ CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003:
|
||||
}
|
||||
|
||||
double CharacteristicWriteProcessor0003::currentGear() {
|
||||
if(zwiftGearReceived)
|
||||
if(zwiftGearReceived || !((bike*)Bike))
|
||||
return currentZwiftGear;
|
||||
else
|
||||
return ((bike*)Bike)->gears();
|
||||
|
||||
@@ -14,7 +14,7 @@ CharacteristicWriteProcessor2AD9::CharacteristicWriteProcessor2AD9(double bikeRe
|
||||
int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
|
||||
if (data.size()) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
if (dt == bluetoothdevice::BIKE || dt == bluetoothdevice::ROWING) {
|
||||
QSettings settings;
|
||||
bool force_resistance =
|
||||
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
|
||||
|
||||
@@ -158,28 +158,7 @@ uint16_t android_antbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t android_antbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ void antbike::update() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -168,28 +168,7 @@ uint16_t antbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t antbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,12 +59,11 @@ void apexbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin
|
||||
|
||||
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
} else {
|
||||
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
|
||||
}
|
||||
|
||||
|
||||
if (!disable_log) {
|
||||
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info;
|
||||
@@ -147,42 +146,37 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
lastPacket = newValue;
|
||||
|
||||
if (newValue.length() == 10 && newValue.at(2) == 0x31) {
|
||||
Resistance = newValue.at(5);
|
||||
// Invert resistance: bike resistance 1-32 maps to app display 32-1
|
||||
uint8_t rawResistance = newValue.at(5);
|
||||
Resistance = 33 - rawResistance; // Invert: 1->32, 32->1
|
||||
emit resistanceRead(Resistance.value());
|
||||
m_pelotonResistance = Resistance.value();
|
||||
|
||||
qDebug() << QStringLiteral("Current resistance: ") + QString::number(Resistance.value());
|
||||
// Parse cadence from 5th byte (index 4) and multiply by 2
|
||||
uint8_t rawCadence = newValue.at(4);
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = rawCadence * 2;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value()) + QStringLiteral(", Raw cadence: ") + QString::number(rawCadence) + QStringLiteral(", Final cadence: ") + QString::number(Cadence.value());
|
||||
}
|
||||
|
||||
if (newValue.length() != 10 || newValue.at(2) != 0x30) {
|
||||
if (newValue.length() != 10 || newValue.at(2) != 0x31) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t distanceData = (newValue.at(3) << 8) | ((uint8_t)newValue.at(4));
|
||||
uint16_t distanceData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
|
||||
double distance = ((double)distanceData);
|
||||
|
||||
if(distance != lastDistance) {
|
||||
if(lastDistance != 0) {
|
||||
double deltaDistance = distance - lastDistance;
|
||||
double deltaTime = fabs(now.msecsTo(lastTS));
|
||||
double timeHours = deltaTime / (1000.0 * 60.0 * 60.0);
|
||||
double k = 0.005333;
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = (deltaDistance *k) / timeHours; // Speed in distance units per hour
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = Speed.value() / 0.37497622;
|
||||
}
|
||||
}
|
||||
lastDistance = distance;
|
||||
lastTS = now;
|
||||
qDebug() << "lastDistance" << lastDistance << "lastTS" << lastTS;
|
||||
// Calculate speed using the same method as echelon bike
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = 0.37497622 * ((double)Cadence.value());
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
|
||||
if (watts())
|
||||
@@ -220,7 +214,7 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -417,7 +411,98 @@ bool apexbike::connected() {
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
uint16_t apexbike::watts() { return wattFromHR(true); }
|
||||
uint16_t apexbike::watts() {
|
||||
double resistance = Resistance.value();
|
||||
double cadence = Cadence.value();
|
||||
|
||||
if (cadence <= 0 || resistance <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Power table based on user-provided data
|
||||
// Format: resistance level (1-19), RPM (10-150 in steps of 10), power (watts)
|
||||
static const int powerTable[19][15] = {
|
||||
// Resistance 1: RPM 10,20,30,40,50,60,70,80,90,100,110,120,130,140,150
|
||||
{12, 24, 36, 48, 61, 73, 85, 97, 109, 121, 133, 145, 157, 170, 182},
|
||||
// Resistance 2
|
||||
{13, 27, 40, 53, 67, 80, 93, 107, 120, 133, 147, 160, 173, 187, 200},
|
||||
// Resistance 3
|
||||
{15, 29, 44, 58, 73, 87, 102, 117, 131, 146, 160, 175, 189, 204, 219},
|
||||
// Resistance 4
|
||||
{16, 32, 48, 64, 80, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239},
|
||||
// Resistance 5
|
||||
{17, 34, 51, 68, 85, 102, 118, 135, 152, 169, 186, 203, 220, 237, 254},
|
||||
// Resistance 6
|
||||
{18, 37, 55, 74, 92, 110, 129, 147, 165, 184, 202, 221, 239, 257, 276},
|
||||
// Resistance 7
|
||||
{19, 39, 58, 77, 97, 116, 136, 155, 174, 194, 213, 232, 252, 271, 291},
|
||||
// Resistance 8
|
||||
{21, 42, 62, 83, 104, 125, 146, 166, 187, 208, 229, 250, 271, 291, 312},
|
||||
// Resistance 9
|
||||
{22, 44, 66, 88, 110, 132, 154, 176, 198, 220, 242, 264, 286, 308, 330},
|
||||
// Resistance 10
|
||||
{23, 46, 69, 92, 116, 139, 162, 185, 208, 231, 254, 277, 300, 324, 347},
|
||||
// Resistance 11
|
||||
{24, 49, 73, 98, 122, 146, 171, 195, 219, 244, 268, 293, 317, 341, 366},
|
||||
// Resistance 12
|
||||
{26, 51, 77, 102, 128, 153, 179, 204, 230, 255, 281, 307, 332, 358, 383},
|
||||
// Resistance 13
|
||||
{27, 54, 80, 107, 134, 161, 188, 214, 241, 268, 295, 322, 348, 375, 402},
|
||||
// Resistance 14
|
||||
{28, 56, 83, 111, 139, 167, 195, 222, 250, 278, 306, 334, 362, 389, 417},
|
||||
// Resistance 15
|
||||
{29, 58, 87, 117, 146, 175, 204, 233, 262, 292, 321, 350, 379, 408, 437},
|
||||
// Resistance 16
|
||||
{30, 61, 91, 121, 152, 182, 212, 242, 273, 303, 333, 364, 394, 424, 455},
|
||||
// Resistance 17
|
||||
{32, 63, 95, 126, 158, 189, 221, 253, 284, 316, 347, 379, 410, 442, 473},
|
||||
// Resistance 18
|
||||
{33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495},
|
||||
// Resistance 19
|
||||
{34, 68, 102, 136, 171, 205, 239, 273, 307, 341, 375, 409, 443, 478, 512}
|
||||
};
|
||||
|
||||
// Clamp resistance to valid range (1-19)
|
||||
int res = qMax(1, qMin(19, (int)qRound(resistance)));
|
||||
|
||||
// Convert to array index (0-18)
|
||||
int resIndex = res - 1;
|
||||
|
||||
// RPM ranges from 10 to 150 in steps of 10
|
||||
// Find the two closest RPM values for interpolation
|
||||
double rpm = qMax(1.0, cadence); // Ensure RPM is at least 1
|
||||
|
||||
if (rpm <= 10.0) {
|
||||
// Below minimum RPM, extrapolate from first data point
|
||||
double factor = rpm / 10.0;
|
||||
return (uint16_t)qMax(0.0, powerTable[resIndex][0] * factor);
|
||||
}
|
||||
|
||||
if (rpm >= 150.0) {
|
||||
// Above maximum RPM, extrapolate from last data point
|
||||
double factor = rpm / 150.0;
|
||||
return (uint16_t)qMax(0.0, powerTable[resIndex][14] * factor);
|
||||
}
|
||||
|
||||
// Find the two RPM values to interpolate between
|
||||
// RPM values are: 10, 20, 30, ..., 150 (indices 0-14)
|
||||
int lowerRpmIndex = ((int)rpm - 1) / 10; // Convert RPM to array index
|
||||
if (lowerRpmIndex > 13) lowerRpmIndex = 13; // Ensure we don't go out of bounds
|
||||
|
||||
int upperRpmIndex = lowerRpmIndex + 1;
|
||||
|
||||
double lowerRpm = (lowerRpmIndex + 1) * 10.0; // Convert index back to RPM
|
||||
double upperRpm = (upperRpmIndex + 1) * 10.0;
|
||||
|
||||
int lowerPower = powerTable[resIndex][lowerRpmIndex];
|
||||
int upperPower = powerTable[resIndex][upperRpmIndex];
|
||||
|
||||
// Linear interpolation between the two power values
|
||||
double ratio = (rpm - lowerRpm) / (upperRpm - lowerRpm);
|
||||
double interpolatedPower = lowerPower + ratio * (upperPower - lowerPower);
|
||||
|
||||
return (uint16_t)qMax(0.0, interpolatedPower);
|
||||
}
|
||||
|
||||
void apexbike::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "devices/bike.h"
|
||||
#include "qdebugfixup.h"
|
||||
#include "homeform.h"
|
||||
#include "virtualgearingdevice.h"
|
||||
#include <QSettings>
|
||||
|
||||
bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); }
|
||||
@@ -70,6 +71,11 @@ void bike::changePower(int32_t power) {
|
||||
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
|
||||
double erg_filter_lower =
|
||||
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
|
||||
|
||||
// Apply bike power offset
|
||||
int bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toInt();
|
||||
power += bike_power_offset;
|
||||
qDebug() << QStringLiteral("changePower: original power with offset applied: ") + QString::number(power) + QStringLiteral(" (offset: ") + QString::number(bike_power_offset) + QStringLiteral(")");
|
||||
|
||||
requestPower = power; // used by some bikes that have ERG mode builtin
|
||||
|
||||
@@ -115,27 +121,60 @@ void bike::setGears(double gears) {
|
||||
gears -= gears_offset;
|
||||
qDebug() << "setGears" << gears;
|
||||
|
||||
// Check for boundaries and emit failure signals
|
||||
// Gear boundary handling with smart clamping logic:
|
||||
// - If we're trying to set a gear outside valid range AND we're already at a valid gear,
|
||||
// reject the change (normal case: user at gear 1 tries to go to 0.5, should fail)
|
||||
// - If we're trying to set a gear outside valid range BUT we're currently below minimum,
|
||||
// clamp to valid range (startup case: system starts at 0, first gearUp with 0.5 gain
|
||||
// goes to 0.5, should be clamped to 1 to allow the system to reach valid state)
|
||||
// This prevents the system from getting stuck below minGears due to fractional gains
|
||||
// while preserving normal boundary rejection behavior for users at valid gear positions
|
||||
if(gears_zwift_ratio && (gears > 24 || gears < 1)) {
|
||||
qDebug() << "new gear value ignored because of gears_zwift_ratio setting!";
|
||||
if(gears > 24) {
|
||||
emit gearFailedUp();
|
||||
if(m_gears >= 24) {
|
||||
qDebug() << "new gear value ignored - already at zwift ratio maximum: 24";
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to zwift ratio maximum: 24";
|
||||
gears = 24;
|
||||
emit gearFailedUp();
|
||||
}
|
||||
} else {
|
||||
emit gearFailedDown();
|
||||
if(m_gears >= 1) {
|
||||
qDebug() << "new gear value ignored - already at zwift ratio minimum: 1";
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to zwift ratio minimum: 1";
|
||||
gears = 1;
|
||||
emit gearFailedDown();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(gears > maxGears()) {
|
||||
qDebug() << "new gear value ignored because of maxGears" << maxGears();
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
if(m_gears >= maxGears()) {
|
||||
qDebug() << "new gear value ignored - already at maxGears" << maxGears();
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to maxGears" << maxGears();
|
||||
gears = maxGears();
|
||||
emit gearFailedUp();
|
||||
}
|
||||
}
|
||||
|
||||
if(gears < minGears()) {
|
||||
qDebug() << "new gear value ignored because of minGears" << minGears();
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
if(m_gears >= minGears()) {
|
||||
qDebug() << "new gear value ignored - already at or above minGears" << minGears();
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to minGears" << minGears();
|
||||
gears = minGears();
|
||||
emit gearFailedDown();
|
||||
}
|
||||
}
|
||||
|
||||
if(m_gears > gears) {
|
||||
@@ -428,7 +467,81 @@ double bike::gearsZwiftRatio() {
|
||||
case 23:
|
||||
return 5.14;
|
||||
case 24:
|
||||
return 5.49;
|
||||
return 5.49;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void bike::gearUp() {
|
||||
QSettings settings;
|
||||
|
||||
// Check if virtual gearing device is enabled
|
||||
if (settings.value(QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device).toBool()) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
VirtualGearingDevice* vgd = VirtualGearingDevice::instance();
|
||||
if (vgd) {
|
||||
// Check if accessibility service is enabled
|
||||
if (!vgd->isAccessibilityServiceEnabled()) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
qDebug() << "bike::gearUp() - VirtualGearingService not enabled in accessibility settings";
|
||||
qDebug() << "Please enable the Virtual Gearing Service in Android Accessibility Settings";
|
||||
warned = true;
|
||||
}
|
||||
} else if (vgd->isServiceRunning()) {
|
||||
qDebug() << "bike::gearUp() - Using virtual gearing device";
|
||||
QString coordinates = vgd->getShiftUpCoordinates();
|
||||
vgd->simulateShiftUp();
|
||||
|
||||
// Show toast with coordinates
|
||||
homeform::singleton()->setToastRequested("Virtual Gear Up → " + coordinates);
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "bike::gearUp() - Virtual gearing service not running, falling back to normal gearing";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Normal gearing logic
|
||||
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 bike::gearDown() {
|
||||
QSettings settings;
|
||||
|
||||
// Check if virtual gearing device is enabled
|
||||
if (settings.value(QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device).toBool()) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
VirtualGearingDevice* vgd = VirtualGearingDevice::instance();
|
||||
if (vgd) {
|
||||
// Check if accessibility service is enabled
|
||||
if (!vgd->isAccessibilityServiceEnabled()) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
qDebug() << "bike::gearDown() - VirtualGearingService not enabled in accessibility settings";
|
||||
qDebug() << "Please enable the Virtual Gearing Service in Android Accessibility Settings";
|
||||
warned = true;
|
||||
}
|
||||
} else if (vgd->isServiceRunning()) {
|
||||
qDebug() << "bike::gearDown() - Using virtual gearing device";
|
||||
QString coordinates = vgd->getShiftDownCoordinates();
|
||||
vgd->simulateShiftDown();
|
||||
|
||||
// Show toast with coordinates
|
||||
homeform::singleton()->setToastRequested("Virtual Gear Down → " + coordinates);
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "bike::gearDown() - Virtual gearing service not running, falling back to normal gearing";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Normal gearing logic
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class bike : public bluetoothdevice {
|
||||
metric currentSteeringAngle() { return m_steeringAngle; }
|
||||
virtual bool inclinationAvailableByHardware();
|
||||
bool ergModeSupportedAvailableByHardware() { return ergModeSupported; }
|
||||
virtual bool ergModeSupportedAvailableBySoftware() { return ergModeSupported; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void changeResistance(resistance_t res) override;
|
||||
@@ -63,18 +64,8 @@ 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;
|
||||
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()));
|
||||
}
|
||||
void gearUp();
|
||||
void gearDown();
|
||||
|
||||
Q_SIGNALS:
|
||||
void bikeStarted();
|
||||
|
||||
@@ -36,6 +36,10 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc
|
||||
|
||||
QString nordictrack_2950_ip =
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
bool fake_bike =
|
||||
settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool();
|
||||
bool fake_treadmill =
|
||||
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
|
||||
|
||||
if (settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool() && !pelotonBike) {
|
||||
pelotonBike = new pelotonbike(noWriteResistance, noHeartService);
|
||||
@@ -47,7 +51,29 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(pelotonBike);
|
||||
}
|
||||
}/* else if (fake_bike) {
|
||||
fakeBike = new fakebike(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(QBluetoothDeviceInfo());
|
||||
connect(fakeBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
connect(fakeBike, &fakebike::debug, this, &bluetooth::debug);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(fakeBike);
|
||||
return;
|
||||
} else if (fake_treadmill) {
|
||||
fakeTreadmill = new faketreadmill(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(QBluetoothDeviceInfo());
|
||||
connect(fakeTreadmill, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
connect(fakeTreadmill, &faketreadmill::debug, this, &bluetooth::debug);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(fakeBike);
|
||||
return;
|
||||
}*/
|
||||
|
||||
#ifdef TEST
|
||||
schwinnIC4Bike = (schwinnic4bike *)new bike();
|
||||
@@ -115,6 +141,7 @@ void bluetooth::finished() {
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
|
||||
QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString();
|
||||
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
bool fake_bike =
|
||||
settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool();
|
||||
bool fakedevice_elliptical =
|
||||
@@ -123,7 +150,7 @@ void bluetooth::finished() {
|
||||
bool fakedevice_treadmill =
|
||||
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
|
||||
// wifi devices on windows
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || antbike || android_antbike) {
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || !proform_rower_ip.isEmpty() || antbike || android_antbike) {
|
||||
// faking a bluetooth device
|
||||
qDebug() << "faking a bluetooth device for nordictrack_2950_ip";
|
||||
deviceDiscovered(QBluetoothDeviceInfo());
|
||||
@@ -423,7 +450,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool() ||
|
||||
settings.value(QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike).toBool() ||
|
||||
settings.value(QZSettings::toorx_bike_srx_500, QZSettings::default_toorx_bike_srx_500).toBool() ||
|
||||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool()) &&
|
||||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool() ||
|
||||
settings.value(QZSettings::taurua_ic90, QZSettings::default_taurua_ic90).toBool()) &&
|
||||
!toorx_ftms;
|
||||
bool snode_bike = settings.value(QZSettings::snode_bike, QZSettings::default_snode_bike).toBool();
|
||||
bool fitplus_bike = settings.value(QZSettings::fitplus_bike, QZSettings::default_fitplus_bike).toBool() ||
|
||||
@@ -470,6 +498,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
|
||||
QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString();
|
||||
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
QString computrainerSerialPort =
|
||||
settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
|
||||
QString csaferowerSerialPort = settings.value(QZSettings::csafe_rower, QZSettings::default_csafe_rower).toString();
|
||||
@@ -493,6 +522,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
QString ftms_rower = settings.value(QZSettings::ftms_rower, QZSettings::default_ftms_rower).toString();
|
||||
QString ftms_bike = settings.value(QZSettings::ftms_bike, QZSettings::default_ftms_bike).toString();
|
||||
QString ftms_treadmill = settings.value(QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill).toString();
|
||||
QString ftms_elliptical = settings.value(QZSettings::ftms_elliptical, QZSettings::default_ftms_elliptical).toString();
|
||||
bool saris_trainer = settings.value(QZSettings::saris_trainer, QZSettings::default_saris_trainer).toBool();
|
||||
bool iconsole_elliptical = settings.value(QZSettings::iconsole_elliptical, QZSettings::default_iconsole_elliptical).toBool();
|
||||
bool iconsole_rower = settings.value(QZSettings::iconsole_rower, QZSettings::default_iconsole_rower).toBool();
|
||||
@@ -890,6 +920,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbElliptical);
|
||||
} else if (!proform_rower_ip.isEmpty() && !nordictrackifitadbRower) {
|
||||
this->stopDiscovery();
|
||||
nordictrackifitadbRower = new nordictrackifitadbrower(noWriteResistance, noHeartService,
|
||||
bikeResistanceOffset, bikeResistanceGain);
|
||||
emit deviceConnected(b);
|
||||
connect(nordictrackifitadbRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(nordictrackifitadbRower, &nordictrackifitadbrower::debug, this, &bluetooth::debug);
|
||||
// nordictrackifitadbRower->deviceDiscovered(b);
|
||||
// connect(this, SIGNAL(searchingStop()), nordictrackifitadbRower, SLOT(searchingStop())); //NOTE: Commented due to #358
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbRower);
|
||||
} else if (((csc_as_bike && b.name().startsWith(cscName)) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) &&
|
||||
!cscBike && filter) {
|
||||
@@ -991,7 +1035,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(trxappgateusbRower);
|
||||
} else if (((b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && !toorx_bike) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_elliptical)) &&
|
||||
!trxappgateusbElliptical && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
!trxappgateusbElliptical && ftms_bike.contains(QZSettings::default_ftms_bike) && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbElliptical = new trxappgateusbelliptical(noWriteResistance, noHeartService,
|
||||
@@ -1027,12 +1071,15 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FEIER-EM-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MX-AS ")) ||
|
||||
(b.name().startsWith(QStringLiteral("Domyos-EL")) && settings.value(QZSettings::domyos_elliptical_fmts, QZSettings::default_domyos_elliptical_fmts).toBool()) ||
|
||||
(b.name().toUpper().startsWith("SF-") && b.name().midRef(3).toInt() > 0) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MYELLIPTICAL ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("CARDIOPOWER EEGO")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("E35")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical)) && !ypooElliptical && filter) {
|
||||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical) ||
|
||||
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
ypooElliptical =
|
||||
@@ -1329,6 +1376,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("TRX7.5")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S77")) && sole_inclination) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F85")) && sole_inclination)) &&
|
||||
ftms_treadmill.contains(QZSettings::default_ftms_treadmill) &&
|
||||
!soleF80 && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1442,10 +1490,14 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
|
||||
(b.name().toUpper().startsWith("SCHWINN 810")) ||
|
||||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
|
||||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FOCUS M3")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KICKR RUN")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
|
||||
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TT8")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ST90")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
@@ -1454,7 +1506,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI WMTP")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("LB600")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T60-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
|
||||
@@ -1701,6 +1752,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("SL010-")) ||
|
||||
(b.name().toUpper().startsWith("EXPERT-SX9")) ||
|
||||
(b.name().toUpper().startsWith("MRK-S26S-")) ||
|
||||
(b.name().toUpper().startsWith("MRK-S26C-")) ||
|
||||
(b.name().toUpper().startsWith("ROBX")) ||
|
||||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
|
||||
(b.name().toUpper().startsWith("XCX-")) ||
|
||||
@@ -1732,6 +1784,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith("KICKR ROLLR") ||
|
||||
b.name().toUpper().startsWith("KICKR CORE") ||
|
||||
(b.name().toUpper().startsWith("KICKR MOVE ")) ||
|
||||
(b.name().toUpper().startsWith("HOI FRAME ")) ||
|
||||
(b.name().toUpper().startsWith("HAMMER ") && saris_trainer) ||
|
||||
(b.name().toUpper().startsWith("WAHOO KICKR"))) &&
|
||||
!wahooKickrSnapBike && !ftmsBike && filter) {
|
||||
@@ -1746,7 +1799,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(wahooKickrSnapBike, &wahookickrsnapbike::debug, this, &bluetooth::debug);
|
||||
wahooKickrSnapBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(wahooKickrSnapBike);
|
||||
} else if (b.name().toUpper().startsWith("BIKE ") && b.name().midRef(5).toInt() > 0 &&
|
||||
} else if (((b.name().toUpper().startsWith("BIKE ") && b.name().midRef(5).toInt() > 0) ||
|
||||
b.name().toUpper().startsWith("MYCYCLING")) &&
|
||||
!technogymBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1853,6 +1907,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("NORDLYS")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) ||
|
||||
@@ -2398,7 +2453,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) &&
|
||||
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower &&
|
||||
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) &&
|
||||
ftms_bike.contains(QZSettings::default_ftms_bike) &&
|
||||
filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2427,7 +2483,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
(toorx_bike))) &&
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower) {
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical)) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbBike =
|
||||
@@ -2563,6 +2619,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().startsWith(QStringLiteral("SW")) && b.name().length() == 14 &&
|
||||
!b.name().contains('(') && !b.name().contains(')') && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WINFITA"))) || // also FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("SW-BLE"))) || // FTMS
|
||||
(b.name().startsWith(QStringLiteral("BF70")))) &&
|
||||
!fitshowTreadmill && !iconsole_elliptical && !horizonTreadmill && filter) {
|
||||
@@ -3033,18 +3090,23 @@ void bluetooth::connectedAndDiscovered() {
|
||||
}
|
||||
}
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() ||
|
||||
bool android_antbike =
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
|
||||
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() || android_antbike ||
|
||||
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
|
||||
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
|
||||
"activity", "()Landroid/app/Activity;");
|
||||
KeepAwakeHelper::antObject(true)->callMethod<void>(
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZ)V", activity.object<jobject>(),
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZZII)V", activity.object<jobject>(),
|
||||
settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(),
|
||||
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(),
|
||||
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(),
|
||||
device()->deviceType() == bluetoothdevice::TREADMILL ||
|
||||
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool());
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool(),
|
||||
settings.value(QZSettings::technogym_group_cycle, QZSettings::default_technogym_group_cycle).toBool(),
|
||||
settings.value(QZSettings::ant_bike_device_number, QZSettings::default_ant_bike_device_number).toInt(),
|
||||
settings.value(QZSettings::ant_heart_device_number, QZSettings::default_ant_heart_device_number).toInt());
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) {
|
||||
@@ -3332,6 +3394,11 @@ void bluetooth::restart() {
|
||||
delete nordictrackifitadbElliptical;
|
||||
nordictrackifitadbElliptical = nullptr;
|
||||
}
|
||||
if (nordictrackifitadbRower) {
|
||||
|
||||
delete nordictrackifitadbRower;
|
||||
nordictrackifitadbRower = nullptr;
|
||||
}
|
||||
if (powerBike) {
|
||||
|
||||
delete powerBike;
|
||||
@@ -3806,6 +3873,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return android_antBike;
|
||||
} else if (nordictrackifitadbTreadmill) {
|
||||
return nordictrackifitadbTreadmill;
|
||||
} else if (nordictrackifitadbRower) {
|
||||
return nordictrackifitadbRower;
|
||||
} else if (nordictrackifitadbBike) {
|
||||
return nordictrackifitadbBike;
|
||||
} else if (nordictrackifitadbElliptical) {
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
#include "devices/nordictrackelliptical/nordictrackelliptical.h"
|
||||
#include "devices/nordictrackifitadbbike/nordictrackifitadbbike.h"
|
||||
#include "devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h"
|
||||
#include "devices/nordictrackifitadbrower/nordictrackifitadbrower.h"
|
||||
#include "devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h"
|
||||
#include "devices/npecablebike/npecablebike.h"
|
||||
#include "devices/octaneelliptical/octaneelliptical.h"
|
||||
@@ -222,6 +223,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr;
|
||||
nordictrackifitadbbike *nordictrackifitadbBike = nullptr;
|
||||
nordictrackifitadbelliptical *nordictrackifitadbElliptical = nullptr;
|
||||
nordictrackifitadbrower *nordictrackifitadbRower = nullptr;
|
||||
octaneelliptical *octaneElliptical = nullptr;
|
||||
octanetreadmill *octaneTreadmill = nullptr;
|
||||
pelotonbike *pelotonBike = nullptr;
|
||||
|
||||
@@ -109,7 +109,52 @@ QTime bluetoothdevice::maxPace() {
|
||||
double bluetoothdevice::odometerFromStartup() { return Distance.valueRaw(); }
|
||||
double bluetoothdevice::odometer() { return Distance.value(); }
|
||||
double bluetoothdevice::lapOdometer() { return Distance.lapValue(); }
|
||||
metric bluetoothdevice::calories() { return KCal; }
|
||||
metric bluetoothdevice::calories() {
|
||||
QSettings settings;
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
|
||||
|
||||
if (fromHR && Heart.value() > 0) {
|
||||
// Calculate calories based on heart rate
|
||||
double totalHRKCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value());
|
||||
hrKCal.setValue(totalHRKCal);
|
||||
|
||||
if (activeOnly) {
|
||||
activeKCal.setValue(metric::calculateActiveKCal(hrKCal.value(), elapsed.value()));
|
||||
return activeKCal;
|
||||
} else {
|
||||
return hrKCal;
|
||||
}
|
||||
} else {
|
||||
// Power-based calculation (current behavior)
|
||||
if (activeOnly) {
|
||||
activeKCal.setValue(metric::calculateActiveKCal(KCal.value(), elapsed.value()));
|
||||
return activeKCal;
|
||||
} else {
|
||||
return KCal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metric bluetoothdevice::totalCalories() {
|
||||
QSettings settings;
|
||||
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
|
||||
|
||||
if (fromHR && Heart.value() > 0) {
|
||||
return hrKCal; // Return HR-based total calories
|
||||
} else {
|
||||
return KCal; // Return power-based total calories
|
||||
}
|
||||
}
|
||||
|
||||
metric bluetoothdevice::activeCalories() {
|
||||
return activeKCal;
|
||||
}
|
||||
|
||||
metric bluetoothdevice::hrCalories() {
|
||||
return hrKCal;
|
||||
}
|
||||
|
||||
metric bluetoothdevice::jouls() { return m_jouls; }
|
||||
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
|
||||
bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
|
||||
@@ -254,7 +299,17 @@ void bluetoothdevice::update_hr_from_external() {
|
||||
#ifndef IO_UNDER_QT
|
||||
lockscreen h;
|
||||
long appleWatchHeartRate = h.heartRate();
|
||||
h.setKcal(KCal.value());
|
||||
QSettings settings;
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
|
||||
if (activeOnly) {
|
||||
// When active calories setting is enabled, send both total and active calories
|
||||
h.setKcal(calories().value()); // This will be active calories
|
||||
h.setTotalKcal(totalCalories().value()); // This will be total calories
|
||||
} else {
|
||||
// When disabled, send total calories as before
|
||||
h.setKcal(calories().value()); // This will be total calories
|
||||
}
|
||||
h.setDistance(Distance.value());
|
||||
h.setSpeed(Speed.value());
|
||||
h.setPower(m_watt.value());
|
||||
@@ -271,6 +326,15 @@ void bluetoothdevice::update_hr_from_external() {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
lockscreen h;
|
||||
double kcal = calories().value();
|
||||
if(kcal < 0)
|
||||
kcal = 0;
|
||||
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value());
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void bluetoothdevice::clearStats() {
|
||||
@@ -279,6 +343,8 @@ void bluetoothdevice::clearStats() {
|
||||
moving.clear(true);
|
||||
Speed.clear(false);
|
||||
KCal.clear(true);
|
||||
hrKCal.clear(true);
|
||||
activeKCal.clear(true);
|
||||
Distance.clear(true);
|
||||
Distance1s.clear(true);
|
||||
Heart.clear(false);
|
||||
@@ -304,6 +370,8 @@ void bluetoothdevice::setPaused(bool p) {
|
||||
elapsed.setPaused(p);
|
||||
Speed.setPaused(p);
|
||||
KCal.setPaused(p);
|
||||
hrKCal.setPaused(p);
|
||||
activeKCal.setPaused(p);
|
||||
Distance.setPaused(p);
|
||||
Distance1s.setPaused(p);
|
||||
Heart.setPaused(p);
|
||||
@@ -327,6 +395,8 @@ void bluetoothdevice::setLap() {
|
||||
elapsed.setLap(true);
|
||||
Speed.setLap(false);
|
||||
KCal.setLap(true);
|
||||
hrKCal.setLap(true);
|
||||
activeKCal.setLap(true);
|
||||
Distance.setLap(true);
|
||||
Distance1s.setLap(true);
|
||||
Heart.setLap(false);
|
||||
|
||||
@@ -108,11 +108,19 @@ class bluetoothdevice : public QObject {
|
||||
|
||||
/**
|
||||
* @brief calories Gets a metric object to get and set the amount of energy expended.
|
||||
* Default implementation returns the protected KCal property. Units: kcal
|
||||
* Default implementation returns the protected KCal property, potentially adjusted for active calories. Units: kcal
|
||||
* Other implementations could have different units.
|
||||
* @return
|
||||
*/
|
||||
virtual metric calories();
|
||||
virtual metric activeCalories();
|
||||
virtual metric hrCalories();
|
||||
|
||||
/**
|
||||
* @brief totalCalories Gets total calories (including BMR) regardless of active calories setting.
|
||||
* @return Total calories metric
|
||||
*/
|
||||
virtual metric totalCalories();
|
||||
|
||||
/**
|
||||
* @brief jouls Gets a metric object to get and set the number of joules expended. Units: joules
|
||||
@@ -548,6 +556,8 @@ class bluetoothdevice : public QObject {
|
||||
* @brief KCal The number of kilocalories expended in the session. Units: kcal
|
||||
*/
|
||||
metric KCal;
|
||||
metric activeKCal;
|
||||
metric hrKCal;
|
||||
|
||||
/**
|
||||
* @brief Speed The simulated speed of the device. Units: km/h
|
||||
|
||||
@@ -284,7 +284,7 @@ void csafeelliptical::update() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -738,7 +738,7 @@ void cycleopsphantombike::characteristicChanged(const QLowEnergyCharacteristic &
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)currentHeart().value());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -75,6 +75,38 @@ void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
connect(unlock_service, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
|
||||
if (unlock_service->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeUnlockCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
unlock_service->writeCharacteristic(unlock_characteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> unlock ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
|
||||
uint8_t result = 0;
|
||||
|
||||
@@ -183,21 +215,26 @@ void deerruntreadmill::update() {
|
||||
lastSpeed = 0.5;
|
||||
}
|
||||
|
||||
// should be:
|
||||
// 0x49 = inited
|
||||
// 0x8a = tape stopped after a pause
|
||||
/*if (lastState == 0x49)*/ {
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43};
|
||||
initData2[2] = pollCounter;
|
||||
if (pitpat) {
|
||||
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
|
||||
} else {
|
||||
// should be:
|
||||
// 0x49 = inited
|
||||
// 0x8a = tape stopped after a pause
|
||||
/*if (lastState == 0x49)*/ {
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43};
|
||||
initData2[2] = pollCounter;
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"),
|
||||
false, true);
|
||||
} /*else {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"),
|
||||
false, true);
|
||||
} /*else {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
}*/
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
}*/
|
||||
}
|
||||
|
||||
requestStart = -1;
|
||||
emit tapeStarted();
|
||||
@@ -219,11 +256,16 @@ void deerruntreadmill::update() {
|
||||
|
||||
requestStop = -1;
|
||||
} else {
|
||||
uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
poll[2] = pollCounter;
|
||||
if (pitpat) {
|
||||
uint8_t poll[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("pitpat poll"), false, true);
|
||||
} else {
|
||||
uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
poll[2] = pollCounter;
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false,
|
||||
true);
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
pollCounter++;
|
||||
@@ -265,13 +307,16 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
emit debug(QStringLiteral(" << ") + QString::number(value.length()) + QStringLiteral(" ") + value.toHex(' '));
|
||||
emit packetReceived();
|
||||
|
||||
if (newValue.length() < 51)
|
||||
if ((newValue.length() < 51 && !pitpat) || (newValue.length() < 50 && pitpat))
|
||||
return;
|
||||
|
||||
lastPacket = value;
|
||||
// lastState = value.at(0);
|
||||
|
||||
double speed = ((double)(((value[9] << 8) & 0xff) + value[10]) / 100.0);
|
||||
if(pitpat) {
|
||||
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
|
||||
}
|
||||
double incline = 0.0;
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -343,6 +388,20 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
void deerruntreadmill::btinit(bool startTape) {
|
||||
if (pitpat) {
|
||||
// PitPat treadmill initialization sequence
|
||||
uint8_t initData1[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("pitpat init 1"), false, true);
|
||||
|
||||
uint8_t unlockData[] = {0x6b, 0x05, 0x9d, 0x98, 0x43};
|
||||
writeUnlockCharacteristic(unlockData, sizeof(unlockData), QStringLiteral("pitpat unlock"), false);
|
||||
|
||||
uint8_t initData2[] = {0x6a, 0x05, 0xd7, 0xd2, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("pitpat init 2"), false, true);
|
||||
|
||||
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
|
||||
}
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
@@ -352,11 +411,30 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xfff1);
|
||||
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2);
|
||||
QBluetoothUuid _pitpatWriteCharacteristicId((quint16)0xfba1);
|
||||
QBluetoothUuid _pitpatNotifyCharacteristicId((quint16)0xfba2);
|
||||
QBluetoothUuid _unlockCharacteristicId((quint16)0x2b2a);
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
|
||||
QLowEnergyService* service = qobject_cast<QLowEnergyService*>(sender());
|
||||
if (service == unlock_service && pitpat) {
|
||||
// Handle unlock service characteristics
|
||||
auto characteristics_list = unlock_service->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("unlock char uuid") << c.uuid() << QStringLiteral("handle") << c.handle()
|
||||
<< c.properties();
|
||||
}
|
||||
|
||||
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId);
|
||||
if (unlock_characteristic.isValid()) {
|
||||
emit debug(QStringLiteral("unlock characteristic found"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << gattCommunicationChannelService->characteristics();
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
@@ -364,8 +442,14 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
<< c.properties();
|
||||
}
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
if (pitpat) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_pitpatWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_pitpatNotifyCharacteristicId);
|
||||
} else {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
}
|
||||
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotifyCharacteristic.isValid());
|
||||
|
||||
@@ -403,6 +487,8 @@ void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha
|
||||
|
||||
void deerruntreadmill::serviceScanDone(void) {
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0);
|
||||
QBluetoothUuid _pitpatServiceId((quint16)0xfba0);
|
||||
QBluetoothUuid _unlockServiceId((quint16)0x1801);
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
@@ -411,7 +497,17 @@ void deerruntreadmill::serviceScanDone(void) {
|
||||
emit debug(s.toString());
|
||||
}
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
// Check if this is a pitpat treadmill by looking for the 0xfba0 service
|
||||
if (services_list.contains(_pitpatServiceId)) {
|
||||
pitpat = true;
|
||||
emit debug(QStringLiteral("Detected pitpat treadmill variant"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_pitpatServiceId);
|
||||
unlock_service = m_control->createServiceObject(_unlockServiceId);
|
||||
} else {
|
||||
pitpat = false;
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService) {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
|
||||
&deerruntreadmill::stateChanged);
|
||||
@@ -419,6 +515,12 @@ void deerruntreadmill::serviceScanDone(void) {
|
||||
} else {
|
||||
emit debug(QStringLiteral("error on find Service"));
|
||||
}
|
||||
|
||||
if (pitpat && unlock_service) {
|
||||
connect(unlock_service, &QLowEnergyService::stateChanged, this,
|
||||
&deerruntreadmill::stateChanged);
|
||||
unlock_service->discoverDetails();
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::errorService(QLowEnergyService::ServiceError err) {
|
||||
|
||||
@@ -48,6 +48,7 @@ class deerruntreadmill : public treadmill {
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len,
|
||||
const QString &info, bool disable_log = false, bool wait_for_response = false);
|
||||
void writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false);
|
||||
void startDiscover();
|
||||
uint8_t calculateXOR(uint8_t arr[], size_t size);
|
||||
bool noConsole = false;
|
||||
@@ -66,6 +67,11 @@ class deerruntreadmill : public treadmill {
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
QLowEnergyService *unlock_service = nullptr;
|
||||
QLowEnergyCharacteristic unlock_characteristic;
|
||||
|
||||
bool pitpat = false;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
@@ -150,6 +150,11 @@ void echelonconnectsport::update() {
|
||||
initDone) {
|
||||
update_metrics(true, watts());
|
||||
|
||||
// Continuous ERG mode support - recalculate resistance as cadence changes when using power zone tiles
|
||||
if (RequestedPower.value() > 0) {
|
||||
changePower(RequestedPower.value());
|
||||
}
|
||||
|
||||
// sending poll every 2 seconds
|
||||
if (sec1Update++ >= (2000 / refresh->interval())) {
|
||||
sec1Update = 0;
|
||||
|
||||
@@ -65,6 +65,14 @@ void fakebike::update() {
|
||||
speedLimit());
|
||||
}
|
||||
|
||||
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
if (watts())
|
||||
KCal +=
|
||||
((((0.048 * ((double)watts()) + 1.19) * weight * 3.5) / 200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
@@ -171,28 +179,7 @@ uint16_t fakebike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t fakebike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1141,20 +1141,5 @@ uint16_t fitplusbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t fitplusbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return max_resistance;
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ void fitshowtreadmill::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << "adding" << gatt.toString() << "as the default service";
|
||||
serviceId = gatt; // NOTE: clazy-rule-of-tow
|
||||
}
|
||||
if(gatt == QBluetoothUuid((quint16)0x1826) && !fs_connected) {
|
||||
if(gatt == QBluetoothUuid((quint16)0x1826) && !fs_connected && !tunturi_t80_connected) {
|
||||
QSettings settings;
|
||||
settings.setValue(QZSettings::ftms_treadmill, bluetoothDevice.name());
|
||||
qDebug() << "forcing FTMS treadmill since it has FTMS";
|
||||
@@ -845,6 +845,9 @@ void fitshowtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << "NOBLEPRO FIX!";
|
||||
minStepInclinationValue = 0.5;
|
||||
noblepro_connected = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-"))) {
|
||||
qDebug() << "TUNTURI T80 detected - ignoring FTMS forcing";
|
||||
tunturi_t80_connected = true;
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -153,6 +153,7 @@ class fitshowtreadmill : public treadmill {
|
||||
double minStepInclinationValue = 1.0;
|
||||
bool noblepro_connected = false;
|
||||
bool fs_connected = false;
|
||||
bool tunturi_t80_connected = false;
|
||||
|
||||
metric rawInclination;
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ void ftmsbike::init() {
|
||||
if (initDone)
|
||||
return;
|
||||
|
||||
if(ICSE) {
|
||||
if(ICSE || HAMMER) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
|
||||
write[0] = {FTMS_RESET};
|
||||
@@ -194,7 +194,7 @@ void ftmsbike::zwiftPlayInit() {
|
||||
}
|
||||
|
||||
void ftmsbike::forcePower(int16_t requestPower) {
|
||||
if(resistance_lvl_mode || TITAN_7000) {
|
||||
if((resistance_lvl_mode || TITAN_7000) && !MAGNUS) {
|
||||
forceResistance(resistanceFromPowerRequest(requestPower));
|
||||
} else {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
|
||||
@@ -216,26 +216,9 @@ uint16_t ftmsbike::wattsFromResistance(double resistance) {
|
||||
return _ergTable.estimateWattage(Cadence.value(), resistance);
|
||||
}
|
||||
|
||||
|
||||
resistance_t ftmsbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
if(DU30_bike)
|
||||
return max_resistance;
|
||||
else
|
||||
return _ergTable.getMaxResistance();
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
@@ -249,6 +232,10 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset);
|
||||
if(ergModeNotSupported) {
|
||||
if(requestResistance < 0) {
|
||||
qDebug() << "Negative resistance detected:" << requestResistance << "using fallback value 1";
|
||||
requestResistance = 1;
|
||||
}
|
||||
requestResistance = _inclinationResistanceTable.estimateInclination(requestResistance) * 10.0;
|
||||
qDebug() << "ergMode Not Supported so the resistance will be" << requestResistance;
|
||||
} else {
|
||||
@@ -414,8 +401,15 @@ void ftmsbike::update() {
|
||||
|
||||
lastGearValue = gears();
|
||||
|
||||
// if a classic request of power from zwift or any other platform is coming, will be transfereed on the ftmsCharacteristicChanged applying the gear mod too
|
||||
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio))) {
|
||||
// Power request routing logic:
|
||||
// 1. No virtualBike: route directly to bike
|
||||
// 2. VirtualBike not connected to FTMS: route directly to bike
|
||||
// 3. ZwiftPlay with gear ratio: route directly to bike
|
||||
// 4. ErgMode supported + power sensor: use delta power system (bypass FTMS routing)
|
||||
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"));
|
||||
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio) || (ergModeSupported && power_sensor))) {
|
||||
qDebug() << QStringLiteral("writing power") << requestPower;
|
||||
init();
|
||||
forcePower(requestPower);
|
||||
@@ -669,14 +663,22 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
if(DU30_bike) {
|
||||
m_watt = wattsFromResistance(Resistance.value());
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else if (MRK_S26C) {
|
||||
m_watt = Cadence.value() * (Resistance.value() * 1.16);
|
||||
emit debug(QStringLiteral("Current Watt (MRK-S26C formula): ") + QString::number(m_watt.value()));
|
||||
} else if (LYDSTO && watt_ignore_builtin) {
|
||||
m_watt = wattFromHR(true);
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
} else {
|
||||
double ftms_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
} else if(DOMYOS) {
|
||||
@@ -754,14 +756,15 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
uint16_t time_division = 1024;
|
||||
uint8_t index = 4;
|
||||
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
if (newValue.length() > 3) {
|
||||
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
if (newValue.length() > 3) {
|
||||
double ftms_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
emit powerChanged(m_watt.value());
|
||||
}
|
||||
|
||||
emit powerChanged(m_watt.value());
|
||||
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
@@ -1049,11 +1052,14 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
}
|
||||
|
||||
if (Flags.instantPower) {
|
||||
double ftms_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
m_rawWatt = ftms_watt; // Always update rawWatt from FTMS bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
index += 2;
|
||||
}
|
||||
@@ -1132,8 +1138,14 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
update_hr_from_external();
|
||||
}
|
||||
|
||||
if(resistance_received && requestPower == -1)
|
||||
_inclinationResistanceTable.collectData(Inclination.value(), Resistance.value(), m_watt.value());
|
||||
if(resistance_received && requestPower == -1) {
|
||||
// Apply the same gears modification as in ftmsCharacteristicChanged
|
||||
double gears_modified_inclination = Inclination.value();
|
||||
if (gears() != 0) {
|
||||
gears_modified_inclination += (gears() * GEARS_SLOPE_MULTIPLIER / 100.0);
|
||||
}
|
||||
_inclinationResistanceTable.collectData(gears_modified_inclination, Resistance.value(), m_watt.value());
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
@@ -1141,7 +1153,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -1324,15 +1336,27 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
|
||||
QSettings settings;
|
||||
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"));
|
||||
|
||||
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
|
||||
if (!autoResistance() || resistance_lvl_mode || ergModeNotSupported) {
|
||||
bool isPowerCommand = (newValue.length() > 0 && (uint8_t)newValue.at(0) == FTMS_SET_TARGET_POWER);
|
||||
|
||||
// FTMS routing filter logic:
|
||||
// - Block simulation commands (0x11) when resistance_lvl_mode=true
|
||||
// - Allow power commands (0x05) only when no external power sensor (delta power system handles external sensors)
|
||||
bool allowPowerRouting = (!power_sensor && ergModeSupported && isPowerCommand);
|
||||
|
||||
if (!autoResistance() || (resistance_lvl_mode && !allowPowerRouting) || ergModeNotSupported) {
|
||||
qDebug() << "ignoring routing FTMS packet to the bike from virtualbike because of auto resistance OFF or resistance lvl mode is on or ergModeNotSupported"
|
||||
<< characteristic.uuid() << newValue.toHex(' ') << ergModeNotSupported << resistance_lvl_mode;
|
||||
<< characteristic.uuid() << newValue.toHex(' ') << "ergModeNotSupported:" << ergModeNotSupported
|
||||
<< "resistance_lvl_mode:" << resistance_lvl_mode << "power_sensor:" << power_sensor << "isPowerCommand:" << isPowerCommand;
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray b = newValue;
|
||||
QSettings settings;
|
||||
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
|
||||
|
||||
if (gattWriteCharControlPointId.isValid()) {
|
||||
@@ -1353,7 +1377,7 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
int16_t slope = (((uint8_t)b.at(3)) + (b.at(4) << 8));
|
||||
|
||||
if (gears() != 0) {
|
||||
slope += (gears() * 50);
|
||||
slope += (gears() * GEARS_SLOPE_MULTIPLIER);
|
||||
}
|
||||
|
||||
if(min_inclination > (((double)slope) / 100.0)) {
|
||||
@@ -1507,7 +1531,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
max_resistance = 16;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("MAGNUS "))) {
|
||||
qDebug() << QStringLiteral("MAGNUS found");
|
||||
resistance_lvl_mode = true;
|
||||
MAGNUS = true;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("DU30-"))) {
|
||||
qDebug() << QStringLiteral("DU30 found");
|
||||
max_resistance = 32;
|
||||
@@ -1518,6 +1544,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("DOMYOS"))) {
|
||||
qDebug() << QStringLiteral("DOMYOS found");
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
DOMYOS = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
|
||||
qDebug() << QStringLiteral("_3G_Cardio_RB found");
|
||||
@@ -1551,6 +1578,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("SPAX-BK-"))) {
|
||||
qDebug() << QStringLiteral("SPAX-BK found");
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
|
||||
qDebug() << QStringLiteral("LYDSTO found");
|
||||
LYDSTO = true;
|
||||
@@ -1559,11 +1587,13 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
SL010 = true;
|
||||
max_resistance = 25;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("REEBOK"))) {
|
||||
qDebug() << QStringLiteral("REEBOK found");
|
||||
REEBOK = true;
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("TITAN 7000"))) {
|
||||
qDebug() << QStringLiteral("Titan 7000 found");
|
||||
TITAN_7000 = true;
|
||||
@@ -1594,7 +1624,17 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
max_resistance = 32;
|
||||
resistance_lvl_mode = true;
|
||||
ergModeSupported = false; // this bike doesn't have ERG mode natively
|
||||
} else if(device.name().toUpper().startsWith("VANRYSEL-HT")) {
|
||||
qDebug() << QStringLiteral("VANRYSEL-HT found");
|
||||
VANRYSEL_HT = true;
|
||||
} else if(device.name().toUpper().startsWith("MRK-S26C-")) {
|
||||
qDebug() << QStringLiteral("MRK-S26C found");
|
||||
MRK_S26C = true;
|
||||
} else if(device.name().toUpper().startsWith("HAMMER")) {
|
||||
qDebug() << QStringLiteral("HAMMER found");
|
||||
HAMMER = true;
|
||||
}
|
||||
|
||||
|
||||
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
|
||||
resistance_lvl_mode = true;
|
||||
@@ -1652,7 +1692,7 @@ void ftmsbike::setWheelDiameter(double diameter) {
|
||||
}
|
||||
|
||||
uint16_t ftmsbike::watts() {
|
||||
if (currentCadence().value() == 0) {
|
||||
if (currentCadence().value() == 0 && !VANRYSEL_HT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ class ftmsbike : public bike {
|
||||
double maxGears() override;
|
||||
double minGears() override;
|
||||
|
||||
// true because or the bike supports it by hardware or because QZ is emulating this in this module
|
||||
bool ergModeSupportedAvailableBySoftware() override { return true; }
|
||||
|
||||
private:
|
||||
bool writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
|
||||
bool wait_for_response = false);
|
||||
@@ -94,6 +97,9 @@ class ftmsbike : public bike {
|
||||
uint16_t wattsFromResistance(double resistance);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
// Gear modification constants
|
||||
static constexpr int GEARS_SLOPE_MULTIPLIER = 50;
|
||||
|
||||
QList<QLowEnergyService *> gattCommunicationChannelService;
|
||||
QLowEnergyCharacteristic gattWriteCharControlPointId;
|
||||
@@ -151,6 +157,10 @@ class ftmsbike : public bike {
|
||||
bool PM5 = false;
|
||||
bool THINK_X = false;
|
||||
bool WLT8828 = false;
|
||||
bool VANRYSEL_HT = false;
|
||||
bool MAGNUS = false;
|
||||
bool MRK_S26C = false;
|
||||
bool HAMMER = false;
|
||||
|
||||
int16_t T2_lastGear = 0;
|
||||
|
||||
@@ -159,7 +169,6 @@ class ftmsbike : public bike {
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
QDateTime lastGoodCadence = QDateTime::currentDateTime();
|
||||
double lastRawRequestedInclinationValue = -100;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -62,12 +62,11 @@ void ftmsrower::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
|
||||
|
||||
void ftmsrower::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
write[3] = ((uint16_t)requestResistance * 100) & 0xFF;
|
||||
write[4] = ((uint16_t)requestResistance * 100) >> 8;
|
||||
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
|
||||
write[1] = ((uint8_t)(requestResistance * 10));
|
||||
writeCharacteristic(write, sizeof(write), QStringLiteral("forceResistance ") + QString::number(requestResistance));
|
||||
if(NORDLYS)
|
||||
Resistance = requestResistance; // Nordlys does not report back the resistance so we set it here
|
||||
}
|
||||
|
||||
void ftmsrower::update() {
|
||||
@@ -143,6 +142,123 @@ void ftmsrower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
|
||||
}
|
||||
|
||||
void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QSettings settings;
|
||||
|
||||
QString charUuid = characteristic.uuid().toString();
|
||||
|
||||
if (charUuid == QStringLiteral("{ce060031-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060031 - Based on go-row implementation
|
||||
if (newValue.length() >= 10) {
|
||||
// Extract RowState from byte 9 - this indicates if user is actively rowing
|
||||
pm5RowState = (uint8_t)newValue.at(9);
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060031 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060032-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060032 - Based on go-row implementation
|
||||
if (newValue.length() >= 7) {
|
||||
// Extract cadence (SPM) from byte 5
|
||||
uint8_t spm = (uint8_t)newValue.at(5);
|
||||
if (spm > 0) {
|
||||
Cadence = spm;
|
||||
lastStroke = now;
|
||||
}
|
||||
|
||||
// Extract speed from bytes 3-4 (little endian) in 0.001m/s
|
||||
uint16_t speedRaw = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
|
||||
if (speedRaw > 0) {
|
||||
Speed = (speedRaw * 0.001) * 3.6; // Convert m/s to km/h
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060032 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" Cadence: ") + QString::number(Cadence.value()) +
|
||||
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060033-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060033 - Additional data
|
||||
if (newValue.length() >= 20) {
|
||||
emit debug(QStringLiteral("PM5 CE060033 RAW: ") + newValue.toHex(' '));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060036-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060036 - Power and stroke count (based on go-row implementation)
|
||||
if (newValue.length() >= 9) {
|
||||
// Extract stroke count from bytes 7-8 (little endian)
|
||||
uint16_t strokeCount = ((uint8_t)newValue.at(8) << 8) | (uint8_t)newValue.at(7);
|
||||
if (strokeCount != StrokesCount.value()) {
|
||||
StrokesCount = strokeCount;
|
||||
lastStroke = now;
|
||||
}
|
||||
|
||||
// Extract power from bytes 3-4 (little endian)
|
||||
uint16_t power = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
|
||||
if (power > 0) {
|
||||
m_watt = power;
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060036 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
|
||||
QStringLiteral(" Stroke Count: ") + QString::number(StrokesCount.value()) +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
else if (charUuid == QStringLiteral("{ce060035-43e5-11e4-916c-0800200c9a66}")) {
|
||||
// Parse characteristic CE060035 - Stroke data including drive length (stroke length)
|
||||
if (newValue.length() >= 7) {
|
||||
// Extract drive length (stroke length) from byte 6 - 0.01 meters LSB, max 2.55m
|
||||
uint8_t driveLengthRaw = (uint8_t)newValue.at(6);
|
||||
if (driveLengthRaw > 0) {
|
||||
// Convert from 0.01m units to meters
|
||||
double strokeLengthMeters = driveLengthRaw * 0.01;
|
||||
StrokesLength = strokeLengthMeters;
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("PM5 CE060035 RAW: ") + newValue.toHex(' ') +
|
||||
QStringLiteral(" Stroke Length: ") + QString::number(StrokesLength.value()) +
|
||||
QStringLiteral("m RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
}
|
||||
|
||||
// Update calories based on power if available
|
||||
if (m_watt.value() > 0) {
|
||||
KCal += ((((0.048 * ((double)m_watt.value()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(now))));
|
||||
}
|
||||
|
||||
// Update crank revolutions for virtual device compatibility
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = now;
|
||||
|
||||
// Apply RowState logic after all characteristics processing
|
||||
if (PM5 && pm5RowState == 0) {
|
||||
m_watt = 0;
|
||||
Cadence = 0;
|
||||
Speed = 0;
|
||||
}
|
||||
|
||||
// Update metrics for virtual device
|
||||
update_metrics(false, m_watt.value());
|
||||
|
||||
emit debug(QStringLiteral("PM5 Metrics - Cadence: ") + QString::number(Cadence.value()) +
|
||||
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
|
||||
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
|
||||
QStringLiteral(" Distance: ") + QString::number(Distance.value()) +
|
||||
QStringLiteral(" Calories: ") + QString::number(KCal.value()) +
|
||||
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
|
||||
}
|
||||
|
||||
void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
@@ -156,6 +272,17 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
|
||||
qDebug() << QStringLiteral(" << ") << characteristic.uuid() << " " << newValue.toHex(' ');
|
||||
|
||||
// Handle Concept2 PM5 characteristics as fallback when FTMS is not available
|
||||
if (PM5 && (characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060031-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060032-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060033-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060035-43e5-11e4-916c-0800200c9a66")) ||
|
||||
characteristic.uuid() == QBluetoothUuid(QStringLiteral("ce060036-43e5-11e4-916c-0800200c9a66")))) {
|
||||
|
||||
parseConcept2Data(characteristic, newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (characteristic.uuid() != QBluetoothUuid((quint16)0x2AD1)) {
|
||||
return;
|
||||
}
|
||||
@@ -569,6 +696,36 @@ void ftmsrower::serviceScanDone(void) {
|
||||
#endif
|
||||
|
||||
auto services_list = m_control->services();
|
||||
bool hasFTMSService = false;
|
||||
bool hasConcept2Services = false;
|
||||
|
||||
// Check if FTMS service (0x1826) is available
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
if (s == ftmsService) {
|
||||
hasFTMSService = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no FTMS service, check for Concept2 PM5 services
|
||||
if (!hasFTMSService && PM5) {
|
||||
QBluetoothUuid concept2InfoService(QStringLiteral("ce060010-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2ControlService(QStringLiteral("ce060020-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2RowingService(QStringLiteral("ce060030-43e5-11e4-916c-0800200c9a66"));
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
if (s == concept2InfoService || s == concept2ControlService || s == concept2RowingService) {
|
||||
hasConcept2Services = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasConcept2Services) {
|
||||
emit debug(QStringLiteral("PM5 without FTMS service detected, using Concept2 protocol"));
|
||||
}
|
||||
}
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
@@ -619,6 +776,9 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
|
||||
PM5 = true;
|
||||
qDebug() << "PM5 found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("NORDLYS"))) {
|
||||
NORDLYS = true;
|
||||
qDebug() << "NORDLYS found!";
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
|
||||
@@ -46,6 +46,7 @@ class ftmsrower : public rower {
|
||||
void startDiscover();
|
||||
uint16_t watts() override;
|
||||
void forceResistance(resistance_t requestResistance);
|
||||
void parseConcept2Data(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
@@ -69,6 +70,7 @@ class ftmsrower : public rower {
|
||||
bool WHIPR = false;
|
||||
bool KINGSMITH = false;
|
||||
bool PM5 = false;
|
||||
bool NORDLYS = false;
|
||||
|
||||
bool WATER_ROWER = false;
|
||||
bool DFIT_L_R = false;
|
||||
@@ -76,6 +78,9 @@ class ftmsrower : public rower {
|
||||
bool ROWER = false;
|
||||
QDateTime lastStroke = QDateTime::currentDateTime();
|
||||
double lastStrokesCount = 0;
|
||||
|
||||
// PM5 specific variables
|
||||
uint8_t pm5RowState = 0;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -972,7 +972,7 @@ void horizontreadmill::update() {
|
||||
forceIncline(requestInclination);
|
||||
|
||||
// this treadmill doesn't send the incline, so i'm forcing it manually
|
||||
if(SW_TREADMILL) {
|
||||
if(SW_TREADMILL || mobvoi_treadmill) {
|
||||
Inclination = requestInclination;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ void kineticinroadbike::characteristicChanged(const QLowEnergyCharacteristic &ch
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -448,7 +448,7 @@ void kingsmithr2treadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xFED7);
|
||||
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xFED8);
|
||||
|
||||
if (KS_NACH_X21C || KS_NGCH_G1C_2) {
|
||||
if (KS_NACH_X21C || KS_NGCH_G1C_2 || KS_HDSY_X21C_2) {
|
||||
_gattWriteCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED7-0000-1000-8000-00805f9b34fb"));
|
||||
_gattNotifyCharacteristicId = QBluetoothUuid(QStringLiteral("0002FED8-0000-1000-8000-00805f9b34fb"));
|
||||
} else if (KS_NGCH_G1C || KS_NACH_MXG || KS_NACH_X21C_2) {
|
||||
@@ -517,6 +517,12 @@ void kingsmithr2treadmill::serviceScanDone(void) {
|
||||
qDebug() << "KS_NACH_X21C default service id not found";
|
||||
_gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00011234-0000-1000-8000-00805f9b34fb"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
} else if(gattCommunicationChannelService == nullptr && KS_HDSY_X21C) {
|
||||
KS_HDSY_X21C_2 = true;
|
||||
KS_HDSY_X21C = false;
|
||||
qDebug() << "KS_HDSY_X21C default service id not found";
|
||||
_gattCommunicationChannelServiceId = QBluetoothUuid(QStringLiteral("00021234-0000-1000-8000-00805f9b34fb"));
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
} else if(gattCommunicationChannelService == nullptr && KS_NGCH_G1C) {
|
||||
KS_NGCH_G1C_2 = true;
|
||||
KS_NGCH_G1C = false;
|
||||
@@ -550,6 +556,9 @@ void kingsmithr2treadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("KS-NACH-X21C"))) {
|
||||
qDebug() << "KS-NACH-X21C workaround!";
|
||||
KS_NACH_X21C = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("KS-HDSY-X21C"))) {
|
||||
qDebug() << "KS-HDSY-X21C workaround!";
|
||||
KS_HDSY_X21C = true;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("KS-NGCH-G1C"))) {
|
||||
qDebug() << "KS-NGCH-G1C workaround!";
|
||||
KS_NGCH_G1C = true;
|
||||
|
||||
@@ -98,6 +98,8 @@ class kingsmithr2treadmill : public treadmill {
|
||||
|
||||
bool KS_NACH_X21C = false;
|
||||
bool KS_NACH_X21C_2 = false;
|
||||
bool KS_HDSY_X21C = false;
|
||||
bool KS_HDSY_X21C_2 = false;
|
||||
bool KS_NGCH_G1C = false;
|
||||
bool KS_NGCH_G1C_2 = false;
|
||||
bool KS_NACH_MXG = false;
|
||||
|
||||
@@ -470,7 +470,7 @@ void nordictrackifitadbelliptical::processPendingDatagrams() {
|
||||
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_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
503
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp
Normal file
503
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp
Normal file
@@ -0,0 +1,503 @@
|
||||
#include "nordictrackifitadbrower.h"
|
||||
#include "qzsettings.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
#include <chrono>
|
||||
#include <math.h>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
nordictrackifitadbrowerLogcatAdbThread::nordictrackifitadbrowerLogcatAdbThread(QString s) { Q_UNUSED(s) }
|
||||
|
||||
void nordictrackifitadbrowerLogcatAdbThread::run() {
|
||||
QSettings settings;
|
||||
QString ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
runAdbCommand("connect " + ip);
|
||||
|
||||
while (1) {
|
||||
runAdbTailCommand("logcat");
|
||||
if(adbCommandPending.length() != 0) {
|
||||
runAdbCommand(adbCommandPending);
|
||||
adbCommandPending = "";
|
||||
}
|
||||
msleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
QString nordictrackifitadbrowerLogcatAdbThread::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 nordictrackifitadbrowerLogcatAdbThread::runCommand(QString command) {
|
||||
if(adbCommandPending.length() == 0) {
|
||||
adbCommandPending = command;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void nordictrackifitadbrowerLogcatAdbThread::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;
|
||||
bool cadenceFound = false;
|
||||
bool resistanceFound = false;
|
||||
foreach (QString line, lines) {
|
||||
if (line.contains("Changed KPH") || line.contains("Changed Actual KPH")) {
|
||||
emit debug(line);
|
||||
speed = line.split(' ').last().toDouble();
|
||||
} else if (line.contains("Changed Resistance")) {
|
||||
emit debug(line);
|
||||
resistance = line.split(' ').last().toDouble();
|
||||
resistanceFound = true;
|
||||
} else if (line.contains("Changed RPM")) {
|
||||
emit debug(line);
|
||||
cadence = line.split(' ').last().toDouble();
|
||||
cadenceFound = true;
|
||||
} 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 onSpeedResistance(speed, resistance);
|
||||
if (cadenceFound)
|
||||
emit onCadence(cadence);
|
||||
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
|
||||
}
|
||||
|
||||
nordictrackifitadbrower::nordictrackifitadbrower(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, &nordictrackifitadbrower::update);
|
||||
ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_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 device init *************************************
|
||||
if (!firstStateChanged && !this->hasVirtualDevice()) {
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
bool virtual_device_force_bike =
|
||||
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && !virtual_device_rower) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
if (virtual_device_rower) {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
// connect(virtualRower,&virtualrower::debug ,this,&nordictrackifitadbrower::debug);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else if (virtual_device_force_bike) {
|
||||
qDebug() << QStringLiteral("creating virtual bike interface...");
|
||||
auto virtualBike =
|
||||
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
// connect(virtualBike,&virtualbike::debug ,this,&nordictrackifitadbrower::debug);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
// connect(virtualRower,&virtualrower::debug ,this,&nordictrackifitadbrower::debug);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
if (nordictrack_ifit_adb_remote) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
QAndroidJniObject IP = QAndroidJniObject::fromString(ip).object<jstring>();
|
||||
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote", "createConnection",
|
||||
"(Ljava/lang/String;Landroid/content/Context;)V",
|
||||
IP.object<jstring>(), QtAndroid::androidContext().object());
|
||||
#elif defined Q_OS_WIN
|
||||
logcatAdbThread = new nordictrackifitadbrowerLogcatAdbThread("logcatAdbThread");
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onCadence, this,
|
||||
&nordictrackifitadbrower::onCadence);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onSpeedResistance, this,
|
||||
&nordictrackifitadbrower::onSpeedResistance);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onWatt, this,
|
||||
&nordictrackifitadbrower::onWatt);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::onHRM, this, &nordictrackifitadbrower::onHRM);
|
||||
connect(logcatAdbThread, &nordictrackifitadbrowerLogcatAdbThread::debug, this, &nordictrackifitadbrower::debug);
|
||||
logcatAdbThread->start();
|
||||
#elif defined Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
h->adb_connect(ip.toStdString().c_str());
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onSpeedResistance(double speed, double resistance) {
|
||||
if(speed > 0)
|
||||
speedReadFromTM = true;
|
||||
Speed = speed;
|
||||
Resistance = resistance;
|
||||
resistanceReadFromTM = true;
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onWatt(double watt) {
|
||||
m_watt = watt;
|
||||
wattReadFromTM = true;
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::onCadence(double cadence) {
|
||||
Cadence = cadence;
|
||||
cadenceReadFromTM = true;
|
||||
}
|
||||
|
||||
double nordictrackifitadbrower::getDouble(QString v) {
|
||||
QChar d = QLocale().decimalPoint();
|
||||
if (d == ',') {
|
||||
v = v.replace('.', ',');
|
||||
}
|
||||
return QLocale().toDouble(v);
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::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::proform_rower_ip, QZSettings::default_proform_rower_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();
|
||||
|
||||
double speed = 0;
|
||||
double cadence = 0;
|
||||
double resistance = 0;
|
||||
double gear = 0;
|
||||
double watt = 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()) {
|
||||
speedReadFromTM = true;
|
||||
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;
|
||||
cadenceReadFromTM = true;
|
||||
if(!speedReadFromTM) {
|
||||
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());
|
||||
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; // adjusted for rower resistance range
|
||||
qDebug() << QStringLiteral("Current Peloton Resistance: ") << m_pelotonResistance.value()
|
||||
<< resistance;
|
||||
if(!gearsAvailable) {
|
||||
Resistance = resistance;
|
||||
resistanceReadFromTM = true;
|
||||
}
|
||||
}
|
||||
} else if (line.contains(QStringLiteral("Changed Watts"))) {
|
||||
QStringList aValues = line.split(" ");
|
||||
if (aValues.length()) {
|
||||
watt = getDouble(aValues.last());
|
||||
m_watt = watt;
|
||||
wattReadFromTM = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), 0, Speed.value(), // no inclination for rower
|
||||
fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), 20);
|
||||
}
|
||||
|
||||
bool nordictrack_ifit_adb_remote =
|
||||
settings.value(QZSettings::nordictrack_ifit_adb_remote, QZSettings::default_nordictrack_ifit_adb_remote)
|
||||
.toBool();
|
||||
|
||||
// resistance
|
||||
if (nordictrack_ifit_adb_remote) {
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance != currentResistance().value()) {
|
||||
int x1 = 1205; // Estimated x-coordinate of the resistance slider (right side)
|
||||
int y2 = (int)(590 - (15.65 * requestResistance));
|
||||
int y1Resistance = (int)(590 - (15.65 * currentResistance().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<jstring>();
|
||||
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/QZAdbRemote",
|
||||
"sendCommand", "(Ljava/lang/String;)V",
|
||||
command.object<jstring>());
|
||||
#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;
|
||||
} else {
|
||||
QByteArray message = (QString::number(requestResistance).toLocal8Bit()) + ";";
|
||||
requestResistance = -1;
|
||||
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
|
||||
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 Cadence: ") + QString::number(Cadence.value()));
|
||||
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 Speed: ") + QString::number(Speed.value()));
|
||||
emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value()));
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::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 nordictrackifitadbrower::forceResistance(double resistance) {}
|
||||
|
||||
void nordictrackifitadbrower::update() {
|
||||
|
||||
QSettings settings;
|
||||
update_metrics(false, 0);
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
// updating the rower 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 nordictrackifitadbrower::watts() {
|
||||
// If we have watts from the machine, use them
|
||||
if (wattReadFromTM && m_watt.value() > 0) {
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
// Otherwise calculate watts from resistance and cadence
|
||||
return wattsFromResistance(currentResistance().value(), currentCadence().value());
|
||||
}
|
||||
|
||||
void nordictrackifitadbrower::changeResistanceRequested(double resistance) {
|
||||
if (resistance < 0)
|
||||
resistance = 0;
|
||||
changeResistance(resistance);
|
||||
}
|
||||
|
||||
bool nordictrackifitadbrower::connected() { return true; }
|
||||
|
||||
uint16_t nordictrackifitadbrower::wattsFromResistance(double resistance, double cadence) {
|
||||
// Rower power estimation based on resistance and cadence
|
||||
// This formula is based on general rowing power curves
|
||||
// Power increases with both resistance and cadence (stroke rate)
|
||||
|
||||
if (cadence <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Basic power formula for rowing: power increases exponentially with stroke rate
|
||||
// and linearly with resistance level
|
||||
double basePower = resistance * 8; // Base power per resistance level
|
||||
double cadenceFactor = 1.0 + (cadence - 20.0) * 0.05; // Cadence multiplier
|
||||
|
||||
// Additional exponential component for higher stroke rates (similar to real rowing)
|
||||
double exponentialFactor = exp(cadence * 0.015);
|
||||
|
||||
double power = basePower * cadenceFactor * (exponentialFactor / 10.0);
|
||||
|
||||
// Ensure minimum and maximum bounds
|
||||
if (power < 10) power = 10;
|
||||
if (power > 500) power = 500; // Reasonable max for most users
|
||||
|
||||
return (uint16_t)power;
|
||||
}
|
||||
126
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h
Normal file
126
src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h
Normal file
@@ -0,0 +1,126 @@
|
||||
#ifndef NORDICTRACKIFITADBROWER_H
|
||||
#define NORDICTRACKIFITADBROWER_H
|
||||
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include "devices/rower.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class nordictrackifitadbrowerLogcatAdbThread : public QThread {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit nordictrackifitadbrowerLogcatAdbThread(QString s);
|
||||
bool runCommand(QString command);
|
||||
|
||||
void run() override;
|
||||
|
||||
signals:
|
||||
void onSpeedResistance(double speed, double resistance);
|
||||
void debug(QString message);
|
||||
void onWatt(double watt);
|
||||
void onHRM(int hrm);
|
||||
void onCadence(double cadence);
|
||||
|
||||
private:
|
||||
QString adbCommandPending = "";
|
||||
QString runAdbCommand(QString command);
|
||||
double speed = 0;
|
||||
double resistance = 0;
|
||||
double cadence = 0;
|
||||
double watt = 0;
|
||||
int hrm = 0;
|
||||
QString name;
|
||||
struct adbfile {
|
||||
QDateTime date;
|
||||
QString name;
|
||||
};
|
||||
|
||||
void runAdbTailCommand(QString command);
|
||||
};
|
||||
|
||||
class nordictrackifitadbrower : public rower {
|
||||
Q_OBJECT
|
||||
public:
|
||||
nordictrackifitadbrower(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
const resistance_t max_resistance = 32; // max resistance for rower
|
||||
void forceResistance(double resistance);
|
||||
uint16_t watts() override;
|
||||
double getDouble(QString v);
|
||||
uint16_t wattsFromResistance(double resistance, double cadence);
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
uint8_t sec1Update = 0;
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
QDateTime lastResistanceChanged = QDateTime::currentDateTime();
|
||||
uint8_t firstStateChanged = 0;
|
||||
uint16_t m_watts = 0;
|
||||
bool cadenceReadFromTM = false;
|
||||
bool resistanceReadFromTM = false;
|
||||
bool wattReadFromTM = false;
|
||||
bool speedReadFromTM = false;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
|
||||
bool gearsAvailable = false;
|
||||
|
||||
QUdpSocket *socket = nullptr;
|
||||
QHostAddress lastSender;
|
||||
|
||||
nordictrackifitadbrowerLogcatAdbThread *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 changeResistanceRequested(double resistance);
|
||||
void onHRM(int hrm);
|
||||
void onWatt(double watt);
|
||||
void onCadence(double cadence);
|
||||
void onSpeedResistance(double speed, double resistance);
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
#endif // NORDICTRACKIFITADBROWER_H
|
||||
@@ -196,6 +196,19 @@ void paferstreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
|
||||
|
||||
emit packetReceived();
|
||||
|
||||
if (newValue.length() == 4 && value.at(0) == 0x55 && value.at(1) == 0x09 && value.at(2) == 0x01 &&
|
||||
value.at(3) == 0x01) {
|
||||
qDebug() << "Paferstreadmill: pressing start button";
|
||||
emit tapeStarted();
|
||||
requestStart = 1;
|
||||
return;
|
||||
} else if (newValue.length() == 4 && value.at(0) == 0x55 && value.at(1) == 0x09 && value.at(2) == 0x01 &&
|
||||
value.at(3) == 0x00) {
|
||||
qDebug() << "Paferstreadmill: pressing stop button";
|
||||
requestStop = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((newValue.length() != 13))
|
||||
return;
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ void pitpatbike::characteristicChanged(const QLowEnergyCharacteristic &character
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -56,26 +56,41 @@ void proformbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QSt
|
||||
}
|
||||
|
||||
resistance_t proformbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
QSettings settings;
|
||||
|
||||
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
|
||||
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
|
||||
|
||||
if (proform_225_csx_PFEX32925_INT_0) {
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
resistance_t best_resistance_match = 1;
|
||||
int min_watt_difference = 1000;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; i++) {
|
||||
if (((wattsFromResistance(i) * watt_gain) + watt_offset) <= power &&
|
||||
((wattsFromResistance(i + 1) * watt_gain) + watt_offset) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest")
|
||||
<< ((wattsFromResistance(i) * watt_gain) + watt_offset)
|
||||
<< ((wattsFromResistance(i + 1) * watt_gain) + watt_offset) << power;
|
||||
uint16_t current_watts = (wattsFromResistance(i) * watt_gain) + watt_offset;
|
||||
uint16_t next_watts = (wattsFromResistance(i + 1) * watt_gain) + watt_offset;
|
||||
|
||||
if (current_watts <= power && next_watts >= power) {
|
||||
qDebug() << current_watts << next_watts << power;
|
||||
return i;
|
||||
}
|
||||
|
||||
int diff = abs(current_watts - power);
|
||||
if (diff < min_watt_difference) {
|
||||
min_watt_difference = diff;
|
||||
best_resistance_match = i;
|
||||
qDebug() << QStringLiteral("best match") << best_resistance_match << "with watts" << current_watts << "diff" << diff;
|
||||
}
|
||||
}
|
||||
if (power < ((wattsFromResistance(1) * watt_gain) + watt_offset))
|
||||
return 1;
|
||||
else
|
||||
return max_resistance;
|
||||
|
||||
qDebug() << "Bracketing not found, best match:" << best_resistance_match;
|
||||
return best_resistance_match;
|
||||
}
|
||||
|
||||
uint16_t proformbike::wattsFromResistance(resistance_t resistance) {
|
||||
@@ -275,6 +290,94 @@ void proformbike::forceResistance(resistance_t requestResistance) {
|
||||
uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02};
|
||||
writeCharacteristic((uint8_t *)noOpData7, sizeof(noOpData7), QStringLiteral("resrequest"), false, false);
|
||||
|
||||
switch (requestResistance) {
|
||||
case 1:
|
||||
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
|
||||
break;
|
||||
case 2:
|
||||
writeCharacteristic((uint8_t *)res2, sizeof(res2), QStringLiteral("resistance2"), false, true);
|
||||
break;
|
||||
case 3:
|
||||
writeCharacteristic((uint8_t *)res3, sizeof(res3), QStringLiteral("resistance3"), false, true);
|
||||
break;
|
||||
case 4:
|
||||
writeCharacteristic((uint8_t *)res4, sizeof(res4), QStringLiteral("resistance4"), false, true);
|
||||
break;
|
||||
case 5:
|
||||
writeCharacteristic((uint8_t *)res5, sizeof(res5), QStringLiteral("resistance5"), false, true);
|
||||
break;
|
||||
case 6:
|
||||
writeCharacteristic((uint8_t *)res6, sizeof(res6), QStringLiteral("resistance6"), false, true);
|
||||
break;
|
||||
case 7:
|
||||
writeCharacteristic((uint8_t *)res7, sizeof(res7), QStringLiteral("resistance7"), false, true);
|
||||
break;
|
||||
case 8:
|
||||
writeCharacteristic((uint8_t *)res8, sizeof(res8), QStringLiteral("resistance8"), false, true);
|
||||
break;
|
||||
case 9:
|
||||
writeCharacteristic((uint8_t *)res9, sizeof(res9), QStringLiteral("resistance9"), false, true);
|
||||
break;
|
||||
case 10:
|
||||
writeCharacteristic((uint8_t *)res10, sizeof(res10), QStringLiteral("resistance10"), false, true);
|
||||
break;
|
||||
case 11:
|
||||
writeCharacteristic((uint8_t *)res11, sizeof(res11), QStringLiteral("resistance11"), false, true);
|
||||
break;
|
||||
case 12:
|
||||
writeCharacteristic((uint8_t *)res12, sizeof(res12), QStringLiteral("resistance12"), false, true);
|
||||
break;
|
||||
case 13:
|
||||
writeCharacteristic((uint8_t *)res13, sizeof(res13), QStringLiteral("resistance13"), false, true);
|
||||
break;
|
||||
case 14:
|
||||
writeCharacteristic((uint8_t *)res14, sizeof(res14), QStringLiteral("resistance14"), false, true);
|
||||
break;
|
||||
case 15:
|
||||
writeCharacteristic((uint8_t *)res15, sizeof(res15), QStringLiteral("resistance15"), false, true);
|
||||
break;
|
||||
case 16:
|
||||
writeCharacteristic((uint8_t *)res16, sizeof(res16), QStringLiteral("resistance16"), false, true);
|
||||
break;
|
||||
}
|
||||
} else if (proform_csx210) {
|
||||
// ProForm CSX210 specific resistance frames (1-16)
|
||||
const uint8_t res1[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x02,
|
||||
0x00, 0x10, 0x01, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res2[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x02,
|
||||
0x00, 0x10, 0x03, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res3[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x52, 0x07, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res4[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xc3, 0x09, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res5[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x34, 0x0c, 0x00, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res6[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xa5, 0x0e, 0x00, 0xca, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res7[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x16, 0x11, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res8[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x87, 0x13, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res9[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xf8, 0x15, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res10[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x69, 0x18, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res11[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xda, 0x1a, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res12[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x4b, 0x1d, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res13[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0xbc, 0x1f, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res14[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x2d, 0x22, 0x00, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res15[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x9e, 0x24, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t res16[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x01,
|
||||
0x04, 0x0f, 0x27, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
uint8_t noOpData7[] = {0xfe, 0x02, 0x0d, 0x02};
|
||||
writeCharacteristic((uint8_t *)noOpData7, sizeof(noOpData7), QStringLiteral("resrequest"), false, false);
|
||||
|
||||
switch (requestResistance) {
|
||||
case 1:
|
||||
writeCharacteristic((uint8_t *)res1, sizeof(res1), QStringLiteral("resistance1"), false, true);
|
||||
@@ -865,7 +968,7 @@ void proformbike::update() {
|
||||
uint8_t noOpData5_proform_bike_PFEVEX71316_0[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x08, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x81, 0x0e, 0x41, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80};
|
||||
|
||||
uint8_t noOpData6_proform_bike_PFEVEX71316_0[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x80, 0x3a, 0x00, 0x00, 0x00,
|
||||
uint8_t noOpData6_proform_bike_PFEVEX71316_0[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xaa, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
uint8_t noOpData2_proform_bike_325_csx[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00, 0x0d, 0x3c, 0x9e, 0x31, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80};
|
||||
@@ -880,10 +983,24 @@ void proformbike::update() {
|
||||
uint8_t noOpData5_proform_xbike[] = {0xff, 0x0d, 0x02, 0x04, 0x02, 0x09, 0x07, 0x09, 0x02, 0x00,
|
||||
0x03, 0x80, 0x00, 0x40, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// proform_csx210
|
||||
uint8_t noOpData1_proform_csx210[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t noOpData2_proform_csx210[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData3_proform_csx210[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0xb9, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t noOpData4_proform_csx210[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t noOpData5_proform_csx210[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x3c, 0x96, 0x71, 0x00, 0x10, 0x40, 0x40, 0x00, 0x80};
|
||||
uint8_t noOpData6_proform_csx210[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x81, 0xfd, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx || proform_bike_325_csx || proform_xbike || proform_225_csx_PFEX32925_INT_0) {
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData1_proform_csx210, sizeof(noOpData1_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_225_csx || proform_bike_325_csx || proform_xbike || proform_225_csx_PFEX32925_INT_0) {
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
} else if(proform_bike_PFEVEX71316_0) {
|
||||
writeCharacteristic(noOpData1_proform_bike_PFEVEX71316_0, sizeof(noOpData1_proform_bike_PFEVEX71316_0), QStringLiteral("noOp"));
|
||||
@@ -892,7 +1009,9 @@ void proformbike::update() {
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (proform_xbike) {
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData2_proform_csx210, sizeof(noOpData2_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData2_proform_xbike, sizeof(noOpData2_proform_xbike), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData2_proform_studio, sizeof(noOpData2_proform_studio), QStringLiteral("noOp"));
|
||||
@@ -926,7 +1045,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData2, sizeof(noOpData2), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 2:
|
||||
if (proform_xbike) {
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData3_proform_csx210, sizeof(noOpData3_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData3_proform_xbike, sizeof(noOpData3_proform_xbike), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData3_proform_studio, sizeof(noOpData3_proform_studio), QStringLiteral("noOp"));
|
||||
@@ -960,7 +1081,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData3, sizeof(noOpData3), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 3:
|
||||
if (proform_xbike) {
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData4_proform_csx210, sizeof(noOpData4_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
innerWriteResistance();
|
||||
writeCharacteristic(noOpData7, sizeof(noOpData7), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
@@ -983,7 +1106,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData4, sizeof(noOpData4), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 4:
|
||||
if (proform_xbike) {
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData5_proform_csx210, sizeof(noOpData5_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_xbike) {
|
||||
writeCharacteristic(noOpData5_proform_xbike, sizeof(noOpData5_proform_xbike), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData5_proform_studio, sizeof(noOpData5_proform_studio), QStringLiteral("noOp"));
|
||||
@@ -1011,7 +1136,9 @@ void proformbike::update() {
|
||||
writeCharacteristic(noOpData5, sizeof(noOpData5), QStringLiteral("noOp"));
|
||||
break;
|
||||
case 5:
|
||||
if (proform_studio || proform_tdf_10)
|
||||
if (proform_csx210) {
|
||||
writeCharacteristic(noOpData6_proform_csx210, sizeof(noOpData6_proform_csx210), QStringLiteral("noOp"));
|
||||
} else if (proform_studio || proform_tdf_10)
|
||||
writeCharacteristic(noOpData6_proform_studio, sizeof(noOpData6_proform_studio), QStringLiteral("noOp"));
|
||||
else if (proform_tour_de_france_clc) {
|
||||
writeCharacteristic(noOpData6_proform_tour_de_france_clc, sizeof(noOpData6_proform_tour_de_france_clc),
|
||||
@@ -1074,7 +1201,7 @@ void proformbike::update() {
|
||||
requestResistance == -1) {
|
||||
// this bike sends the frame noOpData7 only when it needs to change the resistance
|
||||
counterPoll = 0;
|
||||
} else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_325_csx || proform_xbike)) {
|
||||
} else if (counterPoll == 5 && (nordictrack_gx_2_7 || proform_cycle_trainer_300_ci || proform_hybrid_trainer_PFEL03815 || proform_bike_sb || proform_bike_325_csx || proform_xbike || proform_csx210)) {
|
||||
counterPoll = 0;
|
||||
}
|
||||
|
||||
@@ -1185,7 +1312,9 @@ void proformbike::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
(newValue.at(0) != 0x00 && newValue.at(0) != 0x01) || newValue.at(1) != 0x12 ||
|
||||
(newValue.at(0) == 0x00 &&
|
||||
(newValue.at(2) != 0x01 || newValue.at(3) != 0x04 || newValue.at(4) != 0x02 || (proform_bike_PFEVEX71316_0 ? newValue.at(5) != 0x30 : newValue.at(5) != 0x2c))) ||
|
||||
(proform_bike_PFEVEX71316_0 && (uint8_t)newValue.at(2) == 0xFF && (uint8_t)newValue.at(3) == 0xFF)) {
|
||||
(proform_bike_PFEVEX71316_0 && (uint8_t)newValue.at(2) == 0xFF && (uint8_t)newValue.at(3) == 0xFF) ||
|
||||
(proform_bike_PFEVEX71316_0 && (uint8_t)newValue.at(14) == 0xFF && (uint8_t)newValue.at(15) == 0xFF &&
|
||||
(uint8_t)newValue.at(16) == 0xFF && (uint8_t)newValue.at(17) == 0xFF)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1950,10 +2079,13 @@ void proformbike::btinit() {
|
||||
proform_bike_PFEVEX71316_0 = settings.value(QZSettings::proform_bike_PFEVEX71316_0, QZSettings::default_proform_bike_PFEVEX71316_0).toBool();
|
||||
proform_xbike = settings.value(QZSettings::proform_xbike, QZSettings::default_proform_xbike).toBool();
|
||||
proform_225_csx_PFEX32925_INT_0 = settings.value(QZSettings::proform_225_csx_PFEX32925_INT_0, QZSettings::default_proform_225_csx_PFEX32925_INT_0).toBool();
|
||||
proform_csx210 = settings.value(QZSettings::proform_csx210, QZSettings::default_proform_csx210).toBool();
|
||||
|
||||
|
||||
if(nordictrack_GX4_5_bike)
|
||||
max_resistance = 25;
|
||||
if(proform_csx210)
|
||||
max_resistance = 16;
|
||||
|
||||
if (settings.value(QZSettings::proform_studio, QZSettings::default_proform_studio).toBool()) {
|
||||
|
||||
@@ -2923,6 +3055,178 @@ void proformbike::btinit() {
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
} else if (proform_csx210) {
|
||||
// ProForm CSX210 initialization sequence with 16 max resistance
|
||||
|
||||
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, 0x07, 0x04, 0x80, 0x8b,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x07, 0x04, 0x88, 0x93,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData5[] = {0xfe, 0x02, 0x0b, 0x02};
|
||||
uint8_t initData6[] = {0xff, 0x0b, 0x02, 0x04, 0x02, 0x07, 0x02, 0x07, 0x82, 0x00,
|
||||
0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData7[] = {0xfe, 0x02, 0x0a, 0x02};
|
||||
uint8_t initData8[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00,
|
||||
0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData9[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData10[] = {0xfe, 0x02, 0x2c, 0x04};
|
||||
|
||||
// Execute initial setup sequence
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData7, sizeof(initData7), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData8, sizeof(initData8), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData10, sizeof(initData10), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
|
||||
// Main initialization sequence
|
||||
uint8_t initData11[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x04,
|
||||
0x00, 0xb2, 0xf4, 0x34, 0x72, 0xbe, 0x08, 0x40, 0x9e, 0xea};
|
||||
uint8_t initData12[] = {0x01, 0x12, 0x3c, 0x8c, 0xda, 0x26, 0x90, 0xc8, 0x26, 0x82,
|
||||
0xe4, 0x44, 0xa2, 0x0e, 0x98, 0xf0, 0x4e, 0xda, 0x2c, 0xbc};
|
||||
uint8_t initData13[] = {0xff, 0x08, 0x0a, 0x96, 0x20, 0x80, 0x02, 0x00, 0x00, 0x17,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(initData11, sizeof(initData11), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData12, sizeof(initData12), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData13, sizeof(initData13), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
|
||||
// Service discovery and configuration sequence
|
||||
uint8_t initData14[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t initData15[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x07, 0x15, 0x02, 0x0e,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData16[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3d, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData17[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t initData18[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x0c,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData19[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa9, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData20[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x00, 0x10, 0x00, 0xc0, 0x1c, 0x4c, 0x00, 0x00, 0xe0};
|
||||
uint8_t initData21[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x51, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData18, sizeof(initData18), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData19, sizeof(initData19), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData20, sizeof(initData20), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData21, sizeof(initData21), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
|
||||
// Additional configuration and status frames
|
||||
uint8_t initData22[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x3c, 0x96, 0x71, 0x00, 0x10, 0x40, 0x40, 0x00, 0x80};
|
||||
uint8_t initData23[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x81, 0xfd, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData24[] = {0xfe, 0x02, 0x11, 0x02};
|
||||
uint8_t initData25[] = {0xff, 0x11, 0x02, 0x04, 0x02, 0x0d, 0x07, 0x0d, 0x02, 0x05,
|
||||
0x00, 0x00, 0x00, 0x00, 0x08, 0x58, 0x02, 0x00, 0x7d, 0x00};
|
||||
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData22, sizeof(initData22), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData23, sizeof(initData23), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData24, sizeof(initData24), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData25, sizeof(initData25), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
|
||||
// Final status and configuration frames
|
||||
uint8_t initData26[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData27[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0xb9, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData28[] = {0xfe, 0x02, 0x10, 0x02};
|
||||
uint8_t initData29[] = {0xff, 0x10, 0x02, 0x04, 0x02, 0x0c, 0x07, 0x0c, 0x02, 0x04,
|
||||
0x00, 0x00, 0x00, 0x02, 0x98, 0x21, 0x00, 0xd4, 0x00, 0x00};
|
||||
uint8_t initData30[] = {0xfe, 0x02, 0x10, 0x02};
|
||||
uint8_t initData31[] = {0xff, 0x10, 0x02, 0x04, 0x02, 0x0c, 0x07, 0x0c, 0x02, 0x05,
|
||||
0x00, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x2b, 0x00, 0x00};
|
||||
uint8_t initData32[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t initData33[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x0c,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData34[] = {0xff, 0x05, 0x00, 0x80, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t initData35[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t initData36[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x07, 0x13, 0x02, 0x00,
|
||||
0x0d, 0x3c, 0x96, 0x71, 0x00, 0x10, 0x40, 0x40, 0x00, 0x80};
|
||||
uint8_t initData37[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x81, 0xfd, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(initData14, sizeof(initData14), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData15, sizeof(initData15), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData16, sizeof(initData16), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData28, sizeof(initData28), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData29, sizeof(initData29), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData17, sizeof(initData17), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData26, sizeof(initData26), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData27, sizeof(initData27), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData30, sizeof(initData30), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData31, sizeof(initData31), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData32, sizeof(initData32), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData33, sizeof(initData33), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData34, sizeof(initData34), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData35, sizeof(initData35), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData36, sizeof(initData36), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic(initData37, sizeof(initData37), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
} else {
|
||||
|
||||
uint8_t initData10[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x07, 0x28, 0x90, 0x07,
|
||||
|
||||
@@ -96,6 +96,7 @@ class proformbike : public bike {
|
||||
bool proform_bike_PFEVEX71316_0 = false;
|
||||
bool proform_xbike = false;
|
||||
bool proform_225_csx_PFEX32925_INT_0 = false;
|
||||
bool proform_csx210 = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -397,20 +397,5 @@ uint16_t proformtelnetbike::wattsFromResistance(resistance_t resistance) {
|
||||
}
|
||||
|
||||
resistance_t proformtelnetbike::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();
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
@@ -249,6 +249,11 @@ void solebike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
return;
|
||||
}
|
||||
|
||||
if (((unsigned char)newValue.at(1)) != 0x13) {
|
||||
qDebug() << QStringLiteral("not a valid packet");
|
||||
return;
|
||||
}
|
||||
|
||||
double distance = GetDistanceFromPacket(newValue);
|
||||
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
|
||||
@@ -345,14 +345,15 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
uint16_t time_division = 1024;
|
||||
uint8_t index = 4;
|
||||
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
if (newValue.length() > 3) {
|
||||
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
if (newValue.length() > 3) {
|
||||
double tacx_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
|
||||
m_rawWatt = tacx_watt; // Always update rawWatt from TACX bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = tacx_watt; // Only update watt if no external power sensor
|
||||
emit powerChanged(m_watt.value());
|
||||
}
|
||||
|
||||
emit powerChanged(m_watt.value());
|
||||
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
@@ -635,11 +636,14 @@ void tacxneo2::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
|
||||
if (Flags.instantPower) {
|
||||
// power table from an user
|
||||
double tacx_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
m_rawWatt = tacx_watt; // Always update rawWatt from TACX bike data
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled")))
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index))));
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = tacx_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
@@ -104,22 +104,7 @@ uint16_t technogymbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t technogymbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < max_resistance; i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return max_resistance;
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), max_resistance);
|
||||
}
|
||||
|
||||
void technogymbike::forceResistance(resistance_t requestResistance) {
|
||||
@@ -424,7 +409,7 @@ void technogymbike::characteristicChanged(const QLowEnergyCharacteristic &charac
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -33,9 +33,16 @@ void toorxtreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this,
|
||||
&toorxtreadmill::serviceDiscovered);
|
||||
|
||||
// Start a discovery
|
||||
qDebug() << QStringLiteral("toorxtreadmill::deviceDiscovered");
|
||||
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
|
||||
// Start a discovery - use FullDiscovery only if not done before
|
||||
QSettings settings;
|
||||
bool discoveryCompleted = settings.value(QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed).toBool();
|
||||
qDebug() << QStringLiteral("toorxtreadmill::deviceDiscovered - discoveryCompleted:") << discoveryCompleted;
|
||||
|
||||
if (discoveryCompleted) {
|
||||
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::MinimalDiscovery);
|
||||
} else {
|
||||
discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -382,6 +389,14 @@ void toorxtreadmill::rfCommConnected() {
|
||||
qDebug() << QStringLiteral(" init1 write");
|
||||
socket->write((char *)init2, sizeof(init2));
|
||||
qDebug() << QStringLiteral(" init2 write");
|
||||
|
||||
// Mark discovery as completed for future connections
|
||||
QSettings settings;
|
||||
if (!settings.value(QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed).toBool()) {
|
||||
settings.setValue(QZSettings::toorxtreadmill_discovery_completed, true);
|
||||
qDebug() << QStringLiteral("toorxtreadmill discovery marked as completed");
|
||||
}
|
||||
|
||||
initDone = true;
|
||||
// requestStart = 1;
|
||||
emit connectedAndDiscovered();
|
||||
@@ -426,8 +441,8 @@ uint16_t toorxtreadmill::GetCaloriesFromPacket(const QByteArray &packet) {
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
uint16_t toorxtreadmill::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
uint16_t convertedData = (packet.at(9) << 8) | packet.at(10);
|
||||
double toorxtreadmill::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
double convertedData = (double)((packet.at(9) << 8) | packet.at(10)) / 100.0;
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class toorxtreadmill : public treadmill {
|
||||
bool MASTERT409 = false;
|
||||
|
||||
uint16_t GetElapsedTimeFromPacket(const QByteArray &packet);
|
||||
uint16_t GetDistanceFromPacket(const QByteArray &packet);
|
||||
double GetDistanceFromPacket(const QByteArray &packet);
|
||||
uint16_t GetCaloriesFromPacket(const QByteArray &packet);
|
||||
double GetSpeedFromPacket(const QByteArray &packet);
|
||||
uint8_t GetInclinationFromPacket(const QByteArray &packet);
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
treadmill::treadmill() {}
|
||||
|
||||
void treadmill::changeSpeed(double speed) {
|
||||
// Reset target watts only if called from external source
|
||||
if (!callingFromFollowPower) {
|
||||
targetWatts = -1;
|
||||
qDebug() << "External speed change - resetting power following mode";
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
bool stryd_speed_instead_treadmill = settings.value(QZSettings::stryd_speed_instead_treadmill, QZSettings::default_stryd_speed_instead_treadmill).toBool();
|
||||
m_lastRawSpeedRequested = speed;
|
||||
@@ -31,7 +37,7 @@ void treadmill::changeSpeed(double speed) {
|
||||
if (autoResistanceEnable)
|
||||
requestSpeed = (speed * m_difficult) + m_difficult_offset;
|
||||
}
|
||||
void treadmill::changeInclination(double grade, double inclination) {
|
||||
void treadmill::changeInclination(double grade, double inclination) {
|
||||
QSettings settings;
|
||||
double treadmill_incline_min = settings.value(QZSettings::treadmill_incline_min, QZSettings::default_treadmill_incline_min).toDouble();
|
||||
double treadmill_incline_max = settings.value(QZSettings::treadmill_incline_max, QZSettings::default_treadmill_incline_max).toDouble();
|
||||
@@ -587,25 +593,34 @@ bool treadmill::followPowerBySpeed() {
|
||||
if (treadmill_follow_wattage) {
|
||||
|
||||
if (currentInclination().value() != lastInclination && wattsMetric().value() != 0) {
|
||||
|
||||
// If not following power mode, calculate new target from current values
|
||||
if (targetWatts == -1) {
|
||||
targetWatts = wattsCalc(w, currentSpeed().value(), lastInclination);
|
||||
qDebug() << "Starting power following mode with target watts:" << targetWatts;
|
||||
}
|
||||
|
||||
// Find speed to maintain targetWatts with current inclination
|
||||
double newspeed = 0;
|
||||
double bestSpeed = 0.1;
|
||||
|
||||
// don't read the wattage directly from the m_watt because if you were using a power sensor, the power calcuated in the for will not match it
|
||||
double previousWatt = wattsCalc(w, currentSpeed().value(), lastInclination);
|
||||
|
||||
double bestDifference = fabs(wattsCalc(w, bestSpeed, currentInclination().value()) - previousWatt);
|
||||
double bestDifference = fabs(wattsCalc(w, bestSpeed, currentInclination().value()) - targetWatts);
|
||||
|
||||
for (int speed = 1; speed <= 300; speed++) {
|
||||
double s = ((double)speed) / 10.0;
|
||||
double thisDifference = fabs(wattsCalc(w, s, currentInclination().value()) - previousWatt);
|
||||
double thisDifference = fabs(wattsCalc(w, s, currentInclination().value()) - targetWatts);
|
||||
if (thisDifference < bestDifference) {
|
||||
bestDifference = thisDifference;
|
||||
bestSpeed = s;
|
||||
}
|
||||
}
|
||||
// Now bestSpeed is the speed closest to the desired wattage
|
||||
|
||||
newspeed = bestSpeed;
|
||||
qDebug() << QStringLiteral("changing speed to") << newspeed << "due to inclination changed" << currentInclination().value() << lastInclination;
|
||||
qDebug() << "Following power: changing speed to" << newspeed << "to maintain" << targetWatts << "watts (inclination changed" << currentInclination().value() << lastInclination << ")";
|
||||
|
||||
callingFromFollowPower = true; // Set flag before calling
|
||||
changeSpeedAndInclination(newspeed, currentInclination().value());
|
||||
callingFromFollowPower = false; // Reset flag after calling
|
||||
|
||||
r = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ class treadmill : public bluetoothdevice {
|
||||
double m_lastRawInclinationRequested = -100;
|
||||
bool instantaneousStrideLengthCMAvailableFromDevice = false;
|
||||
treadmillErgTable _ergTable;
|
||||
|
||||
// Power following logic
|
||||
bool callingFromFollowPower = false; // Flag to track if change comes from followPowerBySpeed
|
||||
double targetWatts = -1; // Target watts to maintain during power following
|
||||
|
||||
void parseSpeed(double speed);
|
||||
void parseInclination(double speed);
|
||||
|
||||
@@ -186,6 +186,10 @@ void trxappgateusbbike::update() {
|
||||
noOpData[4] = crc;
|
||||
pollCounter += 0x0c;
|
||||
writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true);
|
||||
} else if (bike_type == TYPE::TAURUA_IC90) {
|
||||
|
||||
const uint8_t noOpData[] = {0xf0, 0xa2, 0x01, 0x31, 0xc4};
|
||||
writeCharacteristic((uint8_t *)noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true);
|
||||
} else {
|
||||
|
||||
const uint8_t noOpData[] = {0xf0, 0xa2, 0x23, 0xd3, 0x88};
|
||||
@@ -431,6 +435,10 @@ double trxappgateusbbike::GetWattFromPacket(const QByteArray &packet) {
|
||||
|
||||
double trxappgateusbbike::GetCadenceFromPacket(const QByteArray &packet) {
|
||||
|
||||
QSettings settings;
|
||||
double cadence_gain = settings.value(QZSettings::cadence_gain, QZSettings::default_cadence_gain).toDouble();
|
||||
double cadence_offset = settings.value(QZSettings::cadence_offset, QZSettings::default_cadence_offset).toDouble();
|
||||
|
||||
uint16_t convertedData;
|
||||
if (bike_type != JLL_IC400 && bike_type != ASVIVA && bike_type != FYTTER_RI08 && bike_type != HAMMER_SPEED_BIKE_S) {
|
||||
convertedData = (packet.at(9) - 1) + ((packet.at(8) - 1) * 100);
|
||||
@@ -441,7 +449,7 @@ double trxappgateusbbike::GetCadenceFromPacket(const QByteArray &packet) {
|
||||
if (data < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data;
|
||||
return (data * cadence_gain) + cadence_offset ;
|
||||
}
|
||||
|
||||
double trxappgateusbbike::GetResistanceFromPacket(const QByteArray &packet) {
|
||||
@@ -813,6 +821,24 @@ void trxappgateusbbike::btinit(bool startTape) {
|
||||
QThread::msleep(400);
|
||||
writeCharacteristic((uint8_t *)initData8, sizeof(initData8), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(400);
|
||||
} else if (bike_type == TYPE::TAURUA_IC90) {
|
||||
const uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x00, 0x91};
|
||||
const uint8_t initData2[] = {0xf0, 0xa0, 0x01, 0x31, 0xc2};
|
||||
const uint8_t initData3[] = {0xf0, 0xa1, 0x01, 0x31, 0xc3};
|
||||
const uint8_t initData4[] = {0xf0, 0xa0, 0x01, 0x31, 0xc2};
|
||||
const uint8_t initData5[] = {0xf0, 0xa1, 0x01, 0x31, 0xc3};
|
||||
const uint8_t initData6[] = {0xf0, 0xa3, 0x01, 0x31, 0x01, 0xc6};
|
||||
const uint8_t initData7[] = {0xf0, 0xa4, 0x01, 0x31, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xd0};
|
||||
const uint8_t initData8[] = {0xf0, 0xa5, 0x01, 0x31, 0x02, 0xc9};
|
||||
|
||||
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);
|
||||
writeCharacteristic((uint8_t *)initData5, sizeof(initData5), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData6, sizeof(initData6), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData7, sizeof(initData7), QStringLiteral("init"), false, true);
|
||||
writeCharacteristic((uint8_t *)initData8, sizeof(initData8), QStringLiteral("init"), false, true);
|
||||
} else {
|
||||
|
||||
const uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x01, 0x92};
|
||||
@@ -1100,6 +1126,7 @@ void trxappgateusbbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
bool enerfit_SPX_9500 = settings.value(QZSettings::enerfit_SPX_9500, QZSettings::default_enerfit_SPX_9500).toBool();
|
||||
bool hop_sport_hs_090h_bike = settings.value(QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike).toBool();
|
||||
bool toorx_bike_srx_500 = settings.value(QZSettings::toorx_bike_srx_500, QZSettings::default_toorx_bike_srx_500).toBool();
|
||||
bool taurua_ic90 = settings.value(QZSettings::taurua_ic90, QZSettings::default_taurua_ic90).toBool();
|
||||
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
|
||||
device.address().toString() + ')');
|
||||
// if(device.name().startsWith("TOORX") || device.name().startsWith("V-RUN") || device.name().startsWith("FS-")
|
||||
@@ -1149,6 +1176,11 @@ void trxappgateusbbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
|
||||
bike_type = TYPE::TOORX_SRX_500;
|
||||
qDebug() << QStringLiteral("TOORX_SRX_500 bike found");
|
||||
} else if(taurua_ic90) {
|
||||
refresh->start(500ms);
|
||||
|
||||
bike_type = TYPE::TAURUA_IC90;
|
||||
qDebug() << QStringLiteral("TAURUA_IC90 bike found");
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("REEBOK"))) {
|
||||
bike_type = TYPE::REEBOK;
|
||||
qDebug() << QStringLiteral("REEBOK bike found");
|
||||
@@ -1276,28 +1308,7 @@ uint16_t trxappgateusbbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t trxappgateusbbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
void trxappgateusbbike::resistanceFromFTMSAccessory(resistance_t res) {
|
||||
|
||||
@@ -116,6 +116,7 @@ class trxappgateusbbike : public bike {
|
||||
PASYOU = 27,
|
||||
FAL_SPORTS = 28,
|
||||
HAMMER_SPEED_BIKE_S = 29,
|
||||
TAURUA_IC90 = 30,
|
||||
} TYPE;
|
||||
TYPE bike_type = TRXAPPGATE;
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ void trxappgateusbelliptical::writeCharacteristic(uint8_t *data, uint8_t data_le
|
||||
}
|
||||
|
||||
void trxappgateusbelliptical::forceResistance(resistance_t requestResistance) {
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
|
||||
uint8_t noOpData1[] = {0xf0, 0xa6, 0x01, 0x01, 0x03, 0x9b};
|
||||
noOpData1[4] = requestResistance + 1;
|
||||
noOpData1[5] = noOpData1[4] + 0x98;
|
||||
@@ -95,7 +95,7 @@ void trxappgateusbelliptical::update() {
|
||||
}
|
||||
requestResistance = -1;
|
||||
} else {
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
|
||||
uint8_t noOpData1[] = {0xf0, 0xa2, 0x01, 0x01, 0x94};
|
||||
writeCharacteristic(noOpData1, sizeof(noOpData1), QStringLiteral("noOp"));
|
||||
} else {
|
||||
@@ -139,22 +139,41 @@ void trxappgateusbelliptical::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
|
||||
double trxappgateusbelliptical::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = (packet.at(7) - 1) + ((packet.at(6) - 1) * 100);
|
||||
double data = (double)(convertedData) / 10.0f;
|
||||
return data;
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// JTX Fitness doesn't send speed via bluetooth, calculate from cadence using settings ratio
|
||||
QSettings settings;
|
||||
double cadence_speed_ratio = settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio).toDouble();
|
||||
double cadence = GetCadenceFromPacket(packet);
|
||||
return cadence * cadence_speed_ratio;
|
||||
} else {
|
||||
uint16_t convertedData = (packet.at(7) - 1) + ((packet.at(6) - 1) * 100);
|
||||
double data = (double)(convertedData) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
double trxappgateusbelliptical::GetCadenceFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = ((uint16_t)packet.at(9)) + ((uint16_t)packet.at(8) * 100);
|
||||
uint16_t convertedData;
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// JTX Fitness uses only byte 5 for cadence
|
||||
convertedData = packet.at(5);
|
||||
} else {
|
||||
convertedData = ((uint16_t)packet.at(9)) + ((uint16_t)packet.at(8) * 100);
|
||||
}
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
double trxappgateusbelliptical::GetWattFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = ((packet.at(16) - 1) * 100) + (packet.at(17) - 1);
|
||||
double data = ((double)(convertedData)) / 10.0f;
|
||||
return data;
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// JTX Fitness doesn't send watts via bluetooth, use classic elliptical calculation
|
||||
return 0; // Will be calculated in characteristicChanged using wattsFromResistance
|
||||
} else {
|
||||
uint16_t convertedData = ((packet.at(16) - 1) * 100) + (packet.at(17) - 1);
|
||||
double data = ((double)(convertedData)) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
|
||||
@@ -216,7 +235,7 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
|
||||
|
||||
void trxappgateusbelliptical::btinit() {
|
||||
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS || elliptical_type == TYPE::TAURUS_FX99) {
|
||||
uint8_t initData1[] = {0xf0, 0xa0, 0x01, 0x00, 0x91};
|
||||
uint8_t initData2[] = {0xf0, 0xa0, 0x01, 0x01, 0x92};
|
||||
uint8_t initData3[] = {0xf0, 0xa1, 0x01, 0x01, 0x93};
|
||||
@@ -277,11 +296,12 @@ void trxappgateusbelliptical::stateChanged(QLowEnergyService::ServiceState state
|
||||
QString uuidNotify1 = QStringLiteral("0000fff1-0000-1000-8000-00805f9b34fb");
|
||||
QString uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
|
||||
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
if (elliptical_type == TYPE::DCT2000I || elliptical_type == TYPE::JTX_FITNESS) {
|
||||
uuidWrite = QStringLiteral("49535343-8841-43f4-a8d4-ecbe34729bb3");
|
||||
uuidNotify1 = QStringLiteral("49535343-1E4D-4BD9-BA61-23C647249616");
|
||||
uuidNotify2 = QStringLiteral("49535343-4c8a-39b3-2f49-511cff073b7e");
|
||||
}
|
||||
// TAURUS_FX99 uses standard 0000fff0 characteristics
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId(uuidWrite);
|
||||
QBluetoothUuid _gattNotify1CharacteristicId(uuidNotify1);
|
||||
@@ -364,6 +384,41 @@ void trxappgateusbelliptical::serviceScanDone(void) {
|
||||
uuid = uuid2;
|
||||
}
|
||||
|
||||
// Fallback logic: try to find the service in discovered services
|
||||
bool found = false;
|
||||
foreach (QBluetoothUuid s, m_control->services()) {
|
||||
if (s == QBluetoothUuid::fromString(uuid)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If primary service not found, try fallback service
|
||||
if (!found) {
|
||||
if (elliptical_type == TYPE::DCT2000I) {
|
||||
// I-CONSOLE+ device but DCT2000I service not found, try 0000fff0 service (Taurus FX9.9)
|
||||
bool found_fff0 = false;
|
||||
foreach (QBluetoothUuid s, m_control->services()) {
|
||||
if (s == QBluetoothUuid::fromString(uuid3)) {
|
||||
found_fff0 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found_fff0) {
|
||||
uuid = uuid3;
|
||||
elliptical_type = TYPE::TAURUS_FX99;
|
||||
qDebug() << QStringLiteral("I-CONSOLE+ device detected as Taurus FX9.9 with 0000fff0 service");
|
||||
} else {
|
||||
qDebug() << QStringLiteral("DCT2000I service not found");
|
||||
}
|
||||
} else {
|
||||
// Try DCT2000I/JTX Fitness service as fallback
|
||||
uuid = uuid2;
|
||||
elliptical_type = TYPE::JTX_FITNESS;
|
||||
qDebug() << QStringLiteral("Standard service not found, trying JTX Fitness service as fallback");
|
||||
}
|
||||
}
|
||||
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId(uuid);
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
@@ -448,7 +503,13 @@ void trxappgateusbelliptical::controllerStateChanged(QLowEnergyController::Contr
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t trxappgateusbelliptical::watts() { return m_watt.value(); }
|
||||
uint16_t trxappgateusbelliptical::watts() {
|
||||
if (elliptical_type == TYPE::JTX_FITNESS) {
|
||||
// For JTX Fitness, always use the elliptical class generic calculation
|
||||
return elliptical::watts();
|
||||
}
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
|
||||
void trxappgateusbelliptical::searchingStop() { searchStopped = true; }
|
||||
|
||||
@@ -78,6 +78,8 @@ class trxappgateusbelliptical : public elliptical {
|
||||
typedef enum TYPE {
|
||||
ELLIPTICAL_GENERIC = 0,
|
||||
DCT2000I = 1,
|
||||
JTX_FITNESS = 2,
|
||||
TAURUS_FX99 = 3,
|
||||
} TYPE;
|
||||
TYPE elliptical_type = ELLIPTICAL_GENERIC;
|
||||
|
||||
|
||||
@@ -223,7 +223,6 @@ void wahookickrsnapbike::update() {
|
||||
if (!wahooWithoutWheelDiameter) {
|
||||
QByteArray d = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears()));
|
||||
uint8_t e[20];
|
||||
setGears(settings.value(QZSettings::gears_current_value, QZSettings::default_gears_current_value).toDouble());
|
||||
memcpy(e, d.constData(), d.length());
|
||||
writeCharacteristic(e, d.length(), "setWheelCircumference", false, true);
|
||||
}
|
||||
@@ -324,7 +323,7 @@ void wahookickrsnapbike::update() {
|
||||
} else if (lastGearValue != gears()) {
|
||||
inclinationChanged(lastGrade, lastGrade);
|
||||
}
|
||||
} else if (requestResistance != -1 && KICKR_BIKE == false) {
|
||||
} else if ((requestResistance != -1 || lastGearValue != gears()) && KICKR_BIKE == false) {
|
||||
if (requestResistance > 100) {
|
||||
requestResistance = 100;
|
||||
} else if (requestResistance == 0) {
|
||||
@@ -332,7 +331,7 @@ void wahookickrsnapbike::update() {
|
||||
}
|
||||
|
||||
auto virtualBike = this->VirtualBike();
|
||||
if (requestResistance != currentResistance().value() &&
|
||||
if (requestResistance != currentResistance().value() && requestResistance != -1 &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike)) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
lastForcedResistance = requestResistance;
|
||||
@@ -342,11 +341,14 @@ void wahookickrsnapbike::update() {
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
} else if (requestResistance != currentResistance().value() &&
|
||||
((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike) && lastGearValue != gears()) {
|
||||
emit debug(QStringLiteral("writing resistance due to gears changed ") + QString::number(lastForcedResistance));
|
||||
QByteArray a = setResistanceMode(((double)lastForcedResistance + (gears() - lastGearValue)) / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
emit debug(QStringLiteral("writing resistance due to gears changed ") + QString::number(lastForcedResistance));
|
||||
if(lastForcedResistance == -1)
|
||||
lastForcedResistance = 1;
|
||||
lastForcedResistance = ((double)lastForcedResistance + (gears() - lastGearValue));
|
||||
QByteArray a = setResistanceMode(lastForcedResistance / 100.0);
|
||||
uint8_t b[20];
|
||||
memcpy(b, a.constData(), a.length());
|
||||
writeCharacteristic(b, a.length(), "setResistance", false, false);
|
||||
} else if (virtualBike && virtualBike->ftmsDeviceConnected() && lastGearValue != gears()) {
|
||||
inclinationChanged(lastGrade, lastGrade);
|
||||
}
|
||||
@@ -1015,4 +1017,4 @@ double wahookickrsnapbike::minGears() {
|
||||
return bike::minGears(); // Use base class behavior
|
||||
}
|
||||
return 1; // Use gear minimum when wheel diameter mode is disabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ void ypooelliptical::forceInclination(double inclination) {
|
||||
|
||||
void ypooelliptical::forceResistance(resistance_t requestResistance) {
|
||||
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS) {
|
||||
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
|
||||
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
|
||||
writeCharacteristic(&gattFTMSWriteCharControlPointId, gattFTMSService, write, sizeof(write),
|
||||
@@ -108,7 +108,7 @@ void ypooelliptical::update() {
|
||||
|
||||
if (initRequest) {
|
||||
initRequest = false;
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(&gattFTMSWriteCharControlPointId, gattFTMSService, write, sizeof(write), "requestControl", false, true);
|
||||
} else {
|
||||
@@ -250,7 +250,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
|
||||
if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACE) && !iconsole_elliptical) {
|
||||
|
||||
if(E35 == false && SCH_590E == false && KETTLER == false && CARDIOPOWER_EEGO == false && MYELLIPTICAL == false && SKANDIKA == false && DOMYOS == false) {
|
||||
if(E35 == false && SCH_590E == false && KETTLER == false && CARDIOPOWER_EEGO == false && MYELLIPTICAL == false && SKANDIKA == false && DOMYOS == false && FEIER == false && MX_AS == false && FTMS == false) {
|
||||
if (newvalue.length() == 18) {
|
||||
qDebug() << QStringLiteral("let's wait for the next piece of frame");
|
||||
lastPacket = newvalue;
|
||||
@@ -270,7 +270,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
index += 3;
|
||||
|
||||
if (!Flags.moreData) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS) {
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS) {
|
||||
Speed = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index)))) /
|
||||
100.0;
|
||||
@@ -282,7 +282,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
// this particular device, seems to send the actual speed here
|
||||
if (Flags.avgSpeed) {
|
||||
// double avgSpeed;
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS) {
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS && !FEIER && !MX_AS && !FTMS) {
|
||||
Speed = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index)))) /
|
||||
100.0;
|
||||
@@ -292,7 +292,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
}
|
||||
|
||||
if (Flags.totDistance) {
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS) {
|
||||
if(!E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS && !FEIER && !MX_AS && !FTMS) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)lastPacket.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)lastPacket.at(index)))) /
|
||||
@@ -314,7 +314,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
double divisor = 1.0;
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS)
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)
|
||||
divisor = 2.0;
|
||||
Cadence = (((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index))))) / divisor;
|
||||
@@ -382,7 +382,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
double divisor = 100.0; // i added this because this device seems to send it multiplied by 100
|
||||
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS)
|
||||
if(E35 || SCH_590E || KETTLER || CARDIOPOWER_EEGO || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)
|
||||
divisor = 1.0;
|
||||
|
||||
m_watt = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
@@ -396,7 +396,7 @@ void ypooelliptical::characteristicChanged(const QLowEnergyCharacteristic &chara
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
}
|
||||
|
||||
if (Flags.avgPower && lastPacket.length() > index + 1 && !E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS) { // E35 has a bug about this
|
||||
if (Flags.avgPower && lastPacket.length() > index + 1 && !E35 && !SCH_590E && !KETTLER && !CARDIOPOWER_EEGO && !MYELLIPTICAL && !SKANDIKA && !DOMYOS && !FEIER && !MX_AS && !FTMS) { // E35 has a bug about this
|
||||
double avgPower;
|
||||
avgPower = ((double)(((uint16_t)((uint8_t)lastPacket.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)lastPacket.at(index))));
|
||||
@@ -541,7 +541,7 @@ void ypooelliptical::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
qDebug() << "skipping service" << s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
else if(s->serviceUuid() != _gattFTMSService && (SCH_590E || MYELLIPTICAL || SKANDIKA || DOMYOS)) {
|
||||
else if(s->serviceUuid() != _gattFTMSService && (SCH_590E || MYELLIPTICAL || SKANDIKA || DOMYOS || FEIER || MX_AS || FTMS)) {
|
||||
qDebug() << "skipping service" << s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
@@ -768,6 +768,19 @@ void ypooelliptical::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("DOMYOS-EL"))) {
|
||||
DOMYOS = true;
|
||||
qDebug() << "DOMYOS workaround ON!";
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("FEIER-EM-"))) {
|
||||
FEIER = true;
|
||||
qDebug() << "FEIER workaround ON!";
|
||||
} else if(device.name().toUpper().startsWith(QStringLiteral("MX-AS "))) {
|
||||
MX_AS = true;
|
||||
qDebug() << "MX_AS workaround ON!";
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
QString ftms_elliptical_setting = settings.value(QZSettings::ftms_elliptical, QZSettings::default_ftms_elliptical).toString();
|
||||
if(ftms_elliptical_setting != QStringLiteral("Disabled") && device.name().toUpper() == ftms_elliptical_setting.toUpper()) {
|
||||
FTMS = true;
|
||||
qDebug() << "FTMS Elliptical workaround ON!";
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
|
||||
@@ -83,6 +83,9 @@ class ypooelliptical : public elliptical {
|
||||
bool MYELLIPTICAL = false;
|
||||
bool SKANDIKA = false;
|
||||
bool DOMYOS = false;
|
||||
bool FEIER = false;
|
||||
bool MX_AS = false;
|
||||
bool FTMS = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
|
||||
@@ -184,6 +184,36 @@ class ergTable : public QObject {
|
||||
return sameResPoints.first().wattage;
|
||||
}
|
||||
|
||||
uint16_t resistanceFromPowerRequest(uint16_t power, uint16_t cadence, uint16_t maxResistance) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << cadence;
|
||||
|
||||
if (cadence == 0)
|
||||
return 1;
|
||||
|
||||
uint16_t best_resistance_match = 1;
|
||||
int min_watt_difference = 1000;
|
||||
|
||||
for (uint16_t i = 1; i < maxResistance; i++) {
|
||||
uint16_t current_watts = estimateWattage(cadence, i);
|
||||
uint16_t next_watts = estimateWattage(cadence, i + 1);
|
||||
|
||||
if (current_watts <= power && next_watts >= power) {
|
||||
qDebug() << current_watts << next_watts << power;
|
||||
return i;
|
||||
}
|
||||
|
||||
int diff = abs(current_watts - power);
|
||||
if (diff < min_watt_difference) {
|
||||
min_watt_difference = diff;
|
||||
best_resistance_match = i;
|
||||
qDebug() << QStringLiteral("best match") << best_resistance_match << "with watts" << current_watts << "diff" << diff;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Bracketing not found, best match:" << best_resistance_match;
|
||||
return best_resistance_match;
|
||||
}
|
||||
|
||||
QList<ergDataPoint> getConsolidatedData() const {
|
||||
return consolidatedData;
|
||||
}
|
||||
|
||||
27
src/fitbackupwriter.cpp
Normal file
27
src/fitbackupwriter.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "fitbackupwriter.h"
|
||||
#include <QDebug>
|
||||
|
||||
FitBackupWriter::FitBackupWriter(QObject *parent) : QObject(parent) {
|
||||
}
|
||||
|
||||
FitBackupWriter::~FitBackupWriter() {
|
||||
}
|
||||
|
||||
void FitBackupWriter::writeFitBackup(const QString &filename,
|
||||
const QList<SessionLine> &session,
|
||||
bluetoothdevice::BLUETOOTH_TYPE deviceType,
|
||||
uint32_t processType,
|
||||
FIT_SPORT workoutType,
|
||||
const QString &workoutName,
|
||||
const QString &deviceName) {
|
||||
qDebug() << QStringLiteral("Writing FIT backup file in background thread: ") << filename;
|
||||
|
||||
// Remove existing file
|
||||
QFile::remove(filename);
|
||||
|
||||
// Save FIT file using the same logic as the original backup() method
|
||||
qfit::save(filename, session, deviceType, processType, workoutType,
|
||||
workoutName, deviceName, "", "", "", "");
|
||||
|
||||
qDebug() << QStringLiteral("FIT backup file written successfully: ") << filename;
|
||||
}
|
||||
28
src/fitbackupwriter.h
Normal file
28
src/fitbackupwriter.h
Normal file
@@ -0,0 +1,28 @@
|
||||
// fitbackupwriter.h
|
||||
#ifndef FITBACKUPWRITER_H
|
||||
#define FITBACKUPWRITER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFile>
|
||||
#include "sessionline.h"
|
||||
#include "fit_profile.hpp"
|
||||
#include "qfit.h"
|
||||
#include "bluetoothdevice.h"
|
||||
|
||||
class FitBackupWriter : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FitBackupWriter(QObject *parent = nullptr);
|
||||
virtual ~FitBackupWriter();
|
||||
|
||||
public slots:
|
||||
void writeFitBackup(const QString &filename,
|
||||
const QList<SessionLine> &session,
|
||||
bluetoothdevice::BLUETOOTH_TYPE deviceType,
|
||||
uint32_t processType,
|
||||
FIT_SPORT workoutType,
|
||||
const QString &workoutName,
|
||||
const QString &deviceName);
|
||||
};
|
||||
|
||||
#endif // FITBACKUPWRITER_H
|
||||
386
src/fitdatabaseprocessor.cpp
Normal file
386
src/fitdatabaseprocessor.cpp
Normal file
@@ -0,0 +1,386 @@
|
||||
#include "fitdatabaseprocessor.h"
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QSqlDatabase>
|
||||
#include <QDateTime>
|
||||
|
||||
const QString FitDatabaseProcessor::DB_CONNECTION_NAME = "FitProcessor";
|
||||
|
||||
FitDatabaseProcessor::FitDatabaseProcessor(const QString& dbPath, QObject* parent)
|
||||
: QObject(parent)
|
||||
, dbPath(dbPath)
|
||||
, stopRequested(0)
|
||||
{
|
||||
moveToThread(&workerThread);
|
||||
connect(&workerThread, &QThread::finished, this, &QObject::deleteLater);
|
||||
}
|
||||
|
||||
FitDatabaseProcessor::~FitDatabaseProcessor() {
|
||||
stopProcessing();
|
||||
workerThread.wait();
|
||||
|
||||
QSqlDatabase::removeDatabase(DB_CONNECTION_NAME);
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::initializeDatabase() {
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
if (QSqlDatabase::contains(DB_CONNECTION_NAME)) {
|
||||
db = QSqlDatabase::database(DB_CONNECTION_NAME);
|
||||
} else {
|
||||
db = QSqlDatabase::addDatabase("QSQLITE", DB_CONNECTION_NAME);
|
||||
db.setDatabaseName(dbPath);
|
||||
}
|
||||
|
||||
if (!db.open()) {
|
||||
emit error("Failed to open database: " + db.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start transaction for table creation
|
||||
db.transaction();
|
||||
|
||||
QSqlQuery query(db);
|
||||
|
||||
// Create workouts table - Only storing summary data
|
||||
if (!query.exec("CREATE TABLE IF NOT EXISTS workouts ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"file_hash TEXT UNIQUE,"
|
||||
"file_path TEXT,"
|
||||
"workout_name TEXT,"
|
||||
"sport_type INTEGER,"
|
||||
"start_time DATETIME,"
|
||||
"end_time DATETIME,"
|
||||
"total_time INTEGER," // in seconds
|
||||
"total_distance REAL," // in km
|
||||
"total_calories INTEGER,"
|
||||
"avg_heart_rate INTEGER,"
|
||||
"max_heart_rate INTEGER,"
|
||||
"avg_cadence INTEGER,"
|
||||
"max_cadence INTEGER,"
|
||||
"avg_speed REAL,"
|
||||
"max_speed REAL,"
|
||||
"avg_power INTEGER,"
|
||||
"max_power INTEGER,"
|
||||
"total_ascent REAL,"
|
||||
"total_descent REAL,"
|
||||
"avg_stride_length REAL,"
|
||||
"total_strides INTEGER,"
|
||||
"workout_source TEXT DEFAULT 'QZ',"
|
||||
"peloton_workout_id TEXT,"
|
||||
"peloton_url TEXT,"
|
||||
"training_program_file TEXT,"
|
||||
"processed_at DATETIME DEFAULT CURRENT_TIMESTAMP"
|
||||
")")) {
|
||||
db.rollback();
|
||||
emit error("Failed to create workouts table: " + query.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create index for better performance
|
||||
query.exec("CREATE INDEX IF NOT EXISTS idx_workout_start_time ON workouts(start_time)");
|
||||
|
||||
// Add workout_name column if it doesn't exist (for existing databases)
|
||||
query.exec("ALTER TABLE workouts ADD COLUMN workout_name TEXT");
|
||||
|
||||
// Add new Peloton-related columns if they don't exist (for existing databases)
|
||||
query.exec("ALTER TABLE workouts ADD COLUMN workout_source TEXT DEFAULT 'QZ'");
|
||||
query.exec("ALTER TABLE workouts ADD COLUMN peloton_workout_id TEXT");
|
||||
query.exec("ALTER TABLE workouts ADD COLUMN peloton_url TEXT");
|
||||
query.exec("ALTER TABLE workouts ADD COLUMN training_program_file TEXT");
|
||||
|
||||
return db.commit();
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::processDirectory(const QString& dirPath) {
|
||||
currentDirPath = dirPath;
|
||||
stopRequested.storeRelease(0);
|
||||
|
||||
if (!workerThread.isRunning()) {
|
||||
connect(&workerThread, &QThread::started, this, &FitDatabaseProcessor::doWork);
|
||||
workerThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::processFile(const QString& filePath) {
|
||||
if (!db.isOpen()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!processFitFile(filePath)) {
|
||||
emit error(QString("Failed to process file: %1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
emit fileProcessed(filePath);
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::stopProcessing() {
|
||||
stopRequested.storeRelease(1);
|
||||
workerThread.quit();
|
||||
}
|
||||
|
||||
QString FitDatabaseProcessor::getFileHash(const QString& filePath) {
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
if (!hash.addData(&file)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::isFileProcessed(const QString& filePath) {
|
||||
QString fileHash = getFileHash(filePath);
|
||||
if (fileHash.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.prepare("SELECT COUNT(*) FROM workouts WHERE file_hash = ?");
|
||||
query.addBindValue(fileHash);
|
||||
|
||||
if (!query.exec() || !query.next()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return query.value(0).toInt() > 0;
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::saveWorkout(const QString& filePath,
|
||||
const QList<SessionLine>& session,
|
||||
FIT_SPORT sport,
|
||||
const QString& workoutName,
|
||||
int elapsedSeconds,
|
||||
qint64& workoutId,
|
||||
const QString& workoutSource,
|
||||
const QString& pelotonWorkoutId,
|
||||
const QString& pelotonUrl,
|
||||
const QString& trainingProgramFile) {
|
||||
if (session.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString fileHash = getFileHash(filePath);
|
||||
if (fileHash.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate aggregate values
|
||||
double totalDistance = session.last().distance - session.first().distance;
|
||||
int maxHr = 0, totalHr = 0, hrCount = 0;
|
||||
int maxCadence = 0, totalCadence = 0, cadenceCount = 0;
|
||||
double maxSpeed = 0, totalSpeed = 0, speedCount = 0;
|
||||
int maxPower = 0, totalPower = 0, powerCount = 0;
|
||||
double totalAscent = 0, totalDescent = 0;
|
||||
double lastElevation = session.first().coordinate.altitude();
|
||||
|
||||
for (const SessionLine& point : session) {
|
||||
// Heart rate
|
||||
if (point.heart > 0) {
|
||||
maxHr = qMax(maxHr, static_cast<int>(point.heart));
|
||||
totalHr += point.heart;
|
||||
hrCount++;
|
||||
}
|
||||
|
||||
// Cadence
|
||||
if (point.cadence > 0) {
|
||||
maxCadence = qMax(maxCadence, static_cast<int>(point.cadence));
|
||||
totalCadence += point.cadence;
|
||||
cadenceCount++;
|
||||
}
|
||||
|
||||
// Speed
|
||||
if (point.speed > 0) {
|
||||
maxSpeed = qMax(maxSpeed, point.speed);
|
||||
totalSpeed += point.speed;
|
||||
speedCount++;
|
||||
}
|
||||
|
||||
// Power
|
||||
if (point.watt > 0) {
|
||||
maxPower = qMax(maxPower, static_cast<int>(point.watt));
|
||||
totalPower += point.watt;
|
||||
powerCount++;
|
||||
}
|
||||
|
||||
// Elevation changes
|
||||
if (point.coordinate.isValid()) {
|
||||
double currentElevation = point.coordinate.altitude();
|
||||
if (lastElevation > 0) {
|
||||
double diff = currentElevation - lastElevation;
|
||||
if (diff > 0) totalAscent += diff;
|
||||
else totalDescent += qAbs(diff);
|
||||
}
|
||||
lastElevation = currentElevation;
|
||||
}
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.prepare("INSERT INTO workouts ("
|
||||
"file_hash, file_path, workout_name, sport_type, start_time, end_time, "
|
||||
"total_time, total_distance, total_calories, "
|
||||
"avg_heart_rate, max_heart_rate, avg_cadence, max_cadence, "
|
||||
"avg_speed, max_speed, avg_power, max_power, "
|
||||
"total_ascent, total_descent, avg_stride_length, total_strides, "
|
||||
"workout_source, peloton_workout_id, peloton_url, training_program_file"
|
||||
") VALUES ("
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?"
|
||||
")");
|
||||
|
||||
query.addBindValue(fileHash);
|
||||
query.addBindValue(filePath);
|
||||
query.addBindValue(workoutName);
|
||||
query.addBindValue(static_cast<int>(sport));
|
||||
query.addBindValue(session.first().time);
|
||||
query.addBindValue(session.last().time);
|
||||
query.addBindValue(elapsedSeconds);
|
||||
query.addBindValue(totalDistance);
|
||||
query.addBindValue(session.last().calories);
|
||||
query.addBindValue(hrCount > 0 ? totalHr / hrCount : 0);
|
||||
query.addBindValue(maxHr);
|
||||
query.addBindValue(cadenceCount > 0 ? totalCadence / cadenceCount : 0);
|
||||
query.addBindValue(maxCadence);
|
||||
query.addBindValue(speedCount > 0 ? totalSpeed / speedCount : 0);
|
||||
query.addBindValue(maxSpeed);
|
||||
query.addBindValue(powerCount > 0 ? totalPower / powerCount : 0);
|
||||
query.addBindValue(maxPower);
|
||||
query.addBindValue(totalAscent);
|
||||
query.addBindValue(totalDescent);
|
||||
query.addBindValue(session.last().instantaneousStrideLengthCM);
|
||||
query.addBindValue(session.last().stepCount);
|
||||
query.addBindValue(workoutSource);
|
||||
query.addBindValue(pelotonWorkoutId.isEmpty() ? QVariant() : pelotonWorkoutId);
|
||||
query.addBindValue(pelotonUrl.isEmpty() ? QVariant() : pelotonUrl);
|
||||
query.addBindValue(trainingProgramFile.isEmpty() ? QVariant() : trainingProgramFile);
|
||||
|
||||
if (!query.exec()) {
|
||||
emit error("Failed to save workout: " + query.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
workoutId = query.lastInsertId().toLongLong();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FitDatabaseProcessor::processFitFile(const QString& filePath) {
|
||||
if (isFileProcessed(filePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<SessionLine> session;
|
||||
FIT_SPORT sport = FIT_SPORT_INVALID;
|
||||
QString workoutName = ""; // Initialize to empty string
|
||||
QString workoutSource = "";
|
||||
QString pelotonWorkoutId = "";
|
||||
QString pelotonUrl = "";
|
||||
QString trainingProgramFile = "";
|
||||
|
||||
try {
|
||||
qfit::open(filePath, &session, &sport, &workoutName, &workoutSource, &pelotonWorkoutId, &pelotonUrl, &trainingProgramFile);
|
||||
|
||||
if (session.isEmpty()) {
|
||||
emit error("No data found in file: " + filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
qDebug() << "Processing FIT file:" << filePath;
|
||||
qDebug() << "Sport type detected:" << static_cast<int>(sport);
|
||||
qDebug() << "Session duration (elapsedTime):" << session.last().elapsedTime;
|
||||
qDebug() << "Workout name from FIT:" << workoutName;
|
||||
|
||||
// Validate elapsed time (should be reasonable, between 1 minute and 24 hours)
|
||||
int elapsedSeconds = session.last().elapsedTime;
|
||||
if (elapsedSeconds < 60 || elapsedSeconds > 86400) {
|
||||
qDebug() << "Warning: Unusual elapsed time detected:" << elapsedSeconds << "seconds. Using session duration calculation.";
|
||||
// Calculate duration from first to last record
|
||||
elapsedSeconds = session.first().time.secsTo(session.last().time);
|
||||
if (elapsedSeconds < 60 || elapsedSeconds > 86400) {
|
||||
qDebug() << "Warning: Still unusual duration. Setting to 1 minute minimum.";
|
||||
elapsedSeconds = qMax(60, qMin(86400, elapsedSeconds));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fallback workout name based on sport and duration if not found in FIT file
|
||||
if (workoutName.isEmpty()) {
|
||||
QString sportName;
|
||||
switch (sport) {
|
||||
case FIT_SPORT_RUNNING:
|
||||
case FIT_SPORT_WALKING:
|
||||
sportName = "Run";
|
||||
break;
|
||||
case FIT_SPORT_CYCLING:
|
||||
sportName = "Ride";
|
||||
break;
|
||||
case FIT_SPORT_FITNESS_EQUIPMENT:
|
||||
sportName = "Elliptical";
|
||||
break;
|
||||
case FIT_SPORT_ROWING:
|
||||
sportName = "Row";
|
||||
break;
|
||||
default:
|
||||
sportName = "Workout";
|
||||
qDebug() << "Unknown sport type, using default. Sport value:" << static_cast<int>(sport);
|
||||
break;
|
||||
}
|
||||
|
||||
int totalMinutes = elapsedSeconds / 60;
|
||||
workoutName = QString("%1 minutes %2").arg(totalMinutes).arg(sportName);
|
||||
qDebug() << "Generated fallback workout name:" << workoutName;
|
||||
}
|
||||
|
||||
db.transaction();
|
||||
|
||||
qint64 workoutId;
|
||||
if (!saveWorkout(filePath, session, sport, workoutName, elapsedSeconds, workoutId, workoutSource, pelotonWorkoutId, pelotonUrl, trainingProgramFile)) {
|
||||
db.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
return db.commit();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
emit error(QString("Error processing file %1: %2").arg(filePath, e.what()));
|
||||
db.rollback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FitDatabaseProcessor::doWork() {
|
||||
if (!initializeDatabase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDir dir(currentDirPath);
|
||||
QStringList fitFiles = dir.entryList(QStringList() << "*.fit" << "*.FIT", QDir::Files);
|
||||
int totalFiles = fitFiles.size();
|
||||
int processedFiles = 0;
|
||||
|
||||
for (const QString& fileName : fitFiles) {
|
||||
if (stopRequested.loadAcquire()) {
|
||||
break;
|
||||
}
|
||||
|
||||
QString filePath = dir.absoluteFilePath(fileName);
|
||||
|
||||
if (processFitFile(filePath)) {
|
||||
emit fileProcessed(fileName);
|
||||
}
|
||||
|
||||
processedFiles++;
|
||||
emit progress(processedFiles, totalFiles);
|
||||
}
|
||||
|
||||
emit processingStopped();
|
||||
}
|
||||
61
src/fitdatabaseprocessor.h
Normal file
61
src/fitdatabaseprocessor.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#ifndef FITDATABASEPROCESSOR_H
|
||||
#define FITDATABASEPROCESSOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QSqlDatabase>
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
#include <QMutex>
|
||||
#include <QAtomicInt>
|
||||
#include "qfit.h"
|
||||
|
||||
class FitDatabaseProcessor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FitDatabaseProcessor(const QString& dbPath, QObject* parent = nullptr);
|
||||
~FitDatabaseProcessor();
|
||||
|
||||
void processDirectory(const QString& dirPath);
|
||||
void processFile(const QString& filePath);
|
||||
void stopProcessing();
|
||||
|
||||
static const QString DB_CONNECTION_NAME;
|
||||
|
||||
signals:
|
||||
void processingStopped();
|
||||
void fileProcessed(const QString& filename);
|
||||
void progress(int processedFiles, int totalFiles);
|
||||
void error(const QString& errorMessage);
|
||||
|
||||
private slots:
|
||||
void doWork();
|
||||
|
||||
private:
|
||||
bool initializeDatabase();
|
||||
bool processFitFile(const QString& filePath);
|
||||
bool isFileProcessed(const QString& filePath);
|
||||
QString getFileHash(const QString& filePath);
|
||||
|
||||
// Method for handling workout summary data
|
||||
bool saveWorkout(const QString& filePath,
|
||||
const QList<SessionLine>& session,
|
||||
FIT_SPORT sport,
|
||||
const QString& workoutName,
|
||||
int elapsedSeconds,
|
||||
qint64& workoutId,
|
||||
const QString& workoutSource = "QZ",
|
||||
const QString& pelotonWorkoutId = "",
|
||||
const QString& pelotonUrl = "",
|
||||
const QString& trainingProgramFile = "");
|
||||
|
||||
QThread workerThread;
|
||||
QString dbPath;
|
||||
QString currentDirPath;
|
||||
QAtomicInt stopRequested;
|
||||
QMutex mutex;
|
||||
QSqlDatabase db;
|
||||
};
|
||||
|
||||
#endif // FITDATABASEPROCESSOR_H
|
||||
190
src/fontmanager.cpp
Normal file
190
src/fontmanager.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "fontmanager.h"
|
||||
#include <QDebug>
|
||||
#include <QCoreApplication>
|
||||
|
||||
const QString FontManager::EMOJI_FONT_URL = "https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf";
|
||||
const QString FontManager::EMOJI_FONT_FILENAME = "NotoColorEmoji.ttf";
|
||||
|
||||
FontManager::FontManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
, m_emojiFontReady(false)
|
||||
, m_emojiFontFamily("Arial") // fallback
|
||||
{
|
||||
}
|
||||
|
||||
void FontManager::initializeEmojiFont()
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
QString cacheFile = getCacheFilePath();
|
||||
QFile file(cacheFile);
|
||||
|
||||
if (file.exists()) {
|
||||
// Font già in cache, caricalo
|
||||
loadLocalEmojiFont();
|
||||
} else {
|
||||
// Scarica il font
|
||||
downloadEmojiFont();
|
||||
}
|
||||
#else
|
||||
// Su desktop/iOS usa il font locale
|
||||
QString localFontPath = QCoreApplication::applicationDirPath() + "/fonts/NotoColorEmoji_WindowsCompatible.ttf";
|
||||
QFile file(localFontPath);
|
||||
|
||||
if (file.exists()) {
|
||||
int fontId = QFontDatabase::addApplicationFont(localFontPath);
|
||||
if (fontId != -1) {
|
||||
QStringList fontFamilies = QFontDatabase::applicationFontFamilies(fontId);
|
||||
if (!fontFamilies.isEmpty()) {
|
||||
m_emojiFontFamily = fontFamilies.first();
|
||||
m_emojiFontReady = true;
|
||||
emit emojiFontReadyChanged();
|
||||
emit emojiFontFamilyChanged();
|
||||
qDebug() << "Local emoji font loaded:" << m_emojiFontFamily;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "Failed to load local emoji font, using fallback";
|
||||
m_emojiFontReady = true; // Use fallback
|
||||
emit emojiFontReadyChanged();
|
||||
#endif
|
||||
}
|
||||
|
||||
void FontManager::downloadEmojiFont()
|
||||
{
|
||||
qDebug() << "Downloading emoji font from:" << EMOJI_FONT_URL;
|
||||
|
||||
QNetworkRequest request(EMOJI_FONT_URL);
|
||||
// Headers per simulare un browser
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader,
|
||||
"Mozilla/5.0 (Linux; Android 10; SM-G960F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36");
|
||||
request.setRawHeader("Accept", "*/*");
|
||||
request.setRawHeader("Accept-Language", "en-US,en;q=0.9");
|
||||
request.setRawHeader("Accept-Encoding", "identity"); // No compression
|
||||
request.setRawHeader("Connection", "keep-alive");
|
||||
request.setRawHeader("Cache-Control", "no-cache");
|
||||
|
||||
// Abilita redirect automatici
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, &FontManager::onFontDownloadFinished);
|
||||
}
|
||||
|
||||
void FontManager::onFontDownloadFinished()
|
||||
{
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
|
||||
if (!reply) return;
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QString cacheFile = getCacheFilePath();
|
||||
QFile file(cacheFile);
|
||||
|
||||
// Crea directory se non esiste
|
||||
QDir().mkpath(QFileInfo(cacheFile).dir().absolutePath());
|
||||
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
QByteArray fontData = reply->readAll();
|
||||
|
||||
qDebug() << "Downloaded data size:" << fontData.size() << "bytes";
|
||||
qDebug() << "First 100 chars:" << fontData.left(100);
|
||||
qDebug() << "First 16 bytes hex:" << fontData.left(16).toHex();
|
||||
|
||||
// Validate TTF file (starts with 0x00010000 or "OTTO")
|
||||
if (fontData.size() > 4 &&
|
||||
(fontData.startsWith(QByteArray::fromHex("00010000")) || fontData.startsWith("OTTO"))) {
|
||||
file.write(fontData);
|
||||
file.close();
|
||||
|
||||
qDebug() << "Emoji font downloaded and cached to:" << cacheFile;
|
||||
loadLocalEmojiFont();
|
||||
} else {
|
||||
file.close();
|
||||
// Cancella il file invalido per forzare ri-download
|
||||
if (file.exists()) {
|
||||
file.remove();
|
||||
qDebug() << "Removed invalid font file:" << cacheFile;
|
||||
}
|
||||
qWarning() << "Downloaded file is not a valid TTF font. Size:" << fontData.size();
|
||||
if (fontData.size() > 0) {
|
||||
qWarning() << "Content starts with:" << fontData.left(50);
|
||||
}
|
||||
m_emojiFontReady = true; // Use fallback
|
||||
emit emojiFontReadyChanged();
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Failed to save font to cache:" << file.errorString();
|
||||
m_emojiFontReady = true; // Use fallback
|
||||
emit emojiFontReadyChanged();
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Font download failed:" << reply->errorString();
|
||||
m_emojiFontReady = true; // Use fallback
|
||||
emit emojiFontReadyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void FontManager::loadLocalEmojiFont()
|
||||
{
|
||||
QString fontPath = getCacheFilePath();
|
||||
|
||||
// Debug info sul file
|
||||
QFileInfo fileInfo(fontPath);
|
||||
qDebug() << "Font file path:" << fontPath;
|
||||
qDebug() << "Font file exists:" << fileInfo.exists();
|
||||
qDebug() << "Font file size:" << fileInfo.size() << "bytes";
|
||||
qDebug() << "Font file readable:" << fileInfo.isReadable();
|
||||
|
||||
if (!fileInfo.exists() || fileInfo.size() == 0) {
|
||||
qWarning() << "Font file is missing or empty";
|
||||
m_emojiFontReady = true;
|
||||
emit emojiFontReadyChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verifica i primi bytes del file
|
||||
QFile file(fontPath);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
QByteArray header = file.read(16);
|
||||
qDebug() << "Font file header (hex):" << header.toHex();
|
||||
qDebug() << "Font file header (text):" << header;
|
||||
file.close();
|
||||
}
|
||||
|
||||
int fontId = QFontDatabase::addApplicationFont(fontPath);
|
||||
qDebug() << "QFontDatabase::addApplicationFont returned:" << fontId;
|
||||
|
||||
if (fontId != -1) {
|
||||
QStringList fontFamilies = QFontDatabase::applicationFontFamilies(fontId);
|
||||
qDebug() << "Available font families:" << fontFamilies;
|
||||
if (!fontFamilies.isEmpty()) {
|
||||
m_emojiFontFamily = fontFamilies.first();
|
||||
m_emojiFontReady = true;
|
||||
emit emojiFontReadyChanged();
|
||||
emit emojiFontFamilyChanged();
|
||||
qDebug() << "Cached emoji font loaded:" << m_emojiFontFamily;
|
||||
return;
|
||||
} else {
|
||||
qWarning() << "Font loaded but no families found";
|
||||
}
|
||||
} else {
|
||||
qWarning() << "QFontDatabase::addApplicationFont failed";
|
||||
// Cancella il file corrotto
|
||||
QFile::remove(fontPath);
|
||||
qDebug() << "Removed corrupted font file:" << fontPath;
|
||||
}
|
||||
|
||||
qWarning() << "Failed to load cached emoji font";
|
||||
m_emojiFontReady = true; // Use fallback
|
||||
emit emojiFontReadyChanged();
|
||||
}
|
||||
|
||||
QString FontManager::getCacheFilePath() const
|
||||
{
|
||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
return cacheDir + "/fonts/" + EMOJI_FONT_FILENAME;
|
||||
}
|
||||
46
src/fontmanager.h
Normal file
46
src/fontmanager.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef FONTMANAGER_H
|
||||
#define FONTMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QFontDatabase>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
class FontManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool emojiFontReady READ isEmojiFontReady NOTIFY emojiFontReadyChanged)
|
||||
Q_PROPERTY(QString emojiFontFamily READ emojiFontFamily NOTIFY emojiFontFamilyChanged)
|
||||
|
||||
public:
|
||||
explicit FontManager(QObject *parent = nullptr);
|
||||
|
||||
bool isEmojiFontReady() const { return m_emojiFontReady; }
|
||||
QString emojiFontFamily() const { return m_emojiFontFamily; }
|
||||
|
||||
Q_INVOKABLE void initializeEmojiFont();
|
||||
|
||||
signals:
|
||||
void emojiFontReadyChanged();
|
||||
void emojiFontFamilyChanged();
|
||||
|
||||
private slots:
|
||||
void onFontDownloadFinished();
|
||||
|
||||
private:
|
||||
void loadLocalEmojiFont();
|
||||
void downloadEmojiFont();
|
||||
QString getCacheFilePath() const;
|
||||
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
bool m_emojiFontReady;
|
||||
QString m_emojiFontFamily;
|
||||
|
||||
static const QString EMOJI_FONT_URL;
|
||||
static const QString EMOJI_FONT_FILENAME;
|
||||
};
|
||||
|
||||
#endif // FONTMANAGER_H
|
||||
BIN
src/fonts/NotoColorEmoji_WindowsCompatible.ttf
Normal file
BIN
src/fonts/NotoColorEmoji_WindowsCompatible.ttf
Normal file
Binary file not shown.
645
src/homeform.cpp
645
src/homeform.cpp
@@ -8,10 +8,12 @@
|
||||
#include <jni.h>
|
||||
#include <QAndroidJniObject>
|
||||
#endif
|
||||
#include "fitdatabaseprocessor.h"
|
||||
#include "material.h"
|
||||
#include "qfit.h"
|
||||
#include "simplecrypt.h"
|
||||
#include "templateinfosenderbuilder.h"
|
||||
#include "workoutmodel.h"
|
||||
#include "zwiftworkout.h"
|
||||
|
||||
#include <QAbstractOAuth2>
|
||||
@@ -213,7 +215,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
target_power = new DataObject(QStringLiteral("T.Power(W)"), QStringLiteral("icons/icons/watt.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("target_power"), 48, labelFontSize);
|
||||
target_zone = new DataObject(QStringLiteral("T.Zone"), QStringLiteral("icons/icons/watt.png"), QStringLiteral("1"),
|
||||
false, QStringLiteral("target_zone"), 48, labelFontSize);
|
||||
true, QStringLiteral("target_zone"), 48, labelFontSize);
|
||||
target_speed = new DataObject(QStringLiteral("T.Speed (") + unit + QStringLiteral("/h)"),
|
||||
QStringLiteral("icons/icons/speed.png"), QStringLiteral("0.0"), true,
|
||||
QStringLiteral("target_speed"), 48, labelFontSize);
|
||||
@@ -284,6 +286,12 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
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"));
|
||||
autoVirtualShiftingCruise = new DataObject(QStringLiteral("Cruise"), QStringLiteral("icons/icons/speed.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("autoVirtualShiftingCruise"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Cruise", QStringLiteral("red"));
|
||||
autoVirtualShiftingClimb = new DataObject(QStringLiteral("Climb"), QStringLiteral("icons/icons/inclination.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("autoVirtualShiftingClimb"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Climb", QStringLiteral("red"));
|
||||
autoVirtualShiftingSprint = new DataObject(QStringLiteral("Sprint"), QStringLiteral("icons/icons/watt.png"),
|
||||
QStringLiteral("0"), true, QStringLiteral("autoVirtualShiftingSprint"), 48, labelFontSize, QStringLiteral("white"), QLatin1String(""), 0, true, "Sprint", QStringLiteral("red"));
|
||||
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"),
|
||||
@@ -562,6 +570,19 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
connect(backupTimer, &QTimer::timeout, this, &homeform::backup);
|
||||
backupTimer->start(1min);
|
||||
|
||||
automaticShiftingTimer = new QTimer(this);
|
||||
connect(automaticShiftingTimer, &QTimer::timeout, this, &homeform::ten_hz);
|
||||
|
||||
if (settings.value(QZSettings::automatic_virtual_shifting_enabled, QZSettings::default_automatic_virtual_shifting_enabled).toBool()) {
|
||||
automaticShiftingTimer->start(100); // 100ms = 10Hz
|
||||
}
|
||||
|
||||
// Initialize FIT backup thread
|
||||
fitBackupThread = new QThread(this);
|
||||
fitBackupWriter = new FitBackupWriter();
|
||||
fitBackupWriter->moveToThread(fitBackupThread);
|
||||
fitBackupThread->start();
|
||||
|
||||
QObject *rootObject = engine->rootObjects().constFirst();
|
||||
QObject *home = rootObject->findChild<QObject *>(QStringLiteral("home"));
|
||||
QObject *stack = rootObject;
|
||||
@@ -574,6 +595,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
QObject::connect(stack, SIGNAL(profile_open_clicked(QUrl)), this, SLOT(profile_open_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(trainprogram_preview(QUrl)), this, SLOT(trainprogram_preview(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(gpxpreview_open_clicked(QUrl)), this, SLOT(gpxpreview_open_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(fitfile_preview_clicked(QUrl)), this, SLOT(fitfile_preview_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(trainprogram_zwo_loaded(QString)), this, SLOT(trainprogram_zwo_loaded(QString)));
|
||||
QObject::connect(stack, SIGNAL(gpx_open_clicked(QUrl)), this, SLOT(gpx_open_clicked(QUrl)));
|
||||
QObject::connect(stack, SIGNAL(gpx_save_clicked()), this, SLOT(gpx_save_clicked()));
|
||||
@@ -642,7 +664,20 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
if (!QFile(getWritableAppDir() + "gpx/" + itGpx.fileName()).exists()) {
|
||||
QFile::copy(":/gpx/" + itGpx.fileName(), getWritableAppDir() + "gpx/" + itGpx.fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QDirIterator itFit(getWritableAppDir(), QStringList() << "*.fit", QDir::Files);
|
||||
qDebug() << itFit.path();
|
||||
QDir().mkdir(getWritableAppDir() + "fit");
|
||||
while (itFit.hasNext()) {
|
||||
qDebug() << itFit.filePath() << itFit.fileName() << itFit.filePath().replace(itFit.path(), "");
|
||||
if (!QFile(getWritableAppDir() + "fit/" + itFit.next().replace(itFit.path(), "")).exists() && !itFit.fileName().contains("backup")) {
|
||||
if(QFile::copy(itFit.filePath(), getWritableAppDir() + "fit/" + itFit.filePath().replace(itFit.path(), "")))
|
||||
QFile::remove(itFit.filePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
QString bluetoothName = getBluetoothName();
|
||||
@@ -669,6 +704,30 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
}
|
||||
#endif
|
||||
|
||||
fitProcessor = new FitDatabaseProcessor(getWritableAppDir() + "ddb.sqlite");
|
||||
connect(fitProcessor, &FitDatabaseProcessor::fileProcessed,
|
||||
this, [](const QString& filename) {
|
||||
qDebug() << "FitDatabaseProcessor Processing:" << filename;
|
||||
});
|
||||
connect(fitProcessor, &FitDatabaseProcessor::progress,
|
||||
this, [](int processed, int total) {
|
||||
qDebug() << "FitDatabaseProcessor Progress:" << processed << "/" << total;
|
||||
});
|
||||
connect(fitProcessor, &FitDatabaseProcessor::error,
|
||||
this, [](const QString& error) {
|
||||
qDebug() << "FitDatabaseProcessor Error:" << error;
|
||||
});
|
||||
workoutModel = new WorkoutModel(getWritableAppDir() + "ddb.sqlite");
|
||||
engine->rootContext()->setContextProperty("workoutModel", workoutModel);
|
||||
|
||||
connect(fitProcessor, &FitDatabaseProcessor::processingStopped,
|
||||
this, [this]() {
|
||||
qDebug() << "FitDatabaseProcessor Processing stopped - refreshing workout model";
|
||||
workoutModel->setDatabaseProcessing(false);
|
||||
workoutModel->refresh();
|
||||
});
|
||||
fitProcessor->processDirectory(getWritableAppDir() + "fit");
|
||||
|
||||
m_speech.setLocale(QLocale::English);
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
@@ -762,6 +821,11 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
|
||||
}
|
||||
});
|
||||
});
|
||||
#else
|
||||
#ifndef IO_UNDER_QT
|
||||
h = new lockscreen();
|
||||
h->appleWatchAppInstalled();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (QSslSocket::supportsSsl()) {
|
||||
@@ -1057,10 +1121,18 @@ void homeform::pelotonWorkoutStarted(const QString &name, const QString &instruc
|
||||
setToastRequested(QStringLiteral("Peloton workout auto started skipping the intro! ") + name + QStringLiteral(" - ") + instructor);
|
||||
timer = (pelotonHandler->start_time - QDateTime::currentSecsSinceEpoch()) + 6; // 6 average time to push skip intro and wait the 3 seconds of the intro
|
||||
}
|
||||
if(timer <= 0)
|
||||
if(timer <= 0) {
|
||||
if(paused) {
|
||||
qDebug() << "starting due to peloton auto start";
|
||||
Start_inner(true);
|
||||
}
|
||||
peloton_start_workout();
|
||||
else {
|
||||
} else {
|
||||
QTimer::singleShot(timer * 1000, this, [this]() {
|
||||
if(paused) {
|
||||
qDebug() << "starting due to peloton auto start";
|
||||
Start_inner(true);
|
||||
}
|
||||
peloton_start_workout();
|
||||
});
|
||||
}
|
||||
@@ -1082,6 +1154,12 @@ QString homeform::getWritableAppDir() {
|
||||
if (android_documents_folder || QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::Android, 14)) {
|
||||
path = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/QZ/";
|
||||
QDir().mkdir(path);
|
||||
// Create .nomedia file to prevent gallery indexing
|
||||
QFile nomediaFile(path + ".nomedia");
|
||||
if (!nomediaFile.exists()) {
|
||||
nomediaFile.open(QIODevice::WriteOnly);
|
||||
nomediaFile.close();
|
||||
}
|
||||
} else {
|
||||
path = getAndroidDataAppDir() + "/";
|
||||
}
|
||||
@@ -1098,17 +1176,24 @@ QString homeform::getWritableAppDir() {
|
||||
void homeform::backup() {
|
||||
|
||||
static uint8_t index = 0;
|
||||
qDebug() << QStringLiteral("saving fit file backup...");
|
||||
qDebug() << QStringLiteral("scheduling fit file backup...");
|
||||
|
||||
QString path = getWritableAppDir();
|
||||
bluetoothdevice *dev = bluetoothManager->device();
|
||||
if (dev) {
|
||||
|
||||
QString filename = path + QString::number(index) + backupFitFileName;
|
||||
QFile::remove(filename);
|
||||
qfit::save(filename, Session, dev->deviceType(),
|
||||
qobject_cast<m3ibike *>(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE,
|
||||
stravaPelotonWorkoutType, dev->bluetoothDevice.name());
|
||||
|
||||
// Use thread to write FIT backup file
|
||||
QMetaObject::invokeMethod(fitBackupWriter, "writeFitBackup",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QString, filename),
|
||||
Q_ARG(QList<SessionLine>, Session),
|
||||
Q_ARG(bluetoothdevice::BLUETOOTH_TYPE, dev->deviceType()),
|
||||
Q_ARG(uint32_t, qobject_cast<m3ibike *>(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE),
|
||||
Q_ARG(FIT_SPORT, stravaPelotonWorkoutType),
|
||||
Q_ARG(QString, workoutName()),
|
||||
Q_ARG(QString, dev->bluetoothDevice.name()));
|
||||
|
||||
index++;
|
||||
if (index > 1) {
|
||||
@@ -1117,6 +1202,95 @@ void homeform::backup() {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::ten_hz() {
|
||||
// Automatic Virtual Shifting logic - only for bikes and when device is connected
|
||||
if (!bluetoothManager->device() || bluetoothManager->device()->deviceType() != bluetoothdevice::BIKE) {
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
if (!settings.value(QZSettings::automatic_virtual_shifting_enabled, QZSettings::default_automatic_virtual_shifting_enabled).toBool()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t cadence = bluetoothManager->device()->currentCadence().value();
|
||||
if (cadence == 0) {
|
||||
return; // No cadence data available
|
||||
}
|
||||
|
||||
// Get selected profile (0=cruise, 1=climb, 2=sprint)
|
||||
int profile = settings.value(QZSettings::automatic_virtual_shifting_profile, QZSettings::default_automatic_virtual_shifting_profile).toInt();
|
||||
|
||||
int gearUpCadenceThreshold, gearDownCadenceThreshold;
|
||||
float gearUpTimeThreshold, gearDownTimeThreshold;
|
||||
|
||||
// Load settings based on selected profile
|
||||
switch (profile) {
|
||||
case 1: // Climb profile
|
||||
gearUpCadenceThreshold = settings.value(QZSettings::automatic_virtual_shifting_climb_gear_up_cadence, QZSettings::default_automatic_virtual_shifting_climb_gear_up_cadence).toInt();
|
||||
gearUpTimeThreshold = settings.value(QZSettings::automatic_virtual_shifting_climb_gear_up_time, QZSettings::default_automatic_virtual_shifting_climb_gear_up_time).toFloat();
|
||||
gearDownCadenceThreshold = settings.value(QZSettings::automatic_virtual_shifting_climb_gear_down_cadence, QZSettings::default_automatic_virtual_shifting_climb_gear_down_cadence).toInt();
|
||||
gearDownTimeThreshold = settings.value(QZSettings::automatic_virtual_shifting_climb_gear_down_time, QZSettings::default_automatic_virtual_shifting_climb_gear_down_time).toFloat();
|
||||
break;
|
||||
case 2: // Sprint profile
|
||||
gearUpCadenceThreshold = settings.value(QZSettings::automatic_virtual_shifting_sprint_gear_up_cadence, QZSettings::default_automatic_virtual_shifting_sprint_gear_up_cadence).toInt();
|
||||
gearUpTimeThreshold = settings.value(QZSettings::automatic_virtual_shifting_sprint_gear_up_time, QZSettings::default_automatic_virtual_shifting_sprint_gear_up_time).toFloat();
|
||||
gearDownCadenceThreshold = settings.value(QZSettings::automatic_virtual_shifting_sprint_gear_down_cadence, QZSettings::default_automatic_virtual_shifting_sprint_gear_down_cadence).toInt();
|
||||
gearDownTimeThreshold = settings.value(QZSettings::automatic_virtual_shifting_sprint_gear_down_time, QZSettings::default_automatic_virtual_shifting_sprint_gear_down_time).toFloat();
|
||||
break;
|
||||
default: // Cruise profile (0)
|
||||
gearUpCadenceThreshold = settings.value(QZSettings::automatic_virtual_shifting_gear_up_cadence, QZSettings::default_automatic_virtual_shifting_gear_up_cadence).toInt();
|
||||
gearUpTimeThreshold = settings.value(QZSettings::automatic_virtual_shifting_gear_up_time, QZSettings::default_automatic_virtual_shifting_gear_up_time).toFloat();
|
||||
gearDownCadenceThreshold = settings.value(QZSettings::automatic_virtual_shifting_gear_down_cadence, QZSettings::default_automatic_virtual_shifting_gear_down_cadence).toInt();
|
||||
gearDownTimeThreshold = settings.value(QZSettings::automatic_virtual_shifting_gear_down_time, QZSettings::default_automatic_virtual_shifting_gear_down_time).toFloat();
|
||||
break;
|
||||
}
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
// Check for gear up condition
|
||||
if (cadence >= gearUpCadenceThreshold) {
|
||||
// Start or continue timing for gear up
|
||||
if (automaticShiftingGearUpStartTime.isNull() || automaticShiftingGearUpStartTime.msecsTo(now) < 0) {
|
||||
automaticShiftingGearUpStartTime = now;
|
||||
}
|
||||
// Reset gear down timer since we're above gear up threshold
|
||||
automaticShiftingGearDownStartTime = now;
|
||||
|
||||
// Check if enough time has passed for gear up
|
||||
if (automaticShiftingGearUpStartTime.msecsTo(now) >= (gearUpTimeThreshold * 1000)) {
|
||||
qDebug() << "Automatic gear up triggered: cadence" << cadence << "threshold" << gearUpCadenceThreshold << "time" << automaticShiftingGearUpStartTime.msecsTo(now) / 1000.0;
|
||||
((bike *)bluetoothManager->device())->gearUp();
|
||||
// Reset both timers after shifting
|
||||
automaticShiftingGearUpStartTime = now;
|
||||
automaticShiftingGearDownStartTime = now;
|
||||
}
|
||||
}
|
||||
// Check for gear down condition
|
||||
else if (cadence <= gearDownCadenceThreshold) {
|
||||
// Start or continue timing for gear down
|
||||
if (automaticShiftingGearDownStartTime.isNull() || automaticShiftingGearDownStartTime.msecsTo(now) < 0) {
|
||||
automaticShiftingGearDownStartTime = now;
|
||||
}
|
||||
// Reset gear up timer since we're below gear down threshold
|
||||
automaticShiftingGearUpStartTime = now;
|
||||
|
||||
// Check if enough time has passed for gear down
|
||||
if (automaticShiftingGearDownStartTime.msecsTo(now) >= (gearDownTimeThreshold * 1000)) {
|
||||
qDebug() << "Automatic gear down triggered: cadence" << cadence << "threshold" << gearDownCadenceThreshold << "time" << automaticShiftingGearDownStartTime.msecsTo(now) / 1000.0;
|
||||
((bike *)bluetoothManager->device())->gearDown();
|
||||
// Reset both timers after shifting
|
||||
automaticShiftingGearUpStartTime = now;
|
||||
automaticShiftingGearDownStartTime = now;
|
||||
}
|
||||
}
|
||||
// Cadence is between thresholds - reset both timers
|
||||
else {
|
||||
automaticShiftingGearUpStartTime = now;
|
||||
automaticShiftingGearDownStartTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
QString homeform::stopColor() { return QStringLiteral("#00000000"); }
|
||||
|
||||
QString homeform::startColor() {
|
||||
@@ -1142,6 +1316,15 @@ void homeform::refresh_bluetooth_devices_clicked() {
|
||||
}
|
||||
|
||||
homeform::~homeform() {
|
||||
// Cleanup FIT backup thread
|
||||
if (fitBackupThread && fitBackupThread->isRunning()) {
|
||||
fitBackupThread->quit();
|
||||
fitBackupThread->wait();
|
||||
}
|
||||
if (fitBackupWriter) {
|
||||
delete fitBackupWriter;
|
||||
}
|
||||
|
||||
gpx_save_clicked();
|
||||
fit_save_clicked();
|
||||
}
|
||||
@@ -1312,13 +1495,21 @@ QStringList homeform::tile_order() {
|
||||
|
||||
// these events are coming from the SS2K, so when the auto resistance is off, this event shouldn't be processed
|
||||
void homeform::gearUp() {
|
||||
if (autoResistance())
|
||||
if (autoResistance()) {
|
||||
Plus(QStringLiteral("gears"));
|
||||
// Reset automatic shifting timers when user manually changes gears
|
||||
automaticShiftingGearUpStartTime = QDateTime::currentDateTime();
|
||||
automaticShiftingGearDownStartTime = QDateTime::currentDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::gearDown() {
|
||||
if (autoResistance())
|
||||
if (autoResistance()) {
|
||||
Minus(QStringLiteral("gears"));
|
||||
// Reset automatic shifting timers when user manually changes gears
|
||||
automaticShiftingGearUpStartTime = QDateTime::currentDateTime();
|
||||
automaticShiftingGearDownStartTime = QDateTime::currentDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::ftmsAccessoryConnected(smartspin2k *d) {
|
||||
@@ -2437,6 +2628,25 @@ void homeform::sortTiles() {
|
||||
dataList.append(biggearsMinus);
|
||||
}
|
||||
|
||||
// Automatic Virtual Shifting tiles
|
||||
if (settings.value(QZSettings::tile_auto_virtual_shifting_cruise_enabled, QZSettings::default_tile_auto_virtual_shifting_cruise_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_auto_virtual_shifting_cruise_order, QZSettings::default_tile_auto_virtual_shifting_cruise_order).toInt() == i) {
|
||||
autoVirtualShiftingCruise->setGridId(i);
|
||||
dataList.append(autoVirtualShiftingCruise);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_auto_virtual_shifting_climb_enabled, QZSettings::default_tile_auto_virtual_shifting_climb_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_auto_virtual_shifting_climb_order, QZSettings::default_tile_auto_virtual_shifting_climb_order).toInt() == i) {
|
||||
autoVirtualShiftingClimb->setGridId(i);
|
||||
dataList.append(autoVirtualShiftingClimb);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_auto_virtual_shifting_sprint_enabled, QZSettings::default_tile_auto_virtual_shifting_sprint_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_auto_virtual_shifting_sprint_order, QZSettings::default_tile_auto_virtual_shifting_sprint_order).toInt() == i) {
|
||||
autoVirtualShiftingSprint->setGridId(i);
|
||||
dataList.append(autoVirtualShiftingSprint);
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::tile_preset_powerzone_1_enabled, QZSettings::default_tile_preset_powerzone_1_enabled).toBool() &&
|
||||
settings.value(QZSettings::tile_preset_powerzone_1_order, QZSettings::default_tile_preset_powerzone_1_order).toInt() == i) {
|
||||
preset_powerzone_1->setGridId(i);
|
||||
@@ -3790,23 +4000,32 @@ void homeform::LargeButton(const QString &name) {
|
||||
double zoneValue = settings.value(zoneSetting, zoneNum).toDouble();
|
||||
|
||||
// Calculate target watts based on FTP and zone value
|
||||
// Each zone represents a percentage of FTP
|
||||
double targetWatts;
|
||||
// Map zoneValue to the correct percentage within each power zone
|
||||
double targetPerc;
|
||||
if (zoneValue <= 1.9) {
|
||||
targetWatts = ftp * 0.55 * (zoneValue);
|
||||
// Zone 1: 0-55% FTP range
|
||||
targetPerc = zoneValue * 0.50; // zoneValue 1.0->50%, safely within Zone 1 boundary
|
||||
if (targetPerc > 0.55) targetPerc = 0.55;
|
||||
} else if (zoneValue <= 2.9) {
|
||||
targetWatts = ftp * 0.75 * (zoneValue - 1);
|
||||
// Zone 2: 56-75% FTP range
|
||||
targetPerc = 0.56 + (zoneValue - 2.0) * 0.19; // zoneValue 2.0->56%, 3.0->75%
|
||||
} else if (zoneValue <= 3.9) {
|
||||
targetWatts = ftp * 0.90 * (zoneValue - 2);
|
||||
// Zone 3: 76-90% FTP range
|
||||
targetPerc = 0.76 + (zoneValue - 3.0) * 0.14; // zoneValue 3.0->76%, 4.0->90%
|
||||
} else if (zoneValue <= 4.9) {
|
||||
targetWatts = ftp * 1.05 * (zoneValue - 3);
|
||||
// Zone 4: 91-105% FTP range
|
||||
targetPerc = 0.91 + (zoneValue - 4.0) * 0.14; // zoneValue 4.0->91%, 5.0->105%
|
||||
} else if (zoneValue <= 5.9) {
|
||||
targetWatts = ftp * 1.20 * (zoneValue - 4);
|
||||
// Zone 5: 106-120% FTP range
|
||||
targetPerc = 1.06 + (zoneValue - 5.0) * 0.14; // zoneValue 5.0->106%, 6.0->120%
|
||||
} else if (zoneValue <= 6.9) {
|
||||
targetWatts = ftp * 1.50 * (zoneValue - 5);
|
||||
// Zone 6: 121-150% FTP range
|
||||
targetPerc = 1.21 + (zoneValue - 6.0) * 0.29; // zoneValue 6.0->121%, 7.0->150%
|
||||
} else {
|
||||
targetWatts = ftp * 1.70 * (zoneValue - 6);
|
||||
// Zone 7: 151%+ FTP range
|
||||
targetPerc = 1.51 + (zoneValue - 7.0) * 0.19; // zoneValue 7.0->151%, 8.0->170%
|
||||
}
|
||||
double targetWatts = ftp * targetPerc;
|
||||
bluetoothManager->device()->changePower(targetWatts);
|
||||
} else if (name.contains(QStringLiteral("erg_mode"))) {
|
||||
settings.setValue(QZSettings::zwift_erg, !settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool());
|
||||
@@ -4046,9 +4265,20 @@ void homeform::LargeButton(const QString &name) {
|
||||
gearUp();
|
||||
} else if(name.contains(QStringLiteral("biggearsminus"))) {
|
||||
gearDown();
|
||||
} else if(name.contains(QStringLiteral("autoVirtualShiftingCruise"))) {
|
||||
// Switch to Cruise profile (0)
|
||||
settings.setValue(QZSettings::automatic_virtual_shifting_profile, 0);
|
||||
} else if(name.contains(QStringLiteral("autoVirtualShiftingClimb"))) {
|
||||
// Switch to Climb profile (1)
|
||||
settings.setValue(QZSettings::automatic_virtual_shifting_profile, 1);
|
||||
} else if(name.contains(QStringLiteral("autoVirtualShiftingSprint"))) {
|
||||
// Switch to Sprint profile (2)
|
||||
settings.setValue(QZSettings::automatic_virtual_shifting_profile, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void homeform::Plus(const QString &name) {
|
||||
QSettings settings;
|
||||
|
||||
@@ -4234,15 +4464,34 @@ void homeform::Plus(const QString &name) {
|
||||
}
|
||||
} else if (name.contains(QStringLiteral("resistance")) || name.contains(QStringLiteral("peloton_resistance"))) {
|
||||
if (bluetoothManager->device()) {
|
||||
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
((bike *)bluetoothManager->device())
|
||||
->changeResistance(((bike *)bluetoothManager->device())->currentResistance().value() + 1);
|
||||
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) {
|
||||
((rower *)bluetoothManager->device())
|
||||
->changeResistance(((rower *)bluetoothManager->device())->currentResistance().value() + 1);
|
||||
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
|
||||
((elliptical *)bluetoothManager->device())
|
||||
->changeResistance(((elliptical *)bluetoothManager->device())->currentResistance().value() + 1);
|
||||
auto dev = bluetoothManager->device();
|
||||
double current = dev->currentResistance().value();
|
||||
double diff = dev->difficult();
|
||||
if (diff == 0) diff = 1.0; // safety
|
||||
resistance_t maxRes = dev->maxResistance();
|
||||
|
||||
if (dev->deviceType() == bluetoothdevice::BIKE) {
|
||||
double g = ((bike *)dev)->gears();
|
||||
double target = current + 1; // device-space target
|
||||
int raw = qRound((target - g) / diff);
|
||||
if (raw < 1) raw = 1;
|
||||
if (raw > maxRes) raw = maxRes;
|
||||
((bike *)dev)->changeResistance(raw);
|
||||
} else if (dev->deviceType() == bluetoothdevice::ROWING) {
|
||||
double g = ((rower *)dev)->gears();
|
||||
double target = current + 1; // device-space target
|
||||
int raw = qRound((target - g) / diff);
|
||||
if (raw < 1) raw = 1;
|
||||
if (raw > maxRes) raw = maxRes;
|
||||
((rower *)dev)->changeResistance(raw);
|
||||
} else if (dev->deviceType() == bluetoothdevice::ELLIPTICAL) {
|
||||
double g = ((elliptical *)dev)->gears();
|
||||
double target = current + 1; // device-space target
|
||||
// elliptical::changeResistance does not use difficult(), but keep formula consistent
|
||||
int raw = qRound((target - g) / diff);
|
||||
if (raw < 1) raw = 1;
|
||||
if (raw > maxRes) raw = maxRes;
|
||||
((elliptical *)dev)->changeResistance(raw);
|
||||
}
|
||||
}
|
||||
} else if (name.contains(QStringLiteral("target_power"))) {
|
||||
@@ -4288,6 +4537,17 @@ void homeform::Plus(const QString &name) {
|
||||
if (bluetoothManager->device() && trainProgram) {
|
||||
trainProgram->increaseElapsedTime(1);
|
||||
}
|
||||
} else if (name.contains(QStringLiteral("target_zone"))) {
|
||||
QSettings settings;
|
||||
double currentFtp = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
|
||||
if (currentFtp > 0 && bluetoothManager->device() &&
|
||||
bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
double currentTargetPower = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
double powerIncrement = currentFtp * 0.01; // 1% of FTP
|
||||
double newTargetPower = currentTargetPower + powerIncrement;
|
||||
((bike *)bluetoothManager->device())->changePower(newTargetPower);
|
||||
qDebug() << "Target power increased by" << powerIncrement << "W (1% of FTP) from" << currentTargetPower << "to" << newTargetPower;
|
||||
}
|
||||
} else {
|
||||
qDebug() << name << QStringLiteral("not handled");
|
||||
|
||||
@@ -4494,15 +4754,34 @@ void homeform::Minus(const QString &name) {
|
||||
}
|
||||
} else if (name.contains(QStringLiteral("resistance")) || name.contains(QStringLiteral("peloton_resistance"))) {
|
||||
if (bluetoothManager->device()) {
|
||||
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
((bike *)bluetoothManager->device())
|
||||
->changeResistance(((bike *)bluetoothManager->device())->currentResistance().value() - 1);
|
||||
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) {
|
||||
((rower *)bluetoothManager->device())
|
||||
->changeResistance(((rower *)bluetoothManager->device())->currentResistance().value() - 1);
|
||||
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
|
||||
((elliptical *)bluetoothManager->device())
|
||||
->changeResistance(((elliptical *)bluetoothManager->device())->currentResistance().value() - 1);
|
||||
auto dev = bluetoothManager->device();
|
||||
double current = dev->currentResistance().value();
|
||||
double diff = dev->difficult();
|
||||
if (diff == 0) diff = 1.0; // safety
|
||||
resistance_t maxRes = dev->maxResistance();
|
||||
|
||||
if (dev->deviceType() == bluetoothdevice::BIKE) {
|
||||
double g = ((bike *)dev)->gears();
|
||||
double target = current - 1; // device-space target
|
||||
int raw = qRound((target - g) / diff);
|
||||
if (raw < 1) raw = 1;
|
||||
if (raw > maxRes) raw = maxRes;
|
||||
((bike *)dev)->changeResistance(raw);
|
||||
} else if (dev->deviceType() == bluetoothdevice::ROWING) {
|
||||
double g = ((rower *)dev)->gears();
|
||||
double target = current - 1; // device-space target
|
||||
int raw = qRound((target - g) / diff);
|
||||
if (raw < 1) raw = 1;
|
||||
if (raw > maxRes) raw = maxRes;
|
||||
((rower *)dev)->changeResistance(raw);
|
||||
} else if (dev->deviceType() == bluetoothdevice::ELLIPTICAL) {
|
||||
double g = ((elliptical *)dev)->gears();
|
||||
double target = current - 1; // device-space target
|
||||
// elliptical::changeResistance does not use difficult(), but keep formula consistent
|
||||
int raw = qRound((target - g) / diff);
|
||||
if (raw < 1) raw = 1;
|
||||
if (raw > maxRes) raw = maxRes;
|
||||
((elliptical *)dev)->changeResistance(raw);
|
||||
}
|
||||
}
|
||||
} else if (name.contains(QStringLiteral("target_power"))) {
|
||||
@@ -4552,6 +4831,18 @@ void homeform::Minus(const QString &name) {
|
||||
if (bluetoothManager->device() && trainProgram) {
|
||||
trainProgram->decreaseElapsedTime(1);
|
||||
}
|
||||
} else if (name.contains(QStringLiteral("target_zone"))) {
|
||||
QSettings settings;
|
||||
double currentFtp = settings.value(QZSettings::ftp, QZSettings::default_ftp).toDouble();
|
||||
if (currentFtp > 0 && bluetoothManager->device() &&
|
||||
bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
double currentTargetPower = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
double powerDecrement = currentFtp * 0.01; // 1% of FTP
|
||||
double newTargetPower = currentTargetPower - powerDecrement;
|
||||
if (newTargetPower < 0) newTargetPower = 0; // Prevent negative power
|
||||
((bike *)bluetoothManager->device())->changePower(newTargetPower);
|
||||
qDebug() << "Target power decreased by" << powerDecrement << "W (1% of FTP) from" << currentTargetPower << "to" << newTargetPower;
|
||||
}
|
||||
} else {
|
||||
qDebug() << name << QStringLiteral("not handled");
|
||||
qDebug() << "Minus" << name;
|
||||
@@ -4583,7 +4874,12 @@ void homeform::Start_inner(bool send_event_to_device) {
|
||||
videoPlaybackHalfPlayer->pause();
|
||||
}
|
||||
} else {
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
if(h && !h->appleWatchAppInstalled() && bluetoothManager->device())
|
||||
h->startWorkout(bluetoothManager->device()->deviceType());
|
||||
#endif
|
||||
#endif
|
||||
if (bluetoothManager->device() && send_event_to_device) {
|
||||
bluetoothManager->device()->start();
|
||||
}
|
||||
@@ -4677,6 +4973,13 @@ void homeform::Stop() {
|
||||
|
||||
m_startRequested = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
if(h && !h->appleWatchAppInstalled())
|
||||
h->stopWorkout();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
qDebug() << QStringLiteral("Stop pressed - paused") << paused << QStringLiteral("stopped") << stopped;
|
||||
|
||||
if (stopped) {
|
||||
@@ -4713,6 +5016,10 @@ void homeform::Stop() {
|
||||
|
||||
emit workoutEventStateChanged(bluetoothdevice::STOPPED);
|
||||
|
||||
// Save session as training program only if it's not a Peloton workout
|
||||
if (!(pelotonHandler && !pelotonHandler->current_ride_id.isEmpty())) {
|
||||
saveSessionAsTrainingProgram();
|
||||
}
|
||||
fit_save_clicked();
|
||||
|
||||
if (bluetoothManager->device()) {
|
||||
@@ -4843,7 +5150,19 @@ void homeform::update() {
|
||||
double currentHRZone = 1;
|
||||
double ftpZone = 1;
|
||||
|
||||
qDebug() << "homeform::update fired!";
|
||||
// Timer jitter detection (same logic as trainprogram::scheduler)
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
qint64 msecsElapsed = lastUpdateCall.msecsTo(now);
|
||||
|
||||
// Reset jitter if it's getting too large
|
||||
if (qAbs(currentUpdateJitter) > 5000) {
|
||||
currentUpdateJitter = 0;
|
||||
}
|
||||
|
||||
currentUpdateJitter += msecsElapsed - 1000;
|
||||
lastUpdateCall = now;
|
||||
|
||||
qDebug() << "homeform::update fired!" << "elapsed:" << msecsElapsed << "jitter:" << currentUpdateJitter;
|
||||
|
||||
if (settings.status() != QSettings::NoError) {
|
||||
qDebug() << "!!!!QSETTINGS ERROR!" << settings.status();
|
||||
@@ -4993,8 +5312,9 @@ void homeform::update() {
|
||||
QString::number((bluetoothManager->device())->currentSpeed().max() * unit_conversion, 'f', 1));
|
||||
heart->setValue(QString::number(bluetoothManager->device()->currentHeart().value(), 'f', 0));
|
||||
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
calories->setValue(QString::number(bluetoothManager->device()->calories().value(), 'f', 0));
|
||||
calories->setSecondLine(QString::number(bluetoothManager->device()->calories().rate1s() * 60.0, 'f', 1) +
|
||||
calories->setSecondLine(QString::number((activeOnly ? bluetoothManager->device()->activeCalories().rate1s() : bluetoothManager->device()->calories().rate1s()) * 60.0, 'f', 1) +
|
||||
" /min");
|
||||
if (!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool())
|
||||
fan->setValue(QString::number(bluetoothManager->device()->fanSpeed()));
|
||||
@@ -5172,6 +5492,7 @@ void homeform::update() {
|
||||
QStringLiteral(" MAX: ") +
|
||||
QString::number(((bike *)bluetoothManager->device())->currentCadence().max(), 'f', 0));
|
||||
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
if (settings.value(QZSettings::volume_change_gears, QZSettings::default_volume_change_gears).toBool()) {
|
||||
@@ -5326,6 +5647,9 @@ void homeform::update() {
|
||||
}
|
||||
}
|
||||
|
||||
// Use different zone names for walking vs running workouts
|
||||
bool isWalkingWorkout = pelotonHandler && pelotonHandler->current_workout_type.toLower().startsWith("walking");
|
||||
|
||||
switch (trainProgram->currentRow().pace_intensity) {
|
||||
case 0:
|
||||
this->target_zone->setValue(tr("Rec."));
|
||||
@@ -5334,13 +5658,25 @@ void homeform::update() {
|
||||
this->target_zone->setValue(tr("Easy"));
|
||||
break;
|
||||
case 2:
|
||||
this->target_zone->setValue(tr("Moder."));
|
||||
if (isWalkingWorkout) {
|
||||
this->target_zone->setValue(tr("Brisk"));
|
||||
} else {
|
||||
this->target_zone->setValue(tr("Moder."));
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
this->target_zone->setValue(tr("Chall."));
|
||||
if (isWalkingWorkout) {
|
||||
this->target_zone->setValue(tr("Power"));
|
||||
} else {
|
||||
this->target_zone->setValue(tr("Chall."));
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
this->target_zone->setValue(tr("Hard"));
|
||||
if (isWalkingWorkout) {
|
||||
this->target_zone->setValue(tr("Max"));
|
||||
} else {
|
||||
this->target_zone->setValue(tr("Hard"));
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
this->target_zone->setValue(tr("V.Hard"));
|
||||
@@ -5515,6 +5851,12 @@ void homeform::update() {
|
||||
double elite_rizer_gain =
|
||||
settings.value(QZSettings::elite_rizer_gain, QZSettings::default_elite_rizer_gain).toDouble();
|
||||
ergMode->setLargeButtonColor(settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool() ? "#008000" :"#8B0000");
|
||||
|
||||
// Update automatic virtual shifting tile colors based on active profile
|
||||
int currentProfile = settings.value(QZSettings::automatic_virtual_shifting_profile, QZSettings::default_automatic_virtual_shifting_profile).toInt();
|
||||
autoVirtualShiftingCruise->setLargeButtonColor(currentProfile == 0 ? QStringLiteral("green") : QStringLiteral("red"));
|
||||
autoVirtualShiftingClimb->setLargeButtonColor(currentProfile == 1 ? QStringLiteral("green") : QStringLiteral("red"));
|
||||
autoVirtualShiftingSprint->setLargeButtonColor(currentProfile == 2 ? QStringLiteral("green") : QStringLiteral("red"));
|
||||
extIncline->setSecondLine(QStringLiteral("Gain: ") + QString::number(elite_rizer_gain, 'f', 1));
|
||||
odometer->setValue(QString::number(bluetoothManager->device()->odometer() * unit_conversion, 'f', 2));
|
||||
resistance = ((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
@@ -5563,7 +5905,7 @@ void homeform::update() {
|
||||
QString::number(((bike *)bluetoothManager->device())->currentSteeringAngle().value(), 'f', 1));
|
||||
|
||||
if ((!trainProgram || (trainProgram && !trainProgram->isStarted())) &&
|
||||
!((bike *)bluetoothManager->device())->ergModeSupportedAvailableByHardware() &&
|
||||
!((bike *)bluetoothManager->device())->ergModeSupportedAvailableBySoftware() &&
|
||||
((bike *)bluetoothManager->device())->lastRequestedPower().value() > 0 && m_overridePower) {
|
||||
qDebug() << QStringLiteral("using target power tile for ERG workout manually");
|
||||
((bike *)bluetoothManager->device())
|
||||
@@ -5653,7 +5995,7 @@ void homeform::update() {
|
||||
this->strokesCount->setValue(
|
||||
QString::number(((rower *)bluetoothManager->device())->currentStrokesCount().value(), 'f', 0));
|
||||
this->strokesLength->setValue(
|
||||
QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().value(), 'f', 1));
|
||||
QString::number(((rower *)bluetoothManager->device())->currentStrokesLength().value(), 'f', 2));
|
||||
|
||||
this->target_speed->setValue(QString::number(
|
||||
((rower *)bluetoothManager->device())->lastRequestedSpeed().value() * unit_conversion, 'f', 1));
|
||||
@@ -6498,9 +6840,9 @@ void homeform::update() {
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
double step = 1;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableByHardware();
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableBySoftware();
|
||||
if(ergMode) {
|
||||
step = 5;
|
||||
step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
}
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
@@ -6951,17 +7293,70 @@ void homeform::update() {
|
||||
|
||||
qDebug() << "Current Distance 1s:" << bluetoothManager->device()->currentDistance1s().value() << bluetoothManager->device()->currentSpeed().value() << watts;
|
||||
|
||||
// Calculate current elapsed time in seconds
|
||||
uint32_t currentElapsedSeconds = bluetoothManager->device()->elapsedTime().second() +
|
||||
(bluetoothManager->device()->elapsedTime().minute() * 60) +
|
||||
(bluetoothManager->device()->elapsedTime().hour() * 3600);
|
||||
|
||||
if (Session.empty()) {
|
||||
currentUpdateJitter = 0;
|
||||
}
|
||||
|
||||
// Check for timer jitter gaps and fill missing SessionLine records (same logic as trainprogram)
|
||||
if (!Session.empty() && qAbs(currentUpdateJitter) > 1000) {
|
||||
if (currentUpdateJitter > 1000) {
|
||||
// We are late... fill the missing seconds with SessionLine records
|
||||
int missedSeconds = currentUpdateJitter / 1000;
|
||||
qDebug() << "Timer jitter detected: filling" << missedSeconds << "missing SessionLine records";
|
||||
|
||||
// Create SessionLine records for each missed second using current device values
|
||||
uint32_t lastRecordedTime = Session.last().elapsedTime;
|
||||
for (int i = 1; i <= missedSeconds; i++) {
|
||||
SessionLine gapFill(
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
|
||||
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
|
||||
pace, cadence, bluetoothManager->device()->calories().value(),
|
||||
bluetoothManager->device()->elevationGain().value(),
|
||||
lastRecordedTime + i, // Fill each missing second
|
||||
lapTrigger, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
|
||||
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
|
||||
target_cadence->value().toDouble(), target_power->value().toDouble(), target_resistance->value().toDouble(),
|
||||
target_incline->value().toDouble(), target_speed->value().toDouble(),
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value());
|
||||
|
||||
Session.append(gapFill);
|
||||
qDebug() << "Added gap-filling SessionLine for elapsed time:" << (lastRecordedTime + i);
|
||||
}
|
||||
|
||||
// Adjust jitter counter (same as trainprogram)
|
||||
currentUpdateJitter -= (missedSeconds * 1000);
|
||||
} else if (currentUpdateJitter < -1000) {
|
||||
// We are early (negative jitter)... remove excess SessionLine records
|
||||
int excessSeconds = (-currentUpdateJitter) / 1000;
|
||||
qDebug() << "Negative timer jitter detected: removing" << excessSeconds << "excess SessionLine records";
|
||||
|
||||
// Remove excess SessionLine records from the end
|
||||
for (int i = 0; i < excessSeconds && !Session.empty(); i++) {
|
||||
Session.removeLast();
|
||||
qDebug() << "Removed excess SessionLine record";
|
||||
}
|
||||
|
||||
// Adjust jitter counter (same as trainprogram)
|
||||
currentUpdateJitter += (excessSeconds * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
SessionLine s(
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
|
||||
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
|
||||
pace, cadence, bluetoothManager->device()->calories().value(),
|
||||
bluetoothManager->device()->elevationGain().value(),
|
||||
bluetoothManager->device()->elapsedTime().second() +
|
||||
(bluetoothManager->device()->elapsedTime().minute() * 60) +
|
||||
(bluetoothManager->device()->elapsedTime().hour() * 3600),
|
||||
currentElapsedSeconds,
|
||||
|
||||
lapTrigger, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
|
||||
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
|
||||
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
|
||||
target_cadence->value().toDouble(), target_power->value().toDouble(), target_resistance->value().toDouble(),
|
||||
target_incline->value().toDouble(), target_speed->value().toDouble(),
|
||||
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value());
|
||||
|
||||
Session.append(s);
|
||||
@@ -6972,9 +7367,13 @@ void homeform::update() {
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
if (iphone_socket && iphone_socket->state() == QAbstractSocket::ConnectedState) {
|
||||
QSettings mdns_settings;
|
||||
bool activeOnly = mdns_settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
|
||||
QString toSend =
|
||||
"SENDER=PAD#HR=" + QString::number(bluetoothManager->device()->currentHeart().value()) +
|
||||
"#KCAL=" + QString::number(bluetoothManager->device()->calories().value()) +
|
||||
(activeOnly ? "#TOTALKCAL=" + QString::number(bluetoothManager->device()->totalCalories().value()) : "") +
|
||||
"#BCAD=" + QString::number(bluetoothManager->device()->currentCadence().value()) +
|
||||
"#SPD=" + QString::number(bluetoothManager->device()->currentSpeed().value()) +
|
||||
"#PWR=" + QString::number(bluetoothManager->device()->wattsMetric().value()) +
|
||||
@@ -7153,6 +7552,7 @@ void homeform::trainprogram_preview(const QUrl &fileName) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void homeform::trainprogram_zwo_loaded(const QString &s) {
|
||||
qDebug() << QStringLiteral("trainprogram_zwo_loaded") << s;
|
||||
trainProgram = new trainprogram(zwiftworkout::loadJSON(s), bluetoothManager);
|
||||
@@ -7189,12 +7589,84 @@ void homeform::gpx_save_clicked() {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::saveSessionAsTrainingProgram() {
|
||||
if (Session.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString path = getWritableAppDir();
|
||||
bluetoothdevice *dev = bluetoothManager->device();
|
||||
if (!dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine subdirectory based on device type
|
||||
QString subdir;
|
||||
if (dev->deviceType() == bluetoothdevice::BIKE) {
|
||||
subdir = "ride/";
|
||||
} else if (dev->deviceType() == bluetoothdevice::TREADMILL) {
|
||||
subdir = "run/";
|
||||
} else if (dev->deviceType() == bluetoothdevice::ROWING) {
|
||||
subdir = "row/";
|
||||
} else {
|
||||
subdir = "workout/";
|
||||
}
|
||||
|
||||
// Create the subdirectory if it doesn't exist
|
||||
QDir dir(path + subdir);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QString filename = path + subdir +
|
||||
QDateTime::currentDateTime().toString().replace(QStringLiteral(":"), QStringLiteral("_")) +
|
||||
QStringLiteral("_session.xml");
|
||||
|
||||
// Convert Session data to trainrow format
|
||||
QList<trainrow> rows;
|
||||
for (int i = 0; i < Session.size(); i++) {
|
||||
const SessionLine &sessionLine = Session[i];
|
||||
trainrow row;
|
||||
|
||||
// Set duration to 1 second since we collect data every second
|
||||
row.duration = QTime(0, 0, 1);
|
||||
|
||||
// Set target values based on device type
|
||||
if (dev->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (sessionLine.target_watt > 0) {
|
||||
row.power = static_cast<int32_t>(sessionLine.target_watt);
|
||||
}
|
||||
if (sessionLine.target_cadence > 0) {
|
||||
row.cadence = static_cast<int16_t>(sessionLine.target_cadence);
|
||||
}
|
||||
if (sessionLine.target_resistance >= 0) {
|
||||
row.resistance = sessionLine.target_resistance;
|
||||
}
|
||||
} else if (dev->deviceType() == bluetoothdevice::TREADMILL) {
|
||||
if (sessionLine.target_speed > 0) {
|
||||
row.speed = sessionLine.target_speed;
|
||||
}
|
||||
if (sessionLine.target_inclination >= -50) {
|
||||
row.inclination = sessionLine.target_inclination;
|
||||
}
|
||||
}
|
||||
|
||||
rows.append(row);
|
||||
}
|
||||
|
||||
// Save the XML file
|
||||
if (trainprogram::saveXML(filename, rows)) {
|
||||
lastTrainProgramFileSaved = filename;
|
||||
qDebug() << "Session saved as training program:" << filename;
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::fit_save_clicked() {
|
||||
|
||||
QString path = getWritableAppDir();
|
||||
bluetoothdevice *dev = bluetoothManager->device();
|
||||
if (dev) {
|
||||
QString filename = path +
|
||||
QString filename = path + "fit/" +
|
||||
QDateTime::currentDateTime().toString().replace(QStringLiteral(":"), QStringLiteral("_")) +
|
||||
QStringLiteral(".fit");
|
||||
|
||||
@@ -7202,11 +7674,38 @@ void homeform::fit_save_clicked() {
|
||||
if (!stravaPelotonActivityName.isEmpty() && !stravaPelotonInstructorName.isEmpty())
|
||||
workoutName = stravaPelotonActivityName + " - " + stravaPelotonInstructorName;
|
||||
|
||||
// Determine workout source and metadata
|
||||
QString workoutSource = "QZ";
|
||||
QString pelotonWorkoutId = "";
|
||||
QString pelotonUrl = "";
|
||||
QString trainingProgramFile = "";
|
||||
|
||||
if (pelotonHandler && !pelotonHandler->current_ride_id.isEmpty()) {
|
||||
workoutSource = "PELOTON";
|
||||
pelotonWorkoutId = pelotonHandler->current_ride_id;
|
||||
pelotonUrl = pelotonHandler->getPelotonWorkoutUrl();
|
||||
if (!lastTrainProgramFileSaved.isEmpty()) {
|
||||
trainingProgramFile = lastTrainProgramFileSaved;
|
||||
}
|
||||
} else {
|
||||
// For non-Peloton workouts, use the session XML file if available
|
||||
if (!lastTrainProgramFileSaved.isEmpty()) {
|
||||
trainingProgramFile = lastTrainProgramFileSaved;
|
||||
}
|
||||
}
|
||||
|
||||
qfit::save(filename, Session, dev->deviceType(),
|
||||
qobject_cast<m3ibike *>(dev) ? QFIT_PROCESS_DISTANCENOISE : QFIT_PROCESS_NONE,
|
||||
stravaPelotonWorkoutType, workoutName, dev->bluetoothDevice.name());
|
||||
stravaPelotonWorkoutType, workoutName, dev->bluetoothDevice.name(),
|
||||
workoutSource, pelotonWorkoutId, pelotonUrl, trainingProgramFile);
|
||||
lastFitFileSaved = filename;
|
||||
|
||||
// Process the newly saved file immediately and refresh workout model
|
||||
if (fitProcessor && workoutModel) {
|
||||
fitProcessor->processFile(filename);
|
||||
workoutModel->refresh();
|
||||
}
|
||||
|
||||
QSettings settings;
|
||||
if (!settings.value(QZSettings::strava_accesstoken, QZSettings::default_strava_accesstoken)
|
||||
.toString()
|
||||
@@ -7310,6 +7809,33 @@ void homeform::gpx_open_clicked(const QUrl &fileName) {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::fitfile_preview_clicked(const QUrl &fileName) {
|
||||
qDebug() << QStringLiteral("fitfile_preview_clicked called with URL:") << fileName;
|
||||
qDebug() << QStringLiteral("URL toString:") << fileName.toString();
|
||||
qDebug() << QStringLiteral("URL toLocalFile:") << fileName.toLocalFile();
|
||||
|
||||
// Use the full file path directly instead of reconstructing it
|
||||
QString filePath = fileName.toLocalFile();
|
||||
QFile file(filePath);
|
||||
qDebug() << "Opening FIT file:" << filePath;
|
||||
|
||||
if (file.exists()) {
|
||||
QList<SessionLine> a;
|
||||
FIT_SPORT sport;
|
||||
QString workoutName;
|
||||
qfit::open(filePath, &a, &sport, &workoutName);
|
||||
qDebug() << "FIT file read:" << a.size() << "records, sport:" << sport << "workoutName:" << workoutName;
|
||||
if (!a.isEmpty()) {
|
||||
this->innerTemplateManager->previewSessionOnChart(&a, sport, workoutName);
|
||||
emit previewFitFile(filePath, QTime(0,0,0,0).addSecs(a.last().elapsedTime).toString(), workoutName);
|
||||
} else {
|
||||
qDebug() << "No data read from FIT file";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "FIT file does not exist:" << filePath;
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::gpxpreview_open_clicked(const QUrl &fileName) {
|
||||
qDebug() << QStringLiteral("gpxpreview_open_clicked") << fileName;
|
||||
|
||||
@@ -8316,10 +8842,19 @@ void homeform::loadSettings(const QUrl &filename) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit signal when settings are loaded as they might contain user profile changes
|
||||
if (homeform::singleton()) {
|
||||
emit homeform::singleton()->userProfileChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::deleteSettings(const QUrl &filename) { QFile(filename.toLocalFile()).remove(); }
|
||||
void homeform::restoreSettings() { QZSettings::restoreAll(); }
|
||||
void homeform::restoreSettings() {
|
||||
QZSettings::restoreAll();
|
||||
// Emit signal when settings are restored as this might affect user profiles
|
||||
emit userProfileChanged();
|
||||
}
|
||||
|
||||
QString homeform::getProfileDir() {
|
||||
QString path = getWritableAppDir() + "profiles";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PathController.h"
|
||||
#include "bluetooth.h"
|
||||
#include "fit_profile.hpp"
|
||||
#include "fitdatabaseprocessor.h"
|
||||
#include "gpx.h"
|
||||
#include "OAuth2.h"
|
||||
#include "peloton.h"
|
||||
@@ -14,6 +15,8 @@
|
||||
#include "sessionline.h"
|
||||
#include "smtpclient/src/SmtpMime"
|
||||
#include "trainprogram.h"
|
||||
#include "workoutmodel.h"
|
||||
#include "fitbackupwriter.h"
|
||||
#include <QChart>
|
||||
#include <QColor>
|
||||
#include <QGraphicsScene>
|
||||
@@ -24,9 +27,12 @@
|
||||
#include <QQuickItem>
|
||||
#include <QQuickItemGrabResult>
|
||||
#include <QTextToSpeech>
|
||||
#include <QThread>
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
#include <QAndroidJniEnvironment>
|
||||
#include <QtAndroid>
|
||||
#endif
|
||||
@@ -358,6 +364,8 @@ class homeform : public QObject {
|
||||
|
||||
Q_INVOKABLE bool firstRun() {
|
||||
QSettings settings;
|
||||
|
||||
bool android_antbike = settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
|
||||
QString proformtdf4ip = settings.value(QZSettings::proformtdf4ip, QZSettings::default_proformtdf4ip).toString();
|
||||
QString proformtdf1ip = settings.value(QZSettings::proformtdf1ip, QZSettings::default_proformtdf1ip).toString();
|
||||
QString proformtreadmillip = settings.value(QZSettings::proformtreadmillip, QZSettings::default_proformtreadmillip).toString();
|
||||
@@ -378,7 +386,7 @@ class homeform : public QObject {
|
||||
|
||||
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() &&
|
||||
!fakedevice_rower && !fakedevice_treadmill && !antbike && !android_antbike && proform_elliptical_ip.isEmpty() &&
|
||||
proformtdf4ip.isEmpty() && proformtdf1ip.isEmpty() && proformtreadmillip.isEmpty();
|
||||
}
|
||||
|
||||
@@ -704,6 +712,9 @@ class homeform : public QObject {
|
||||
DataObject *tile_heat_time_in_zone_3;
|
||||
DataObject *tile_heat_time_in_zone_4;
|
||||
DataObject *coreTemperature;
|
||||
DataObject *autoVirtualShiftingCruise;
|
||||
DataObject *autoVirtualShiftingClimb;
|
||||
DataObject *autoVirtualShiftingSprint;
|
||||
|
||||
private:
|
||||
static homeform *m_singleton;
|
||||
@@ -740,11 +751,21 @@ class homeform : public QObject {
|
||||
bool stopped = false;
|
||||
bool lapTrigger = false;
|
||||
|
||||
// Automatic Virtual Shifting variables
|
||||
QDateTime automaticShiftingGearUpStartTime = QDateTime::currentDateTime();
|
||||
QDateTime automaticShiftingGearDownStartTime = QDateTime::currentDateTime();
|
||||
|
||||
// Timer jitter detection variables (same logic as trainprogram::scheduler)
|
||||
QDateTime lastUpdateCall = QDateTime::currentDateTime();
|
||||
qint64 currentUpdateJitter = 0;
|
||||
|
||||
peloton *pelotonHandler = nullptr;
|
||||
bool m_pelotonAskStart = false;
|
||||
QString m_pelotonProvider = "";
|
||||
QString m_toastRequested = "";
|
||||
bool m_stravaUploadRequested = false;
|
||||
FitDatabaseProcessor *fitProcessor = nullptr;
|
||||
WorkoutModel *workoutModel = nullptr;
|
||||
int m_pelotonLoginState = -1;
|
||||
int m_pzpLoginState = -1;
|
||||
int m_zwiftLoginState = -1;
|
||||
@@ -771,6 +792,11 @@ class homeform : public QObject {
|
||||
|
||||
QTimer *timer;
|
||||
QTimer *backupTimer;
|
||||
QTimer *automaticShiftingTimer;
|
||||
|
||||
// FIT backup threading
|
||||
QThread *fitBackupThread;
|
||||
FitBackupWriter *fitBackupWriter;
|
||||
|
||||
QString strava_code;
|
||||
QOAuth2AuthorizationCodeFlow *strava_connect();
|
||||
@@ -790,6 +816,7 @@ class homeform : public QObject {
|
||||
int16_t fanOverride = 0;
|
||||
|
||||
void update();
|
||||
void ten_hz();
|
||||
double heartRateMax();
|
||||
void backup();
|
||||
bool getDevice();
|
||||
@@ -813,6 +840,9 @@ class homeform : public QObject {
|
||||
bool floating_open = false;
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = nullptr;
|
||||
#endif
|
||||
bool m_locationServices = true;
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
@@ -836,6 +866,7 @@ class homeform : public QObject {
|
||||
bool pelotonAskStart() { return m_pelotonAskStart; }
|
||||
void Minus(const QString &);
|
||||
void Plus(const QString &);
|
||||
void trainprogram_open_clicked(const QUrl &fileName);
|
||||
|
||||
private slots:
|
||||
void Start();
|
||||
@@ -853,17 +884,18 @@ class homeform : public QObject {
|
||||
void openFloatingWindowBrowser();
|
||||
void deviceFound(const QString &name);
|
||||
void deviceConnected(QBluetoothDeviceInfo b);
|
||||
void ftmsAccessoryConnected(smartspin2k *d);
|
||||
void trainprogram_open_clicked(const QUrl &fileName);
|
||||
void ftmsAccessoryConnected(smartspin2k *d);
|
||||
void trainprogram_open_other_folder(const QUrl &fileName);
|
||||
void gpx_open_other_folder(const QUrl &fileName);
|
||||
void profile_open_clicked(const QUrl &fileName);
|
||||
void trainprogram_preview(const QUrl &fileName);
|
||||
void gpxpreview_open_clicked(const QUrl &fileName);
|
||||
void fitfile_preview_clicked(const QUrl &fileName);
|
||||
void trainprogram_zwo_loaded(const QString &comp);
|
||||
void gpx_open_clicked(const QUrl &fileName);
|
||||
void gpx_save_clicked();
|
||||
void fit_save_clicked();
|
||||
void saveSessionAsTrainingProgram();
|
||||
void strava_connect_clicked();
|
||||
void trainProgramSignals();
|
||||
void refresh_bluetooth_devices_clicked();
|
||||
@@ -941,6 +973,7 @@ class homeform : public QObject {
|
||||
void pelotonLoginChanged(int ok);
|
||||
void pzpLoginChanged(int ok);
|
||||
void zwiftLoginChanged(int ok);
|
||||
void userProfileChanged();
|
||||
void workoutNameChanged(QString name);
|
||||
void workoutStartDateChanged(QString name);
|
||||
void instructorNameChanged(QString name);
|
||||
@@ -950,6 +983,9 @@ class homeform : public QObject {
|
||||
void previewWorkoutPointsChanged(int value);
|
||||
void previewWorkoutDescriptionChanged(QString value);
|
||||
void previewWorkoutTagsChanged(QString value);
|
||||
|
||||
void previewFitFile(const QString &filename, const QString &result, const QString &workoutName);
|
||||
|
||||
void stravaAuthUrlChanged(QString value);
|
||||
void stravaWebVisibleChanged(bool value);
|
||||
void pelotonAuthUrlChanged(QString value);
|
||||
|
||||
@@ -33,24 +33,122 @@
|
||||
overflow-x: none;
|
||||
margin: 0px;
|
||||
}
|
||||
.zoom-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(156, 39, 176, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
font-size: 12px;
|
||||
}
|
||||
.zoom-button:hover {
|
||||
background: rgba(156, 39, 176, 1);
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#1d2330">
|
||||
<table style="border-spacing: 0px">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="divcanvas" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="chartContainer">
|
||||
<table id="bothChartsTable" style="border-spacing: 0px; display: table;">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="divcanvas" class="chart-container" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
|
||||
<button class="zoom-button" onclick="toggleZoom('power')">📊</button>
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="divcanvasheart" class="chart-container" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
|
||||
<button class="zoom-button" onclick="toggleZoom('heart')">❤️</button>
|
||||
<canvas id="canvasheart"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div id="powerChartOnly" style="display: none;">
|
||||
<div id="divcanvasFull" class="chart-container" style="width:100vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
|
||||
<button class="zoom-button" onclick="toggleZoom('power')">📊</button>
|
||||
<canvas id="canvasFull"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="divcanvasheart" style="width:50vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
|
||||
<canvas id="canvasheart"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="heartChartOnly" style="display: none;">
|
||||
<div id="divcanvasheartFull" class="chart-container" style="width:100vw;height:100vh; background-color:white; border: 0px solid #aaa; overflow: hidden;">
|
||||
<button class="zoom-button" onclick="toggleZoom('heart')">❤️</button>
|
||||
<canvas id="canvasheartFull"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentZoomMode = {power: false, heart: false};
|
||||
let originalTimeRange = null;
|
||||
|
||||
function setChartDisplayMode(mode) {
|
||||
const bothChartsTable = document.getElementById('bothChartsTable');
|
||||
const powerChartOnly = document.getElementById('powerChartOnly');
|
||||
const heartChartOnly = document.getElementById('heartChartOnly');
|
||||
|
||||
// Hide all containers
|
||||
bothChartsTable.style.display = 'none';
|
||||
powerChartOnly.style.display = 'none';
|
||||
heartChartOnly.style.display = 'none';
|
||||
|
||||
// Show selected mode
|
||||
switch(mode) {
|
||||
case 0: // Both charts
|
||||
bothChartsTable.style.display = 'table';
|
||||
break;
|
||||
case 1: // Heart rate only
|
||||
heartChartOnly.style.display = 'block';
|
||||
break;
|
||||
case 2: // Power only
|
||||
powerChartOnly.style.display = 'block';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleZoom(chartType) {
|
||||
currentZoomMode[chartType] = !currentZoomMode[chartType];
|
||||
|
||||
if (currentZoomMode[chartType]) {
|
||||
// Enable zoom mode (-30s to +2min from now)
|
||||
setZoomMode(chartType, true);
|
||||
} else {
|
||||
// Disable zoom mode (show all data)
|
||||
setZoomMode(chartType, false);
|
||||
}
|
||||
}
|
||||
|
||||
function setZoomMode(chartType, enabled) {
|
||||
// This function will be extended to work with the actual chart instances
|
||||
// For now, it's a placeholder that the chart scripts can override
|
||||
console.log(`Zoom ${enabled ? 'enabled' : 'disabled'} for ${chartType} chart`);
|
||||
|
||||
if (window.toggleChartZoom) {
|
||||
window.toggleChartZoom(chartType, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from the QML WebView to change display mode
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.action === 'setChartDisplayMode') {
|
||||
setChartDisplayMode(event.data.mode);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with both charts visible
|
||||
setChartDisplayMode(0);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -57,8 +57,19 @@ function process_trainprogram(arr) {
|
||||
}
|
||||
|
||||
function process_arr(arr) {
|
||||
let ctx = document.getElementById('canvas').getContext('2d');
|
||||
let div = document.getElementById('divcanvas');
|
||||
// Try to get the active canvas - check all possible canvas IDs
|
||||
let ctx, div;
|
||||
if (document.getElementById('canvas') && document.getElementById('canvas').offsetParent !== null) {
|
||||
ctx = document.getElementById('canvas').getContext('2d');
|
||||
div = document.getElementById('divcanvas');
|
||||
} else if (document.getElementById('canvasFull') && document.getElementById('canvasFull').offsetParent !== null) {
|
||||
ctx = document.getElementById('canvasFull').getContext('2d');
|
||||
div = document.getElementById('divcanvasFull');
|
||||
} else {
|
||||
// Fallback to the first available canvas
|
||||
ctx = (document.getElementById('canvas') || document.getElementById('canvasFull')).getContext('2d');
|
||||
div = document.getElementById('divcanvas') || document.getElementById('divcanvasFull');
|
||||
}
|
||||
|
||||
let reqpower = [];
|
||||
let reqcadence = [];
|
||||
@@ -383,6 +394,122 @@ function process_arr(arr) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
// Global variables for zoom functionality
|
||||
var isZoomedPower = false;
|
||||
var isZoomedHeart = false;
|
||||
var currentTime = 0;
|
||||
var zoomUpdateIntervalPower = null;
|
||||
var zoomUpdateIntervalHeart = null;
|
||||
|
||||
// Function to toggle zoom mode
|
||||
window.toggleChartZoom = function(chartType, enabled) {
|
||||
if (chartType === 'power' && powerChart) {
|
||||
isZoomedPower = enabled;
|
||||
|
||||
if (enabled) {
|
||||
startZoomMode('power');
|
||||
} else {
|
||||
stopZoomMode('power');
|
||||
}
|
||||
} else if (chartType === 'heart' && window.heartChart) {
|
||||
isZoomedHeart = enabled;
|
||||
|
||||
if (enabled) {
|
||||
startZoomMode('heart');
|
||||
} else {
|
||||
stopZoomMode('heart');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function startZoomMode(chartType) {
|
||||
if (chartType === 'power' && powerChart) {
|
||||
// Update zoom range every 1 second to follow "now"
|
||||
zoomUpdateIntervalPower = setInterval(function() {
|
||||
updateZoomRange('power');
|
||||
}, 1000);
|
||||
|
||||
// Initial zoom setup
|
||||
updateZoomRange('power');
|
||||
} else if (chartType === 'heart' && window.heartChart) {
|
||||
// Update zoom range every 1 second to follow "now"
|
||||
zoomUpdateIntervalHeart = setInterval(function() {
|
||||
updateZoomRange('heart');
|
||||
}, 1000);
|
||||
|
||||
// Initial zoom setup
|
||||
updateZoomRange('heart');
|
||||
}
|
||||
}
|
||||
|
||||
function stopZoomMode(chartType) {
|
||||
if (chartType === 'power' && powerChart) {
|
||||
// Clear the interval
|
||||
if (zoomUpdateIntervalPower) {
|
||||
clearInterval(zoomUpdateIntervalPower);
|
||||
zoomUpdateIntervalPower = null;
|
||||
}
|
||||
|
||||
// Reset to show all data and restore original tick settings
|
||||
powerChart.options.scales.x.min = undefined;
|
||||
powerChart.options.scales.x.max = undefined;
|
||||
powerChart.options.scales.x.ticks.stepSize = undefined;
|
||||
powerChart.options.scales.x.ticks.maxTicksLimit = undefined;
|
||||
powerChart.update('none');
|
||||
} else if (chartType === 'heart' && window.heartChart) {
|
||||
// Clear the interval
|
||||
if (zoomUpdateIntervalHeart) {
|
||||
clearInterval(zoomUpdateIntervalHeart);
|
||||
zoomUpdateIntervalHeart = null;
|
||||
}
|
||||
|
||||
// Reset to show all data and restore original tick settings
|
||||
window.heartChart.options.scales.x.min = undefined;
|
||||
window.heartChart.options.scales.x.max = undefined;
|
||||
window.heartChart.options.scales.x.ticks.stepSize = undefined;
|
||||
window.heartChart.options.scales.x.ticks.maxTicksLimit = undefined;
|
||||
window.heartChart.update('none');
|
||||
}
|
||||
}
|
||||
|
||||
function updateZoomRange(chartType) {
|
||||
if (chartType === 'power' && powerChart && powerChart.data.datasets[0] && powerChart.data.datasets[0].data) {
|
||||
// Get the latest data point time (current time)
|
||||
let latestDataPoint = powerChart.data.datasets[0].data[powerChart.data.datasets[0].data.length - 1];
|
||||
if (!latestDataPoint) return;
|
||||
|
||||
currentTime = latestDataPoint.x;
|
||||
|
||||
// Set zoom range: -30s to +2min from current time
|
||||
let zoomStart = Math.max(0, currentTime - 30); // -30 seconds, but not below 0
|
||||
let zoomEnd = currentTime + 120; // +2 minutes
|
||||
|
||||
// Update chart scale with proper tick configuration for zoom
|
||||
powerChart.options.scales.x.min = zoomStart;
|
||||
powerChart.options.scales.x.max = zoomEnd;
|
||||
powerChart.options.scales.x.ticks.stepSize = 30; // 30 second intervals in zoom mode
|
||||
powerChart.options.scales.x.ticks.maxTicksLimit = 6; // Limit number of ticks
|
||||
powerChart.update('none');
|
||||
} else if (chartType === 'heart' && window.heartChart && window.heartChart.data.datasets[0] && window.heartChart.data.datasets[0].data) {
|
||||
// Get the latest data point time (current time)
|
||||
let latestDataPoint = window.heartChart.data.datasets[0].data[window.heartChart.data.datasets[0].data.length - 1];
|
||||
if (!latestDataPoint) return;
|
||||
|
||||
currentTime = latestDataPoint.x;
|
||||
|
||||
// Set zoom range: -30s to +2min from current time
|
||||
let zoomStart = Math.max(0, currentTime - 30); // -30 seconds, but not below 0
|
||||
let zoomEnd = currentTime + 120; // +2 minutes
|
||||
|
||||
// Update chart scale with proper tick configuration for zoom
|
||||
window.heartChart.options.scales.x.min = zoomStart;
|
||||
window.heartChart.options.scales.x.max = zoomEnd;
|
||||
window.heartChart.options.scales.x.ticks.stepSize = 30; // 30 second intervals in zoom mode
|
||||
window.heartChart.options.scales.x.ticks.maxTicksLimit = 6; // Limit number of ticks
|
||||
window.heartChart.update('none');
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
el = new MainWSQueueElement({
|
||||
msg: null
|
||||
|
||||
@@ -55,8 +55,19 @@ function process_trainprogram_heart(arr) {
|
||||
}
|
||||
|
||||
function process_arr_heart(arr) {
|
||||
let ctx = document.getElementById('canvasheart').getContext('2d');
|
||||
let div = document.getElementById('divcanvasheart');
|
||||
// Try to get the active canvas - check all possible canvas IDs
|
||||
let ctx, div;
|
||||
if (document.getElementById('canvasheart') && document.getElementById('canvasheart').offsetParent !== null) {
|
||||
ctx = document.getElementById('canvasheart').getContext('2d');
|
||||
div = document.getElementById('divcanvasheart');
|
||||
} else if (document.getElementById('canvasheartFull') && document.getElementById('canvasheartFull').offsetParent !== null) {
|
||||
ctx = document.getElementById('canvasheartFull').getContext('2d');
|
||||
div = document.getElementById('divcanvasheartFull');
|
||||
} else {
|
||||
// Fallback to the first available canvas
|
||||
ctx = (document.getElementById('canvasheart') || document.getElementById('canvasheartFull')).getContext('2d');
|
||||
div = document.getElementById('divcanvasheart') || document.getElementById('divcanvasheartFull');
|
||||
}
|
||||
|
||||
let reqpower = [];
|
||||
let reqcadence = [];
|
||||
@@ -218,24 +229,7 @@ function process_arr_heart(arr) {
|
||||
options: {
|
||||
animation: {
|
||||
onComplete: function() {
|
||||
if(saveScreenshot[1])
|
||||
return;
|
||||
saveScreenshot[1] = true;
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'savechart',
|
||||
content: {
|
||||
name: 'heart',
|
||||
image: heartChart.toBase64Image()
|
||||
}
|
||||
}, function(msg) {
|
||||
if (msg.msg === 'R_savechart') {
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 3);
|
||||
el.enqueue().catch(function(err) {
|
||||
console.error('Error is ' + err);
|
||||
});
|
||||
// Live charts should not auto-save during workout
|
||||
}
|
||||
},
|
||||
responsive: true,
|
||||
|
||||
@@ -933,6 +933,19 @@
|
||||
});
|
||||
}
|
||||
setTimeout(a, 0);
|
||||
|
||||
// Add global event listeners for any touch/click to enable dragging margins
|
||||
document.addEventListener('click', function(event) {
|
||||
if (typeof Android !== 'undefined' && Android.enableDraggingMargins) {
|
||||
Android.enableDraggingMargins();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('touchstart', function(event) {
|
||||
if (typeof Android !== 'undefined' && Android.enableDraggingMargins) {
|
||||
Android.enableDraggingMargins();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -36,6 +36,38 @@
|
||||
scrollbar-color: #444 #222;
|
||||
}
|
||||
|
||||
/* Quick action buttons that appear on the horizontal bar */
|
||||
.quick-actions {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
background-color: #4C70BF;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
opacity: 1;
|
||||
background-color: #5A7FDF;
|
||||
}
|
||||
|
||||
/* Show quick actions when bar is hovered or focused */
|
||||
.horizontal-bar:hover .quick-actions,
|
||||
.horizontal-bar:focus-within .quick-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Style scrollbar for webkit browsers */
|
||||
.horizontal-bar::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
@@ -395,6 +427,15 @@
|
||||
|
||||
<!-- Main horizontal metrics bar -->
|
||||
<div id="metrics-bar" class="horizontal-bar">
|
||||
<!-- Quick action buttons that appear on hover -->
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action-btn" onclick="Start()" title="Start/Pause">▶/⏸</button>
|
||||
<button class="quick-action-btn" onclick="Stop()" title="Stop">⏹</button>
|
||||
<button class="quick-action-btn autoresistance" onclick="AutoResistance()" title="Auto Resistance">🧲</button>
|
||||
<button class="quick-action-btn" onclick="toggleMetricSelector()" title="Select Metrics">⚙</button>
|
||||
<button class="quick-action-btn" onclick="toggleCompletePanel()" title="Full Controls">⋯</button>
|
||||
<button class="quick-action-btn" onclick="Close()" title="Close" style="color: red;">🗙</button>
|
||||
</div>
|
||||
<!-- The metrics will be populated dynamically by JavaScript -->
|
||||
</div>
|
||||
|
||||
@@ -449,14 +490,41 @@
|
||||
|
||||
// Toggle the controls panel when clicking on the metrics bar
|
||||
document.getElementById("metrics-bar").addEventListener("click", function(event) {
|
||||
// Enable dragging margins on any click
|
||||
if (typeof Android !== 'undefined' && Android.enableDraggingMargins) {
|
||||
Android.enableDraggingMargins();
|
||||
}
|
||||
|
||||
// Only toggle if the click was directly on the metrics bar, not on a button or another panel
|
||||
if (event.target.closest("#controls-panel") === null &&
|
||||
event.target.closest("#metric-selector") === null &&
|
||||
event.target.closest("#complete-panel") === null) {
|
||||
event.target.closest("#complete-panel") === null &&
|
||||
event.target.closest(".quick-actions") === null &&
|
||||
!event.target.classList.contains("quick-action-btn")) {
|
||||
toggleControlsPanel();
|
||||
}
|
||||
});
|
||||
|
||||
// Add touch events for better mobile experience with quick actions
|
||||
document.getElementById("metrics-bar").addEventListener("touchstart", function(event) {
|
||||
// Enable dragging margins on any touch
|
||||
if (typeof Android !== 'undefined' && Android.enableDraggingMargins) {
|
||||
Android.enableDraggingMargins();
|
||||
}
|
||||
|
||||
// Show quick actions on touch start for mobile devices
|
||||
var quickActions = document.querySelector(".quick-actions");
|
||||
if (quickActions) {
|
||||
quickActions.style.display = "flex";
|
||||
// Hide after 3 seconds if not interacted with
|
||||
setTimeout(function() {
|
||||
if (quickActions.style.display === "flex") {
|
||||
quickActions.style.display = "none";
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// Function to toggle controls panel
|
||||
function toggleControlsPanel() {
|
||||
var controlsPanel = document.getElementById("controls-panel");
|
||||
@@ -464,8 +532,16 @@
|
||||
|
||||
if (controlsPanel.style.display === "block") {
|
||||
controlsPanel.style.display = "none";
|
||||
// Restore window height when panel is hidden
|
||||
if (typeof Android !== 'undefined' && Android.restoreFloatingWindow) {
|
||||
Android.restoreFloatingWindow();
|
||||
}
|
||||
} else {
|
||||
controlsPanel.style.display = "block";
|
||||
// Expand window height when panel is shown (controls-panel height is approximately 150px)
|
||||
if (typeof Android !== 'undefined' && Android.expandFloatingWindow) {
|
||||
Android.expandFloatingWindow(150);
|
||||
}
|
||||
// Hide other panels
|
||||
metricSelector.style.display = "none";
|
||||
document.getElementById("complete-panel").style.display = "none";
|
||||
@@ -479,8 +555,16 @@
|
||||
|
||||
if (completePanel.style.display === "block") {
|
||||
completePanel.style.display = "none";
|
||||
// Restore window height when panel is hidden
|
||||
if (typeof Android !== 'undefined' && Android.restoreFloatingWindow) {
|
||||
Android.restoreFloatingWindow();
|
||||
}
|
||||
} else {
|
||||
completePanel.style.display = "block";
|
||||
// Expand window height when panel is shown (complete-panel needs more space, approximately 300px)
|
||||
if (typeof Android !== 'undefined' && Android.expandFloatingWindow) {
|
||||
Android.expandFloatingWindow(300);
|
||||
}
|
||||
// Hide regular controls panel
|
||||
controlsPanel.style.display = "none";
|
||||
}
|
||||
@@ -493,10 +577,18 @@
|
||||
|
||||
if (metricSelector.style.display === "block") {
|
||||
metricSelector.style.display = "none";
|
||||
// Restore window height when panel is hidden
|
||||
if (typeof Android !== 'undefined' && Android.restoreFloatingWindow) {
|
||||
Android.restoreFloatingWindow();
|
||||
}
|
||||
} else {
|
||||
// Populate the metric selector grid
|
||||
populateMetricSelector();
|
||||
metricSelector.style.display = "block";
|
||||
// Expand window height when panel is shown (metric-selector-panel height is approximately 200px)
|
||||
if (typeof Android !== 'undefined' && Android.expandFloatingWindow) {
|
||||
Android.expandFloatingWindow(200);
|
||||
}
|
||||
// Hide controls panel
|
||||
controlsPanel.style.display = "none";
|
||||
}
|
||||
@@ -546,11 +638,53 @@
|
||||
metricsPreference[item.dataset.metric] = true;
|
||||
});
|
||||
|
||||
// Save preferences to backend by updating tile settings
|
||||
var settingsToUpdate = {};
|
||||
|
||||
// Map horizontal bar preferences to tile settings
|
||||
settingsToUpdate['tile_speed_enabled'] = metricsPreference["speed"];
|
||||
settingsToUpdate['tile_pace_enabled'] = metricsPreference["pace"];
|
||||
settingsToUpdate['tile_inclination_enabled'] = metricsPreference["inclination"];
|
||||
settingsToUpdate['tile_elevation_enabled'] = metricsPreference["elevation"];
|
||||
settingsToUpdate['tile_cadence_enabled'] = metricsPreference["cadence"];
|
||||
settingsToUpdate['tile_calories_enabled'] = metricsPreference["calories"];
|
||||
settingsToUpdate['tile_jouls_enabled'] = metricsPreference["jouls"];
|
||||
settingsToUpdate['tile_odometer_enabled'] = metricsPreference["distance"];
|
||||
settingsToUpdate['tile_resistance_enabled'] = metricsPreference["resistance"];
|
||||
settingsToUpdate['tile_watt_enabled'] = metricsPreference["watt"];
|
||||
settingsToUpdate['tile_heart_enabled'] = metricsPreference["heart"];
|
||||
settingsToUpdate['tile_elapsed_enabled'] = metricsPreference["elapsed"];
|
||||
settingsToUpdate['tile_remainingtimetrainprogramrow_enabled'] = metricsPreference["rowremainingtime"];
|
||||
settingsToUpdate['tile_nextrowstrainprogram_enabled'] = metricsPreference["nextrow"];
|
||||
settingsToUpdate['tile_peloton_resistance_enabled'] = metricsPreference["pelotonresistance"];
|
||||
settingsToUpdate['tile_peloton_offset_enabled'] = metricsPreference["pelotonoffset"];
|
||||
settingsToUpdate['tile_gears_enabled'] = metricsPreference["gears"];
|
||||
settingsToUpdate['tile_ftp_enabled'] = metricsPreference["powerzone"];
|
||||
|
||||
// Send settings update to backend
|
||||
let el = new MainWSQueueElement({
|
||||
msg: 'setsettings',
|
||||
content: settingsToUpdate
|
||||
}, function (msg) {
|
||||
if (msg.msg === 'R_setsettings') {
|
||||
console.log('Horizontal bar preferences saved successfully');
|
||||
return msg.content;
|
||||
}
|
||||
return null;
|
||||
}, 15000, 1);
|
||||
|
||||
el.enqueue().catch(function (err) {
|
||||
console.error('Error saving horizontal bar preferences: ' + err);
|
||||
});
|
||||
|
||||
// Update the display
|
||||
updateHorizontalMetrics();
|
||||
|
||||
// Hide selector
|
||||
// Hide selector and restore window height
|
||||
document.getElementById("metric-selector").style.display = "none";
|
||||
if (typeof Android !== 'undefined' && Android.restoreFloatingWindow) {
|
||||
Android.restoreFloatingWindow();
|
||||
}
|
||||
}
|
||||
|
||||
// Create HTML for a horizontal metric
|
||||
@@ -1500,6 +1634,19 @@
|
||||
|
||||
// Initialize the template when the page loads
|
||||
setTimeout(initializeTemplate, 0);
|
||||
|
||||
// Add global event listeners for any touch/click to enable dragging margins
|
||||
document.addEventListener('click', function(event) {
|
||||
if (typeof Android !== 'undefined' && Android.enableDraggingMargins) {
|
||||
Android.enableDraggingMargins();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('touchstart', function(event) {
|
||||
if (typeof Android !== 'undefined' && Android.enableDraggingMargins) {
|
||||
Android.enableDraggingMargins();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
39
src/inner_templates/previewchart/.eslintrc.js
Normal file
39
src/inner_templates/previewchart/.eslintrc.js
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es2021': true,
|
||||
'commonjs': true,
|
||||
'es6': true,
|
||||
'jquery': true
|
||||
},
|
||||
'extends': 'eslint:recommended',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 12,
|
||||
//'sourceType': 'module'
|
||||
},
|
||||
'globals': {
|
||||
'host_url': true,
|
||||
'MainWSQueueElement': true,
|
||||
'Chart': true,
|
||||
'get_template_name': true
|
||||
},
|
||||
'rules': {
|
||||
'indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'windows'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
'no-unused-vars': ['off']
|
||||
}
|
||||
};
|
||||
3
src/inner_templates/previewchart/.jshintrc
Normal file
3
src/inner_templates/previewchart/.jshintrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"esversion": 6
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user