Compare commits

...

182 Commits

Author SHA1 Message Date
Roberto Viola
3a250fe3a3 Enhance Virtual Gearing: Android-only visibility and coordinate feedback
- Make Virtual Gearing Device visible only on Android platform
- Add customizable coordinate settings for different cycling apps (MyWhoosh, IndieVelo, etc.)
- Implement app selection ComboBox with auto-populated default coordinates
- Add real-time coordinate customization UI with percentage-based values
- Show toast messages with exact touch coordinates when gear changes occur
- Integrate settings bridge between Java and C++ for dynamic coordinate access
- Add debug logging for screen dimensions and coordinate calculations
- Replace hardcoded coordinates with user-configurable system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 08:10:00 +02:00
Roberto Viola
094d2c88cb adding new files 2025-09-24 11:58:43 +02:00
Roberto Viola
7f3dda70fd Update settings.qml 2025-09-22 13:30:17 +02:00
Roberto Viola
79d96ba182 fix 2025-09-22 08:29:07 +02:00
Roberto Viola
5ec861dab3 Merge branch 'virtualgear_device_android' of https://github.com/cagnulein/qdomyos-zwift into virtualgear_device_android 2025-09-22 04:57:15 +02:00
Roberto Viola
f90edbd632 log 2025-09-22 04:57:00 +02:00
Roberto Viola
621ed69627 Merge branch 'master' into virtualgear_device_android 2025-09-21 14:33:30 +02:00
Roberto Viola
b754b7f773 other files 2025-09-21 14:04:07 +02:00
Roberto Viola
efb9dfbdb1 virtual gear device on android 2025-09-21 14:01:01 +02:00
Roberto Viola
a40fec4082 adding LSApplicationCategoryType on iOS 2025-09-21 07:38:50 +02:00
Roberto Viola
f6a9d8ca4e removed gears gain changes from Wizard.qml 2025-09-18 16:30:25 +02:00
Roberto Viola
dd2bfc4e1b Update proformbike.cpp 2025-09-18 12:09:24 +02:00
Roberto Viola
06fd78378e Proform CSX210 2025-09-18 11:38:11 +02:00
Roberto Viola
f28574245c Update project.pbxproj 2025-09-18 09:58:38 +02:00
Roberto Viola
b964c523dd How to Make 10s Intervals Work with Virtual Shifting #3603 2025-09-18 09:50:06 +02:00
Roberto Viola
0721bc3ec5 Update project.pbxproj 2025-09-17 17:00:58 +02:00
Roberto Viola
3f783305b2 How to Make 10s Intervals Work with Virtual Shifting #3603 2025-09-17 17:00:09 +02:00
Roberto Viola
be29180e48 Update project.pbxproj 2025-09-17 12:22:46 +02:00
Roberto Viola
19c65d7d90 Taurua IC90 (#3697) 2025-09-17 12:20:21 +02:00
Roberto Viola
704c7f1f80 Kingsmith WalkingPad R3 Hybrid+ 2025-09-16 10:49:48 +02:00
Roberto Viola
678ac9d466 adding info for MAC 2025-09-16 10:00:15 +02:00
Roberto Viola
a8a6c5d736 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-16 09:39:33 +02:00
Roberto Viola
e8408710df adding MACCATALYST for ios 2025-09-16 09:39:13 +02:00
Roberto Viola
47825f0783 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-16 09:16:26 +02:00
Roberto Viola
f7ce518812 mobvoi se manual incline (Issue #3690) 2025-09-16 09:16:21 +02:00
Roberto Viola
f887a068b9 Power zone tiles not using erg mode (Issue #3681) 2025-09-15 13:49:29 +02:00
Roberto Viola
6ecbce4b87 adding KICKR RUN treadmill 2025-09-15 11:26:44 +02:00
Roberto Viola
9454d75f55 Update project.pbxproj 2025-09-12 10:39:04 +02:00
Roberto Viola
4063321418 Update project.pbxproj 2025-09-12 09:27:16 +02:00
Roberto Viola
bb88d58e47 Power zone tiles not using erg mode (Issue #3681) 2025-09-12 09:26:17 +02:00
Roberto Viola
7bc2f065c0 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-12 08:52:58 +02:00
Roberto Viola
c773b45ddf 2.20.11 2025-09-12 08:52:55 +02:00
Roberto Viola
eaf7db7813 Implement threaded FIT backup file writing (#3676)
* Implement threaded FIT backup file writing

- Add FitBackupWriter class to handle FIT file saving in background thread
- Move FIT backup writing from main thread to dedicated worker thread
- Use Qt's signal/slot mechanism with QueuedConnection for thread safety
- Similar implementation pattern to existing LogWriter threading
- Prevents UI blocking during FIT file saves every minute

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* build fix

* fix

* fix signal

* fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-11 20:46:36 +02:00
Roberto Viola
a29f6350d0 Update project.pbxproj 2025-09-11 12:39:53 +02:00
Roberto Viola
65ad925d37 How to Make 10s Intervals Work with Virtual Shifting (Discussion #3603) 2025-09-11 12:33:49 +02:00
Roberto Viola
8fd486d582 adding decimal places to current strokes length 2025-09-10 09:35:47 +02:00
Roberto Viola
8fa5dcadcb strokelength for Concept rower 2025-09-09 16:03:58 +02:00
Roberto Viola
6abf6c9cfd strokelength for Concept rower 2025-09-08 16:05:19 +02:00
Roberto Viola
b4603da714 Saris H3+ Slow to gear and resistance/incline changes on Zwift (Windows) + QZ on Android 16 (Pixel 8 Pro) #3660 2025-09-08 15:26:17 +02:00
Roberto Viola
b27e84de69 Update project.pbxproj 2025-09-05 11:44:02 +02:00
Roberto Viola
49337cbbc6 Update toorxtreadmill.h 2025-09-05 11:34:34 +02:00
Roberto Viola
fe2f5e923c Pafer treadmill #2985 2025-09-05 09:20:43 +02:00
Roberto Viola
69f54dbd54 Pafer treadmill #2985 2025-09-05 09:18:53 +02:00
Roberto Viola
bc20ec0d8f Option to Enable/Disable Haptic Feedback on Zwift Play Controllers (Issue #3669) 2025-09-05 08:44:45 +02:00
Roberto Viola
278add7a11 Setting resistance for skandika nordlys (Issue #3667) 2025-09-04 16:11:18 +02:00
Roberto Viola
6e90091883 Support for LSG Treadmills (Issue #3665) 2025-09-04 09:29:16 +02:00
Roberto Viola
ebda22d7b4 skandika nordlys 2025-09-03 16:20:01 +02:00
Roberto Viola
625ffb3932 skandika nordlys 2025-09-03 16:17:28 +02:00
Roberto Viola
fe6868911e Controlar intensidad de un workout aumentando o disminuyendo el porcentaje de FTP objetivo (Discussion #3664) 2025-09-03 10:28:57 +02:00
Roberto Viola
1c73d15377 webserverinfosender disconnect crash (#3661) 2025-09-01 15:20:50 +02:00
Roberto Viola
c33ee55efb Update homeform.cpp 2025-09-01 11:50:25 +02:00
Roberto Viola
56979a2122 Update homeform.cpp 2025-09-01 11:07:31 +02:00
Roberto Viola
3e1db8bfdf Support ROWING device type in writeProcess
mail "Question re QZ App" from Michael M. of 31/8/2025

Extended the writeProcess method to handle the ROWING device type in addition to BIKE. This allows the processor to support additional device types for characteristic writes.
2025-09-01 11:05:57 +02:00
Roberto Viola
10fdc52446 QZ & Peloton Sync Drift in Tread Classes (Issue #3624) 2025-09-01 10:56:41 +02:00
Roberto Viola
23d23c40a5 QZ & Peloton Sync Drift in Tread Classes (Issue #3624) 2025-09-01 09:54:59 +02:00
Roberto Viola
90e8eeb983 Update main.yml 2025-08-31 06:47:17 +02:00
Roberto Viola
dcf395ec46 Update main.yml 2025-08-29 16:06:13 +02:00
Roberto Viola
d55cb553d3 Add chart display mode setting with zoom controls (#3627)
Introduces a new 'chart_display_mode' setting allowing users to select between both charts, heart rate only, or power only in the chart footer. Updates QML and settings UI to support this option, and adds zoom buttons to each chart for focused time-range viewing. JavaScript logic is enhanced to handle dynamic chart display and zooming, including interval-based updates to the visible time window.
2025-08-29 15:32:52 +02:00
Roberto Viola
b862d26bc3 Multiple files in different instances Other Folder in Training Program List (#3651)
* Update TrainingProgramsList.qml

* Update TrainingProgramsList.qml

* did for gpx, profiles and settings
2025-08-29 10:35:37 +02:00
Roberto Viola
d5e4f11849 Active Calories (#3630)
* first commit

* Update AppDelegate.swift

* watchkit

* apex bike cadence updated

* adding something for debug

* Update project.pbxproj

* removing basal

* fixing

* build 1145

* Update project.pbxproj

* Add option to calculate calories from heart rate

Introduces a new setting to calculate calories based on heart rate data instead of power. Updates the bluetoothdevice logic to support HR-based calorie calculation, adds a new metric for HR calories, and exposes the option in the settings UI. Also updates QZSettings to include the new configuration key and default.

* build 1149

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Zwift erg mode workouts not functioning #3643

* Update project.pbxproj
2025-08-29 08:43:29 +02:00
Roberto Viola
5e9679f6c3 Merach W50 FTMS Treadmill 2025-08-29 08:40:25 +02:00
Roberto Viola
8799c447fb QZ not working with Taurus FX9.9 elliptical (Issue #3618) 2025-08-27 14:57:18 +02:00
Roberto Viola
bcdb767b7e adding compensation when there is a power sensor and an ergModeSupported bike (PR #3388) 2025-08-27 11:29:22 +02:00
Roberto Viola
15e208d34c Zwift erg mode workouts not functioning (Discussion #3643)
Improves logic for routing power requests to the bike, including handling of virtual bikes, ZwiftPlay, and external power sensors. Updates FTMS characteristic change handling to block simulation commands in resistance level mode and only allow power commands when no external power sensor is present.
2025-08-27 10:48:48 +02:00
Roberto Viola
f16c41e6dd Zwift erg mode workouts not functioning (Discussion #3643)
Refines the logic for routing FTMS power commands to the bike by considering the presence of an external power sensor and erg mode support. Now allows power commands through when no external power sensor is configured and erg mode is supported, even if resistance level mode is active. Adds more detailed debug output for easier troubleshooting.
2025-08-27 10:43:57 +02:00
Roberto Viola
9110c55cb1 Zwift erg mode workouts not functioning (Discussion #3643) 2025-08-27 10:39:17 +02:00
Roberto Viola
881e155cbc QZ & Peloton Sync Drift in Tread Classes #3624 2025-08-27 09:39:26 +02:00
Roberto Viola
e2d187a7bd Zwift erg mode workouts not functioning (Discussion #3643) 2025-08-26 14:57:11 +02:00
Roberto Viola
66821d884a Update main.yml 2025-08-26 13:54:32 +02:00
Roberto Viola
73ad1dc46c Add support for KS-HDSY-X21C treadmill variant
Introduces detection and handling for KS-HDSY-X21C devices, including new flags and GATT service/characteristic UUIDs. This improves compatibility with additional Kingsmith treadmill models.
2025-08-26 08:22:20 +02:00
Roberto Viola
c91a2d3ee5 Update main.yml 2025-08-25 14:13:00 +02:00
Roberto Viola
87c0e95b01 Update main.yml 2025-08-25 14:10:59 +02:00
Roberto Viola
174da2ac14 Update main.yml 2025-08-25 14:05:45 +02:00
Roberto Viola
b61ba37b8f Update main.yml 2025-08-25 14:05:07 +02:00
Roberto Viola
27333e7836 Constant low wattage regardless of resistance #3641 2025-08-25 10:48:32 +02:00
Roberto Viola
58a9e81bd8 peloton bike setting removed 2025-08-25 10:10:38 +02:00
Roberto Viola
d78e92f42f Connected JTX Fitness elliptical trainer but no data in QZ fitness panel (Issue #3638) 2025-08-23 16:18:15 +02:00
Roberto Viola
2a5eb7b057 Connected JTX Fitness elliptical trainer but no data in QZ fitness panel (Issue #3638) 2025-08-23 14:58:48 +02:00
Roberto Viola
ae5f70645a Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-08-23 14:48:33 +02:00
Roberto Viola
d26b14276e How to Make 10s Intervals Work with Virtual Shifting (Discussion #3603) 2025-08-23 14:47:42 +02:00
Roberto Viola
9166ce7218 removing tester android 14 2025-08-23 14:39:10 +02:00
Roberto Viola
5f0ec98b0c Update main.yml 2025-08-23 12:17:15 +02:00
Roberto Viola
1bc7af0a88 fix ios github actions (#3637) 2025-08-23 09:25:42 +02:00
Roberto Viola
df75d33ca6 Fix SQL linking in GitHub Actions by adding sql module to defaults.pri (#3635) 2025-08-22 15:20:51 +02:00
Roberto Viola
34f7df6bfb Kettler HOI Frame Connectivity (Issue #3636) 2025-08-22 15:06:48 +02:00
Roberto Viola
1208b439fa Concept2 RowERG PM5 and QZ not getting metrocs (Issue #3625) (#3626) 2025-08-21 06:58:33 +02:00
Roberto Viola
14a9faa2ee apex bike cadence updated 2025-08-20 15:47:34 +02:00
Roberto Viola
ca4fb0b35e technogym trainer 2025-08-19 15:25:29 +02:00
Roberto Viola
6ea6e6d9b2 Update project.pbxproj 2025-08-18 16:03:25 +02:00
Roberto Viola
2e17aa40ec apex bike table updated 2025-08-18 15:14:59 +02:00
Roberto Viola
098392684f 2.20.8 2025-08-17 15:28:24 +02:00
Roberto Viola
6678e225c5 Fixing Trainprogram Timer Jitter (#1849)
* fixing

* Update trainprogram.cpp

* Update trainprogram.cpp

* Update trainprogram.cpp
2025-08-17 15:17:24 +02:00
Roberto Viola
ca0bd15e69 virtual rower on nordictrackifitadbrower 2025-08-17 06:23:11 +02:00
Roberto Viola
1675240f13 fix wrong wattage for proform_bike_PFEVEX71316_0 2025-08-16 16:37:57 +02:00
Roberto Viola
b21c6325bb rebook bike fix for treadmill 2025-08-16 16:03:41 +02:00
Roberto Viola
b2f9e3d754 Update project.pbxproj 2025-08-15 16:33:48 +02:00
Roberto Viola
6dc5d74de3 proform_bike_PFEVEX71316_0 (Issue #3448)
mail QZ control of Proform TDF 1.0 with Zwift
from steve b. 12/08/2025
2025-08-15 16:06:10 +02:00
Roberto Viola
be560aae89 apex bike watt table improved 2025-08-15 15:52:24 +02:00
Roberto Viola
37858ca972 Removing Huge font from android (#3616)
* removing font

* fixing

* Update fontmanager.h

* fixing
2025-08-14 16:15:17 +02:00
Roberto Viola
d3a1a2aafb QZ not working with Taurus FX9.9 elliptical (Issue #3618) 2025-08-14 12:01:45 +02:00
Roberto Viola
49b890715e Update project.pbxproj 2025-08-13 16:28:22 +02:00
Roberto Viola
f19449107b fixing inclinationResistanceTable with gears 2025-08-13 16:25:14 +02:00
Roberto Viola
8bbed4fa76 Update project.pbxproj 2025-08-13 15:26:11 +02:00
Roberto Viola
efc4950f92 QZ not working with Taurus FX9.9 elliptical (Issue #3618) 2025-08-13 15:12:19 +02:00
Roberto Viola
23fd13ad0c Matrix A50 XR elliptical 2025-08-13 14:56:31 +02:00
Roberto Viola
64cd90dfaa let's push to the release even if the linux builds are not ok 2025-08-12 16:38:44 +02:00
Roberto Viola
ec5919d67f Update main.yml 2025-08-12 06:40:15 +02:00
Roberto Viola
0fc9d7fb40 apex bike watt table improved 2025-08-12 06:35:38 +02:00
Roberto Viola
4f03554fbb fixing crash on mac 2025-08-11 07:07:13 +02:00
Roberto Viola
4a16605f43 auto start peloton now starts the QZ session too if in pause mode 2025-08-10 17:47:09 +02:00
Roberto Viola
3ae60e1c41 Adding libqt5sql5-sqlite to github actions 2025-08-10 07:17:15 +02:00
Roberto Viola
edab888e31 fixing sql github actions for linux? 2025-08-10 06:31:21 +02:00
Roberto Viola
2eefcee9b7 fixing negative calories on apple health without apple watch 2025-08-10 06:23:49 +02:00
Roberto Viola
c1db263dcf Sole F63 Cannot get stable connection or auto incline (Issue #3606) 2025-08-09 06:52:17 +02:00
Roberto Viola
cdf0d34b86 fixing crash pressing start when a device is not connected 2025-08-08 10:55:39 +02:00
Roberto Viola
eb0528215b Update project.pbxproj 2025-08-08 08:51:53 +02:00
Roberto Viola
30d0940359 2.20.7 2025-08-08 08:48:05 +02:00
Roberto Viola
9f7cdd8b42 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-08-08 08:47:06 +02:00
Roberto Viola
af00334455 Handle negative resistance in forceResistance
Adds a check for negative resistance values in forceResistance. If a negative value is detected, it logs a debug message and sets the resistance to a fallback value of 1 to prevent invalid input.
2025-08-08 08:40:10 +02:00
Roberto Viola
4e8af61539 Workout history and start of previous trainings #2383 2025-08-07 12:31:18 +02:00
Roberto Viola
a17b78c56b Revert "Update WorkoutsHistory.qml"
This reverts commit d59eabc9b3.
2025-08-07 12:20:28 +02:00
Roberto Viola
8f536f487e Update project.pbxproj 2025-08-07 12:11:27 +02:00
Roberto Viola
82cad601bf Update project.pbxproj 2025-08-07 11:47:27 +02:00
Roberto Viola
a3579c42fa fixing crash without apple watch 2025-08-07 11:46:55 +02:00
Roberto Viola
9af0046554 Nordictrack RW900 rower v1 2025-08-07 11:31:16 +02:00
Roberto Viola
d59eabc9b3 Update WorkoutsHistory.qml 2025-08-07 09:34:19 +02:00
Roberto Viola
d8d55cfbf8 fixing linux builds 2025-08-07 09:07:08 +02:00
Roberto Viola
bce3f3cef3 adding sql dependencies 2025-08-06 16:13:21 +02:00
Roberto Viola
e2d5e602e1 saving training session even outside peloton 2025-08-06 15:35:03 +02:00
Roberto Viola
054087a3bf Update main.yml 2025-08-06 14:11:30 +02:00
Roberto Viola
123d1f9634 Update 10_Installation.md 2025-08-06 14:10:25 +02:00
Roberto Viola
9130cabc65 adding sql for linux in the github actions 2025-08-06 14:09:27 +02:00
Roberto Viola
3c893444e6 Update project.pbxproj 2025-08-06 13:45:21 +02:00
Roberto Viola
24935046e9 added cruise, sprint and climb profile for automatic shifting 2025-08-06 13:41:30 +02:00
Roberto Viola
ecf596623e Workout history and healthkit without apple watch (#3594)
* preparing form...

* workout history works with a bluetooth device connected

* using a different template for the  preview charts

* sport type added to preview function

* build fixed

* added target cadence, watt and resistance to fit file along with user info

* Update WorkoutTracking.swift

* building

* Update WorkoutTracking.swift

* Update lockscreen.mm

* doing

* Update lockscreen.mm

* Update WorkoutTracking.swift

* Update homeform.cpp

* Update WorkoutTracking.swift

* Update WorkoutTracking.swift

* seems working

* Update project.pbxproj

* Update project.pbxproj

* fixing speed

* adding metrics also when the virtualbike is not the zwift interface

* adding device type

* fix build

* let's work on build up the list

* emitting signal not tested

* connection works

* Update project.pbxproj

* fix build issue, forcing bike

* adding kcal

* fix build

* Update project.pbxproj

* fix build

* fake bike to test

* fixing crash and metrics

* Update WorkoutTracking.swift

* Update project.pbxproj

* adding logs

* improving logs

* Update project.pbxproj

* fixing

* adding fit file processor

* the workout history works with the db!

* kind of works on ios

* data fixed

* removed workoutdetails because db would be too heavy. let's open the fit files

* preview of the fit file is almost ready

* details start to work!

* adding kcal on the summary

* Update bluetooth.cpp

* adding check that apple watch is available

* Update homeform.cpp

* fixing build and tested

* Update project.pbxproj

* fixing crash?

* fake treadmill simulatoion on the simulator

* Update homeform.cpp

* Update project.pbxproj

* Update lockscreen.mm

* Update project.pbxproj

* BT Log share for LifeSpan-TM-2000 #3021

* adding steps for treadmills

* NOT TESTED handling device type

* fixing whitespaces

* fixing build

* fixing build on xcode

* fixed distance issue and steps

* Update project.pbxproj

* fixing high steps

https://github.com/cagnulein/qdomyos-zwift/discussions/3277#discussioncomment-12425461

* Update project.pbxproj

* fix build

* fix build

* build fix

* build fix

* claude fixes

* it kind of works

* improving

* fixing summary

* optimized!

* rogue bike fix

* Update project.pbxproj

* Update qfit.cpp

* Update qfit.cpp

* Update project.pbxproj

* Update qdomyos-zwift-tests.pro

* fix build

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update virtualrower.cpp

* restoring changes

* Update lockscreen.mm

* fixing build

* Update project.pbxproj

* removing save from the dochart in the preview

* Update dochartliveheart.js

* Update project.pbxproj

* reducing logging

* Update Server.swift

* Update WorkoutsHistory.qml

* Update project.pbxproj

* adding fit file processor right after the workout stopped

* streak message

* Update bluetooth.cpp

* Update fitdatabaseprocessor.cpp

* Update WorkoutsHistory.qml

* 2.20.6

* adding font for emoji on android

* Update settings.qml

* Update WorkoutsHistory.qml

* html android emoji font

* workout calendar

* calendar with points work!

* peloton link and download from the workout history

* fix point in the calendar

* Update project.pbxproj

* fixing

* fixing

* fixing

* fixing and debug

* Update qfit.cpp

* Update WorkoutsHistory.qml

* Update WorkoutsHistory.qml

* Update WorkoutsHistory.qml

* Update project.pbxproj
2025-08-06 11:26:56 +02:00
Roberto Viola
620be36635 first version of automatic virtual shifting 2025-08-04 14:48:35 +02:00
Roberto Viola
3c98edfb6d Start the activity from the treadmill's start button #3590 2025-08-04 08:59:46 +02:00
Roberto Viola
b0c4690489 Start the activity from the treadmill's start button (Issue #3590) 2025-08-04 08:58:18 +02:00
Roberto Viola
64f1fce8c8 Tunturi T80 QZ doesn't start when I start via the treadmill start button (Issue #3568) 2025-08-04 08:39:35 +02:00
Roberto Viola
f2df53b94b 2.20.5 2025-08-01 08:11:03 +02:00
Roberto Viola
ae4aec68c6 Peloton Walking Pace Targets #3550 2025-08-01 08:10:07 +02:00
Roberto Viola
68696a585a Peloton Walking Pace Targets #3550 2025-07-31 08:43:11 +02:00
Roberto Viola
ee0279186a Improve gear boundary handling with smart clamping
Refines the logic for setting gears to allow clamping to valid ranges when starting from invalid states, preventing the system from getting stuck below minimum gears due to fractional gains. Maintains normal rejection behavior when already at valid gear boundaries, and adds detailed debug output for each case.

Wahoo kickr core and Zwift Play And Fulgaz #3575
2025-07-29 13:57:32 +02:00
Roberto Viola
60c4747a0e Tunturi T80 QZ doesn't start when I start via the treadmill start button (Issue #3568) (#3569) 2025-07-27 21:08:26 +02:00
Roberto Viola
23e2202bc0 Update project.pbxproj 2025-07-27 14:26:34 +02:00
Roberto Viola
e9c2ed8a76 VANRYSEL_HT Kcal issue fixed
mail from Sina M. 27/7/2025
2025-07-27 14:12:36 +02:00
Roberto Viola
9b9174b45a fixing crash
mail from Barry W. 27/7/2025
2025-07-27 14:10:44 +02:00
Roberto Viola
e9451c1c76 build 1126 was already submitted 2025-07-25 00:11:41 +02:00
Roberto Viola
28bd6423b7 PELOTON: removing frequent /me calling 2025-07-25 00:03:48 +02:00
Roberto Viola
083fe13ce3 adding watt step for pid hr for bikes 2025-07-24 23:07:59 +02:00
Roberto Viola
574a78ba0b ApexBike: resistance and wattage fixed 2025-07-23 22:42:30 +02:00
Roberto Viola
54177f927e ApexBike: resistance and wattage fixed 2025-07-23 16:17:27 +02:00
Roberto Viola
a9c0a23f0a Peloton Walking Pace Targets (Issue #3550) 2025-07-23 08:52:16 +02:00
Roberto Viola
5f92401c98 ANDROID: fix crash on exit 2025-07-22 18:36:50 +02:00
Roberto Viola
2d959a580f ignoring frames for solebikes 2025-07-22 09:02:08 +02:00
Roberto Viola
cc046278fd Techogym group cycle (Issue #3479) (#3497)
* Update BikeChannelController.java

* Update BikeChannelController.java

* Update BikeChannelController.java

* Update bluetooth.cpp

* let's commenting the other profiles for now. then i will need to add a settings for them

* finalazing

* Add ANT+ bike device number configuration support

Introduces a new setting for specifying the ANT+ bike device number, allowing users to select a specific device or use auto-detection (0). Updates Java, C++, and QML code to pass and handle this parameter throughout the ANT+ bike connection workflow, and adds the setting to the UI and settings management.

* fixint UI and antstart

* ANT Heart Device ID

* wizard fixed
2025-07-22 08:55:58 +02:00
Roberto Viola
af82f731cf 2.20.3
#3543
2025-07-21 13:23:41 +02:00
Roberto Viola
a9ff106e54 android: fixing toast and left padding on landscape 2025-07-21 12:51:58 +02:00
Roberto Viola
8e2cf858b9 Wahoo bike: setGears in the init is not required and it also could lead to misalignments
mail from D. Wickham on 18/07/2025
2025-07-19 16:47:36 +02:00
Roberto Viola
d19eee81b3 Peloton Walking Pace Targets (Issue #3550) (#3551)
* Peloton Walking Pace Targets (Issue #3550)

* adding min speed, 0 can't be a right speed
2025-07-18 11:42:35 +02:00
Roberto Viola
9f5a2ae120 DeerRun Treadmill integration #2621 (#3547) 2025-07-17 20:37:17 +02:00
Roberto Viola
6bb520a0a9 2.20.2 2025-07-17 11:55:10 +02:00
Roberto Viola
5031e01e00 Horizontal floating bar issues (Issue #3513) (#3515)
* to test

* Add dynamic floating window resizing and drag timeout

Introduces a JavaScript interface to allow the floating window to expand or restore its height from the HTML UI, enabling panels to request more space as needed. Adds a temporary drag mode with a 5-second timeout for improved touch interaction, and updates the HTML to coordinate window resizing and visual feedback with the Android service.

* margin fixed

* fixing margin
2025-07-17 11:28:45 +02:00
Roberto Viola
4ae0c5c638 Peloton vs QZ Rower Pace Targets #3533 2025-07-17 09:34:24 +02:00
Roberto Viola
fb45b52341 Rename nordictrack APK in build workflow
After building the APK, the workflow now renames android-debug.apk to android-debug-nordictrack.apk. This helps distinguish the NordicTrack build artifact in the output directory.
2025-07-17 09:21:27 +02:00
Roberto Viola
156ea9e7ae Add software check for ERG mode support in bike classes
Email Lower target-resistance values from 15/07/2025

Introduces ergModeSupportedAvailableBySoftware() to the bike base class and overrides it in ftmsbike to always return true. Updates homeform.cpp to use the new software-based check instead of the hardware-based one for ERG mode support logic.
2025-07-16 12:02:34 +02:00
Roberto Viola
1b7e86481b Revert "Improve resistance estimation logic for inclinations"
This reverts commit 122ff3e25f.
2025-07-16 10:48:46 +02:00
Roberto Viola
e11d6d7f6a Refactor resistanceFromPowerRequest to use ergTable
Unified the resistanceFromPowerRequest logic across multiple bike device classes by delegating to ergTable::resistanceFromPowerRequest. This reduces code duplication and centralizes the resistance calculation based on power, cadence, and max resistance. Device-specific logic is preserved where necessary, such as in proformbike.

ASCEND S2 BIKE + QDOYMOS / Virtual machine (Issue #3419)
2025-07-16 09:00:00 +02:00
Roberto Viola
c72759c70a Improved followPowerBySpeed (#3506) 2025-07-15 16:42:39 +02:00
Roberto Viola
38570d855e Update project.pbxproj 2025-07-15 14:04:35 +02:00
Roberto Viola
122ff3e25f Improve resistance estimation logic for inclinations
Mail: Lower target-resistance values 15/07/2025

Refactored estimateResistance to sort data points, handle edge cases for inclinations below minimum and above maximum, and ensure interpolation uses sorted data. The method now returns the lowest or highest resistance for out-of-range inclinations and improves clarity and robustness of the estimation process.
2025-07-15 12:39:40 +02:00
Roberto Viola
1d48c42aa4 Update trxappgateusbbike.cpp 2025-07-15 11:45:12 +02:00
Roberto Viola
daae5659cf TRXAPPGATEUSBBIKE: Apply gain and offset to cadence calculation
Added retrieval of cadence_gain and cadence_offset from settings and applied them to the cadence value in GetCadenceFromPacket. This allows dynamic adjustment of cadence readings based on user or device configuration.
2025-07-15 11:04:12 +02:00
Roberto Viola
7c11ff324f Update project.pbxproj 2025-07-14 12:16:03 +02:00
Roberto Viola
e4beee9baf ASCEND S2 BIKE + QDOYMOS / Virtual machine (Issue #3419) 2025-07-14 11:31:54 +02:00
Roberto Viola
815d8758b0 Fix Android header positioning under status bar (#3540)
* Fix Android header positioning under status bar

- Remove fullscreen flags from CustomQtActivity to allow normal window mode
- Add dynamic top padding to main.qml header toolbar for Android
- Use Screen.height - Screen.desktopAvailableHeight for proper status bar compensation
- Maintains fullscreen QML visibility while preventing header overlap

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* seems ok

* creating nomedia file for the gallery

* Update Android emulator permissions for comprehensive app testing

- Added comprehensive permission grants for all Android API levels (24-36)
- Includes Bluetooth permissions for modern Android versions (12+)
- Added storage, camera, audio, and network permissions
- Configured app ops for special permissions (MANAGE_EXTERNAL_STORAGE, SYSTEM_ALERT_WINDOW)
- All permissions use || true to handle API compatibility gracefully

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* trying to reduce the gap

* could be ok?

* fixed orientation

* 2.20.1

* Update main.yml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-14 10:05:28 +02:00
Roberto Viola
43fc6f795d Update project.pbxproj 2025-07-12 14:12:18 +02:00
Roberto Viola
075c316bfa fixing gears immediately in the ftms bike (Issue #3511) 2025-07-12 14:02:16 +02:00
Roberto Viola
1ff42c9658 PELOTON: ignoring time diff in case of bootcamps 2025-07-12 08:14:58 +02:00
Roberto Viola
2add1a9425 Update project.pbxproj 2025-07-11 15:15:00 +02:00
Roberto Viola
0fd7f40412 Feierdun elliptical 2025-07-11 15:09:04 +02:00
Roberto Viola
7e0604032a APEX BIKE Fix distance parsing and add speed/cadence timeout reset
Mail from Paul E. from 10/07/2025

Corrects the distance data extraction in
characteristicChanged by using the correct byte indices and value check. Adds logic to reset speed and cadence to zero if no new data is received within 2 seconds, improving data accuracy during communication timeouts.
2025-07-11 10:07:38 +02:00
154 changed files with 12296 additions and 2011 deletions

View File

@@ -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
View File

@@ -1,3 +1,5 @@
src/qdomyos-zwift.pro.user
.idea/
src/Makefile

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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")
}
}
}

View File

@@ -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

View File

@@ -10,7 +10,7 @@ These instructions build the app itself, not the test project.
```buildoutcfg
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev 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/

View File

@@ -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 + ");
}
");
}
}
}

View File

@@ -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
View 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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View 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
}
}

View File

@@ -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" />

View 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>

View 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" />

View File

@@ -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");

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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();
}
});
}
}
}

View File

@@ -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:

View File

@@ -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);
}

View 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();
}
}

View 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
View 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
View 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

View File

@@ -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);

View File

@@ -31,7 +31,7 @@ CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003:
}
double CharacteristicWriteProcessor0003::currentGear() {
if(zwiftGearReceived)
if(zwiftGearReceived || !((bike*)Bike))
return currentZwiftGear;
else
return ((bike*)Bike)->gears();

View File

@@ -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)

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -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()));
}

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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;
}
{

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View 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;
}

View 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

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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)

View File

@@ -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()));
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
View 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
View 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

View 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();
}

View 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
View 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
View 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

Binary file not shown.

View File

@@ -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";

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View 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']
}
};

View File

@@ -0,0 +1,3 @@
{
"esversion": 6
}

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