Compare commits

...

478 Commits

Author SHA1 Message Date
Roberto Viola
9e1537caad 2.20.12 2025-11-05 10:07:38 +01:00
Roberto Viola
9fe72d13c0 Support for Proform Trainer 9.0 (PFTL69921-INT.4) (Issue #144)
https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/144#issuecomment-3490007042
2025-11-05 09:58:09 +01:00
Roberto Viola
df5e80a5be Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-05 09:39:30 +01:00
Roberto Viola
3b751d44e6 Treadmill incline multiplied on QZ output [BUG] #2511
BH Vanquish II no regula la velocidad automáticamente #3839
2025-11-05 09:39:24 +01:00
Roberto Viola
3815e45107 PitPat-T01 treadmill #3589 (#3737)
* PitPat-T01 treadmill #3589

* fixing

* wait for a packet and init from 0

* Update deerruntreadmill.cpp

* Update deerruntreadmill.cpp

* new xor

* stop command handled

* minspeedstep handled

* start and stop?

* start and stop

* Update deerruntreadmill.cpp
2025-11-05 08:25:12 +01:00
Roberto Viola
580eb3f092 Create libc++_shared.so 2025-11-04 17:02:41 +01:00
Roberto Viola
aba59cd136 Update peloton.h 2025-11-04 17:02:20 +01:00
Roberto Viola
369fbc4bc0 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-04 17:01:42 +01:00
Roberto Viola
871e704852 resistance for elliptical in the summary must be not the peloton resistance 2025-11-04 14:44:03 +01:00
Roberto Viola
b574e86804 cadence for elliptical in apple health must divide by 2 2025-11-04 14:43:44 +01:00
Roberto Viola
91735a714b Update project.pbxproj 2025-11-04 12:48:51 +01:00
Roberto Viola
da3b5b168e Trying to figure out how to best use this, monitor vs control (Discussion #3834) 2025-11-04 12:47:47 +01:00
Roberto Viola
d339cd461d Cadence and Wattage no responding the right way in Zwift (using a Elite Drivo 2) (Issue #3767) 2025-11-04 12:05:03 +01:00
Roberto Viola
f5ac438905 Update project.pbxproj 2025-11-04 11:42:42 +01:00
Roberto Viola
073b331535 Trying to figure out how to best use this, monitor vs control (Discussion #3834) 2025-11-04 11:40:49 +01:00
Roberto Viola
184c99ff6c Bkool smart bike (Issue #3774) 2025-11-04 11:35:06 +01:00
Roberto Viola
b25f7acf20 Support for Proform Trainer 9.0 (PFTL69921-INT.4)
https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/144#issuecomment-3482194626
2025-11-04 11:11:33 +01:00
Roberto Viola
cb0df3ae27 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-04 10:56:20 +01:00
Roberto Viola
7f9ffb7e0e heart rate monitors battery levels (Issue #3833) 2025-11-04 10:55:49 +01:00
Roberto Viola
dd104da06d Update project.pbxproj 2025-11-04 10:37:54 +01:00
Roberto Viola
9440089d05 Trying to figure out how to best use this, monitor vs control (Discussion #3834) 2025-11-04 10:35:51 +01:00
Roberto Viola
39b6ad7463 Peloton and Yesoul resistance numbers the same #3729 2025-11-04 09:46:13 +01:00
Roberto Viola
fdc548fda7 Matrix FTMS Bikes not showing any data (Issue #3837) 2025-11-04 09:40:57 +01:00
Roberto Viola
cc85cbba4f Update project.pbxproj 2025-11-03 16:03:23 +01:00
Roberto Viola
7576a77cd8 Add sanity check for cadence calculation
Cadence is now only updated if the 5-second average is below 200, preventing unrealistic values from being set due to erroneous stride data.
2025-11-03 15:53:48 +01:00
Roberto Viola
61c633474a iOS Apple Health: elliptical as cycling speed and distance 2025-11-03 14:44:29 +01:00
Roberto Viola
69aefc0b30 Domyos 900 + garmin fenix = ANT+ FTMS profile ( smart trainer bike) (Issue #3835) 2025-11-03 14:01:32 +01:00
Roberto Viola
19ca844968 Update qdomyos-zwift.pri 2025-11-03 13:40:40 +01:00
Roberto Viola
5bb3a808a1 ftmselliptical: Refactor cadence calculation logic
Introduces an instantCadence metric to store the immediate cadence value and updates Cadence to use a 5-second average. This improves the accuracy and stability of cadence reporting in ypooelliptical.
2025-11-03 13:15:06 +01:00
Roberto Viola
63cedd457d Update project.pbxproj 2025-11-03 12:11:31 +01:00
Roberto Viola
d9925ac780 SCH_411_510E cadence overflow 2025-11-03 12:11:03 +01:00
Roberto Viola
e4a71e2940 Improve cadence calculation using stride count
Email: Schwinn 411/510e compatibility 03/11/2025

Cadence is now calculated from stride count differences when no external cadence sensor is present, providing more accurate RPM values. Added tracking for last stride count and timestamp to support this calculation.
2025-11-03 12:06:22 +01:00
Roberto Viola
d66d2fd915 Add support for FS-YK Bluetooth bike model
Recognizes devices with names starting with 'FS-YK-' and sets up model-specific flags and behavior. FS-YK bikes are now detected and marked as not supporting ERG mode natively.
2025-11-03 11:37:17 +01:00
Roberto Viola
d20f651672 Add option to force virtual treadmill mode
Introduces a new setting 'virtual_device_force_treadmill' allowing rower devices to be presented as treadmills to client apps. Updates the settings UI, QZSettings class, and device logic to support this feature.
2025-11-03 10:03:18 +01:00
Roberto Viola
19a564d832 Domyos bike 900 disconnecting[BUG] (Issue #3829) 2025-11-03 08:40:15 +01:00
Roberto Viola
5e100f8857 Enable manual workflow dispatch for nordictrack-build job
Modified the nordictrack-build job condition to allow manual triggering
in addition to scheduled runs.

Changes:
- Updated job condition from 'schedule' only to 'schedule || workflow_dispatch'
- This allows users to manually trigger the nordictrack build from GitHub Actions UI

Now the nordictrack-build job can be triggered:
1. Automatically: Every night at midnight (cron schedule)
2. Manually: Via GitHub Actions "Run workflow" button

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:16:46 +01:00
Roberto Viola
5491007a2a Add NordicTrack Rower variant to GitHub Actions workflow
Extended the nordictrack-build job to include a third variant for rower
devices, in addition to existing treadmill and bike variants.

Changes:
- Added rower variant to matrix strategy with proform_rower_ip setting
- Updated artifact list to include nordictrack-rower-android-trial APK
- Added rower description in release notes section

The workflow will now build three specialized APKs:
- android-debug-nordictrack-treadmill.apk (nordictrack_2950_ip)
- android-debug-nordictrack-bike.apk (tdf_10_ip)
- android-debug-nordictrack-rower.apk (proform_rower_ip) [NEW]

All three variants use the same nordictrack-build-grpc branch (PR #3478)
and set their respective IP settings to "localhost" for gRPC communication.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:12:57 +01:00
Giuseppe Macario
028b5b4c4a typos and grammar (#3768)
* Update HomeForm.ui.qml

grammar

* Update settings.qml

typos
2025-11-02 15:19:33 +01:00
Roberto Viola
8eb0083897 Add SCH_411_510E to power divisor exception list
SCH_411_510E devices now use a divisor of 1.0 for instant power calculation, aligning with other listed models. This change ensures correct power readings for SCH_411_510E ellipticals.
2025-11-02 14:52:52 +01:00
Roberto Viola
89ed87ecb1 Update project.pbxproj 2025-11-02 14:19:15 +01:00
Roberto Viola
eac18d7a51 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-02 14:18:15 +01:00
Roberto Viola
d9a50973cd Improve cadence and power calculation for SCH_411_510E
Cadence is now calculated from speed for SCH_411_510E devices when step count is unavailable and the cadence sensor is disabled. Instant power calculation logic is updated to always process when the flag is set, and SCH_411_510E is excluded from the DOMYOS power calculation branch.
2025-11-02 14:18:11 +01:00
Roberto Viola
39c33f3ebc Make SCH_290R behave like hammer_racer_s in FTMS bike
Add SCH_290R device support to FTMS bike with same behavior as hammer_racer_s.
The device will subscribe only to FTMS service and use the same initialization logic.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 12:15:28 +01:00
Roberto Viola
2858468a04 Update project.pbxproj 2025-11-01 21:16:52 +01:00
Roberto Viola
1bbbda4efb Technogym ride 2025-11-01 21:16:21 +01:00
Roberto Viola
49fc672538 Technogym ride 2025-11-01 21:08:18 +01:00
Roberto Viola
d93107e286 Update project.pbxproj 2025-11-01 20:00:38 +01:00
Roberto Viola
08af7d61a5 rouvy dircon fixed! 2025-11-01 19:07:54 +01:00
Roberto Viola
e25dfc2354 Add debug logging and update slope change handling
Added debug output for requested slope changes in BLEPeripheralManagerTreadmillZwift and corrected indentation. In virtualtreadmill.cpp, restored lastSlopeChanged assignment after heart rate update to ensure proper slope change timing.
2025-11-01 15:28:07 +01:00
Roberto Viola
a4a4d1b9c5 App closes after Bluetooth connection (Issue #3807) 2025-10-31 20:37:34 +01:00
Roberto Viola
5761865916 Only able to control resistance or incline at once (Issue #3798) (#3813) 2025-10-31 13:22:23 +01:00
Roberto Viola
5be7b8530e Add SCH_290R to recognized Bluetooth devices
Included support for devices with names starting with SCH_290R in the device discovery logic to ensure proper recognition and handling.
2025-10-31 10:40:29 +01:00
Roberto Viola
fe44490ad9 Update project.pbxproj 2025-10-31 08:19:52 +01:00
Roberto Viola
97138d8492 Add FTMS bike filter to device discovery
Device discovery now checks if the FTMS bike list contains the default FTMS bike before proceeding. This adds an additional filter to improve device selection accuracy.
2025-10-31 08:14:12 +01:00
Roberto Viola
76845d5507 Toputure TEB1 (Issue #3814)
Updated forceResistance and deviceDiscovered methods to handle SPORT01 devices. Resistance is now initialized to 1 for SPORT01, ensuring correct behavior on device discovery and resistance setting.
2025-10-30 14:20:23 +01:00
Roberto Viola
28bc5670c4 Update project.pbxproj 2025-10-30 13:24:55 +01:00
Roberto Viola
e0b84cb4a3 Check SCH_411_510E before processing instant power
Added a condition to ensure instant power is only processed when SCH_411_510E is not set. This prevents unintended behavior for devices with SCH_411_510E.
2025-10-30 13:23:27 +01:00
Roberto Viola
4cdf23d544 Toputure TEB1 #3814 2025-10-30 11:22:34 +01:00
Roberto Viola
7beb1aed6f set watchos to 6.0
https://developer.apple.com/forums/thread/805538
2025-10-30 09:00:59 +01:00
Roberto Viola
91841a1ff7 Find minimum start offset in target metrics
mail from Christian P. 29/10/2025 "qdomyos-zwift"

Replaces logic that assumed the first element was chronologically first with a search for the minimum 'start' offset among all target metrics. This ensures the correct offset is used even if the data is unordered.
2025-10-30 08:54:07 +01:00
Roberto Viola
886310497c Update project.pbxproj 2025-10-29 15:36:01 +01:00
Roberto Viola
5e96d3cff3 Update ypooelliptical.cpp 2025-10-29 13:59:20 +01:00
Roberto Viola
d66af32eed Update project.pbxproj 2025-10-29 13:55:41 +01:00
Roberto Viola
982318326f Elliptical FlowFitness 2Xi (Issue #3811) 2025-10-29 13:54:37 +01:00
Roberto Viola
2d680b9c4c Update project.pbxproj 2025-10-29 09:47:19 +01:00
Roberto Viola
17e2d211c1 Update watt calculation for SCH_411_510E device
Modified the power calculation logic to exclude SCH_411_510E from the divisor adjustment and to handle watt calculation for SCH_411_510E similarly to DOMYOS. Also moved the debug emit for current watt outside conditional blocks for consistent logging.
2025-10-29 09:41:14 +01:00
Roberto Viola
e1020be250 Bkool smart bike #3774 2025-10-29 09:17:22 +01:00
Roberto Viola
ce219a790a Sportstech sbike lite autoresistance in Rouvy #3803 2025-10-28 14:51:41 +01:00
Roberto Viola
7e55ded95d QZ Android Sends Data Even Without Pedaling (Issue #3763) 2025-10-28 12:31:45 +01:00
Roberto Viola
f70c6e8feb Add FIT_BK to 2-byte resistance protocol in ftmsbike
This ensures FIT_BK bikes receive resistance values multiplied by 10 using the 2-byte protocol, consistent with other similar bikes (JFBK5_0, DIRETO_XR, YPBM).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 19:53:36 +01:00
Roberto Viola
bdfaaecc83 Update WorkoutTracking.swift 2025-10-27 15:34:42 +01:00
Roberto Viola
4622fd6df2 Update project.pbxproj 2025-10-27 15:24:43 +01:00
Roberto Viola
fa5691fa2a Fix cadence handling for SCH_411_510E 2025-10-27 15:12:54 +01:00
Roberto Viola
2b995f8396 adding rowing, elliptical and walking on ios apple health 2025-10-27 14:55:34 +01:00
Roberto Viola
e044dc69bc fixing github ios CI 2025-10-27 11:45:25 +01:00
Roberto Viola
26bf095f19 Update project.pbxproj 2025-10-27 10:41:21 +01:00
Roberto Viola
2ba66d9625 writing ios heart rate without using apple watch and air pods 3 2025-10-27 10:40:45 +01:00
Roberto Viola
621440f981 writing ios heart rate without using apple watch and air pods 3 2025-10-27 09:47:56 +01:00
Roberto Viola
a0c1efce9c right device type in apple health from ios directly 2025-10-26 15:53:22 +01:00
Roberto Viola
861f916eb4 Update Windows build job to windows-2022
Changed the GitHub Actions workflow to use 'windows-2022' instead of 'windows-latest' for the window-build job. Also updated the build step to use Windows path separators and removed the explicit msys2 shell specification.
2025-10-26 14:23:28 +01:00
Roberto Viola
8ae1c59b41 Update project.pbxproj 2025-10-26 13:51:13 +01:00
Roberto Viola
d213b5dffe Add support for SCH411/510E elliptical device 2025-10-26 13:49:03 +01:00
Roberto Viola
099531be72 Después de cargar el archivo gpx, no se visualiza el mapa #3795 2025-10-26 13:40:54 +01:00
Roberto Viola
b7e92ab33c Fix Windows MinGW build after GitHub Actions runner update
GitHub migrated windows-latest from Windows Server 2022 to 2025, which
changed how PowerShell inherits PATH from MSYS2. The qthttpserver build
step now explicitly uses the msys2 shell to access MinGW tools (g++, make).

Changes:
- Add shell: msys2 {0} to Build qthttpserver step
- Fix path separator from backslash to forward slash for MSYS2 compatibility

Fixes: "Project ERROR: Cannot run compiler 'g++'"

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 13:30:28 +01:00
Roberto Viola
0720d431db ios github build fixed 2025-10-26 13:16:39 +01:00
Roberto Viola
62c7b7b9df Add FTMS auto-detection for Domyos-Bike
- Check ftms_bike is disabled before connecting to domyosbike in bluetooth.cpp
- Auto-switch to FTMS bike when main service not found but FTMS service available
- Show toast notification to restart app when FTMS service detected

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 08:38:43 +02:00
Roberto Viola
4eed4be958 Update project.pbxproj 2025-10-24 17:55:13 +02:00
Roberto Viola
eb0dc48d24 Add support for SCH411/510E elliptical device
Added SCH411/510E device support to ypooelliptical class following the same FTMS pattern used for other devices like SCH_590E, E35, and KETTLER.

Changes:
- Added SCH_411_510E boolean flag to ypooelliptical.h
- Added device detection in bluetooth.cpp
- Updated all FTMS conditional checks to include SCH_411_510E

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 17:27:16 +02:00
Roberto Viola
4ff8e8335a GTABikeV ios compatibility 2025-10-24 08:42:05 +02:00
Roberto Viola
ec263a402d Update project.pbxproj 2025-10-23 21:14:47 +02:00
Roberto Viola
0ffb06cc79 Bkool smart bike #3774 (#3792) 2025-10-23 21:08:53 +02:00
Roberto Viola
302526000f Enable continuous gear shifting by decoupling from system volume limits on Android (Issue #3775) (#3779) 2025-10-23 13:34:33 +02:00
Roberto Viola
76891d41e2 Request : add support for Proform Sport 7.0 treadmill #2635
mail Re: I'm having problems with a Proform 7.0 Sport treadmill. The data is refreshed every 5 seconds, so it becomes impossible to uy application... in fact, with Kinomaps it ends up taking me out of the training due to many disconnections... for Kinomaps, when it stays at 0 km/h it pauses... thank you very much in advance and congratulations for this magnificent application 21/10/2025
2025-10-22 09:03:09 +02:00
Roberto Viola
bb3f9fe216 GTABikeV ios compatibility 2025-10-22 08:17:47 +02:00
Roberto Viola
dd0ce73260 Update trxappgateusbbike.cpp 2025-10-21 17:06:57 +02:00
Roberto Viola
206fa06049 Adjust packet length check for TOORX_SRX_500
Changed the minimum packet length for TOORX_SRX_500 from 21 to 19 in characteristicChanged. This ensures short packets are correctly ignored for this bike type.
2025-10-21 08:51:44 +02:00
Roberto Viola
3985eecfe6 Update trxappgateusbbike.cpp 2025-10-21 08:50:40 +02:00
Roberto Viola
97a7b5c27c Think A102-0063521 2025-10-20 15:05:24 +02:00
Roberto Viola
02c7063655 Update project.pbxproj 2025-10-20 10:59:18 +02:00
Roberto Viola
0067a728a4 iOS live activity continues after app closes (Issue #3783) 2025-10-20 09:34:23 +02:00
Roberto Viola
3ec15253d0 issues connecting zwift play with thinkrider max 2 (Issue #3758) (#3759)
* issues connecting zwift play with thinkrider max 2 (Issue #3758)

* Revert "issues connecting zwift play with thinkrider max 2 (Issue #3758)"

This reverts commit c657127675.

* avoiding char 0xFFF4 for cadence increment

* Update tacxneo2.cpp

* Update tacxneo2.cpp
2025-10-16 15:53:07 +02:00
Roberto Viola
09772d0968 ICSE patching
mail from Jean - Connectivity from 16/10/2025
2025-10-16 15:37:57 +02:00
Roberto Viola
6496e0cf7c Update project.pbxproj 2025-10-16 13:43:30 +02:00
Roberto Viola
b6ef01c59a ICSE patching
mail from Jean - Connectivity from 16/10/2025
2025-10-16 13:41:20 +02:00
Roberto Viola
522ea54ff2 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-15 10:24:55 +02:00
Roberto Viola
ba4cc11196 Walking speed surges/spikes #3757 2025-10-15 10:24:49 +02:00
Roberto Viola
95622182c9 Update project.pbxproj 2025-10-13 17:08:55 +02:00
Roberto Viola
0349dfcbaf Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-13 17:08:13 +02:00
Roberto Viola
9fc9d9cfe9 patching ftmsbike for multiple connectedAndDiscovered.
email from Jean-Noel - Connectivity from 13/10/2025
2025-10-13 16:12:53 +02:00
Roberto Viola
5ea16d0869 fixing git for live activities 2025-10-13 09:15:25 +02:00
Roberto Viola
57b259fcba ICSE reset to 15 seconds
mail "Connectivity" from Jean 13/10/2025
2025-10-13 08:05:34 +02:00
Roberto Viola
b78cf1fca5 fixing crash on ios 2025-10-11 07:39:04 +02:00
Roberto Viola
77b71e56de Update project.pbxproj 2025-10-10 14:00:25 +02:00
Roberto Viola
c8e3d370a1 fixing build on ios 2025-10-10 13:54:27 +02:00
Roberto Viola
ab8a325da2 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-10 13:43:24 +02:00
Roberto Viola
a3158514af Update project.pbxproj 2025-10-10 13:42:54 +02:00
Roberto Viola
2804d4686a Proform Rower (Proform 750R) cannot change resistance from app (Issue #3746) 2025-10-10 13:39:06 +02:00
Roberto Viola
cf0dc2d00d nordictrack integration broken #3731 2025-10-10 10:12:08 +02:00
Roberto Viola
164aa38cb1 adding horizon treadmill XP
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 20:53:30 +02:00
Roberto Viola
14998d0f25 TOPUTURE TP1 2025-10-09 13:29:40 +02:00
Roberto Viola
ca74fe7ccd fixing wattage reset on the elliptical class 2025-10-09 09:55:38 +02:00
Roberto Viola
facba11bae miles on live activity 2025-10-09 08:00:17 +02:00
Giuseppe Macario
8b8302fb53 typos (#3744) 2025-10-09 04:59:43 +02:00
Roberto Viola
eaea4bf8b8 Recently I managed to decode the Rowing data from Merach NovaRow R50, are any guideline I can follow in order to add it to the app? #3593 2025-10-08 14:01:24 +02:00
Roberto Viola
3d9c3e4103 Recently I managed to decode the Rowing data from Merach NovaRow R50, are any guideline I can follow in order to add it to the app? (Discussion #3593) 2025-10-08 12:00:02 +02:00
Roberto Viola
e840d7b3e9 Update bkoolbike.cpp 2025-10-07 15:03:27 +02:00
Roberto Viola
3a248ad2c5 Update project.pbxproj 2025-10-07 15:01:35 +02:00
Roberto Viola
5912d7df2d Airpods pro 3 heart rate (#3718)
* Airpods Pro 3 Heart Rate

* Update project.pbxproj
2025-10-07 14:53:48 +02:00
Roberto Viola
94842114e6 BKOOL Bike V 1 2025-10-07 14:47:16 +02:00
Roberto Viola
d83df0ba5a Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-07 08:54:33 +02:00
Roberto Viola
0764fb50b2 Peloton Walking Pace Targets at bottom of range (Issue #3738) 2025-10-07 08:54:26 +02:00
Roberto Viola
bb5de868ab Live Actions iOS (#3735)
* Add iOS Live Activity support at startup

- Created LiveActivityManager.swift for managing fitness metrics Live Activities
- Added Objective-C++ bridge (ios_liveactivity.h/mm) for Qt integration
- Updated Info.plist with NSSupportsLiveActivities flag
- Initialized LiveActivityManager in AppDelegate on app startup (iOS 16.1+)
- Added new files to qdomyos-zwift.pri build configuration

Live Activities display real-time fitness metrics (speed, cadence, power, heart rate, distance, calories) on Lock Screen and Dynamic Island.

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

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

* Update project.pbxproj

* Update AppDelegate.swift

* fixing build error

* let's see

* qzwidget

* distance fixed in live activities

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-05 14:34:17 +02:00
Roberto Viola
2b8fe6c28d Schwinn CABLE concerns (Issue #3727) 2025-10-04 06:50:45 +02:00
Roberto Viola
0153e09f0d TOPUTURE TP1 treadmill 2025-10-03 15:19:09 +02:00
Roberto Viola
dc44433d7c Update project.pbxproj 2025-10-03 12:45:32 +02:00
Roberto Viola
8bdefdb331 Auto inclination not working when using an Android tablet (Issue #3730) 2025-10-03 12:44:01 +02:00
Roberto Viola
c7f5e320fc Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-02 15:35:10 +02:00
Roberto Viola
c1582cc763 When using Watt Gain < 1 , unable to fulfil the requested power in Workouts (ERG) (Issue #3728) 2025-10-02 15:34:58 +02:00
Roberto Viola
f2f0f7a793 Update project.pbxproj 2025-10-01 17:07:23 +02:00
Roberto Viola
3d665e397e BKOOL Bike V 1 2025-10-01 17:06:13 +02:00
Roberto Viola
194f8686f3 Update project.pbxproj 2025-10-01 15:49:33 +02:00
Roberto Viola
fb79d0ddd6 SportsTech sWalk Walking Pad 2025-10-01 15:39:55 +02:00
Roberto Viola
d7e0a4e441 stop workout confirmation 2025-10-01 09:54:44 +02:00
Roberto Viola
465123a156 KUBIsport BC91EK 2025-10-01 09:30:48 +02:00
Roberto Viola
88d01562b1 DMASUN Bikes 2025-09-30 15:34:15 +02:00
Roberto Viola
85421f41b8 KUBIsport BC91EK 2025-09-30 08:11:11 +02:00
Roberto Viola
a67cb10633 Update project.pbxproj 2025-09-29 13:35:41 +02:00
Roberto Viola
f00a161fc1 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-29 13:33:10 +02:00
Roberto Viola
c071c56eb7 Toorx BRX R100 ergo bicycle 2025-09-29 13:33:03 +02:00
Roberto Viola
b39f769423 Update project.pbxproj 2025-09-28 08:41:58 +02:00
Roberto Viola
dde526c059 Merach MRK-T25-EF79 (T25) discovered but no tiles report any activity (Issue #3720) 2025-09-28 08:38:39 +02:00
Roberto Viola
c223d6e81d ORLAUF ARES device added 2025-09-27 14:44:58 +02:00
Roberto Viola
d531a1d313 ios adb debug 2025-09-26 09:05:07 +02:00
Roberto Viola
b0722cc827 ios adb log 2025-09-26 08:46:06 +02:00
Roberto Viola
2e534abfbb Update lockscreen.mm 2025-09-24 16:23:48 +02:00
Roberto Viola
6d3ca9877a Update project.pbxproj 2025-09-24 16:08:07 +02:00
Roberto Viola
f477cb32ab Proform CSX210 2025-09-24 12:10:34 +02:00
Roberto Viola
51b79ed413 Wattage gain max value increase #3709 2025-09-24 09:32:56 +02:00
Roberto Viola
fa78f03f0a D500 trainer added 2025-09-22 08:16:39 +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
Roberto Viola
265275d8aa SOLE LCR Bike #3226 2025-07-11 09:23:01 +02:00
Roberto Viola
705eb57414 SOLE LCR Bike #3226 2025-07-10 08:27:32 +02:00
Roberto Viola
9c446bcaf6 fixing CI 2025-07-10 06:46:20 +02:00
Roberto Viola
86118c04e2 2.20.0 2025-07-09 15:17:26 +02:00
Roberto Viola
081d9d4e24 Peloton vs QZ Rower Pace Targets (Issue #3533) 2025-07-09 11:24:18 +02:00
Roberto Viola
4ffc0867e3 fixing CI 2025-07-09 06:31:57 +02:00
Roberto Viola
3bfecadd1f fixing CI 2025-07-09 06:21:34 +02:00
Roberto Viola
06aa01d755 Update project.pbxproj 2025-07-08 15:38:50 +02:00
Roberto Viola
e432df9f6b Add nordictrack-build CI target (#3531)
* Add nordictrack-build CI target

Added new CI job 'nordictrack-build' that builds Android APK from refs/pull/3478/head branch.
This target uses the same build structure as the existing android-build job but checks out
the Nordic Track gRPC implementation PR instead of master branch.

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

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

* Update main.yml

* Update main.yml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-08 12:21:22 +02:00
Roberto Viola
e3f4384014 Unify inclination step setting for treadmill and bike
Updated the inclination step adjustment in homeform.cpp to use the treadmill_step_incline setting for both treadmills and bikes. Moved the inclination step setting UI in settings.qml to a more general location and clarified its effect on both device types.
2025-07-08 09:42:40 +02:00
Roberto Viola
563ced3de1 2.19.3 2025-07-08 06:06:34 +02:00
Roberto Viola
e48c6525ea Update project.pbxproj 2025-07-08 05:04:28 +02:00
Roberto Viola
ca34e99277 PELOTON fixing pop classes 2025-07-08 05:03:56 +02:00
Roberto Viola
446f5200ba ASCEND S2 BIKE + QDOYMOS / Virtual machine (Issue #3419) 2025-07-06 15:53:11 +02:00
Roberto Viola
edcb7ab359 SPEEDMAGPRO distance fix
Email from Patrick L. 5/7/2025
2025-07-06 15:46:07 +02:00
Roberto Viola
3844808b60 adding SPEEDMAGPRO 2025-07-06 15:31:08 +02:00
Roberto Viola
8e1ddc502f bike losing connection #3528 2025-07-06 15:29:14 +02:00
Roberto Viola
e633f0f671 Nautilus 616
Mail from Leanne 5/7/2025
2025-07-05 21:59:28 +02:00
Roberto Viola
93a38a7b79 Improve token debug messages for Strava, Peloton, Zwift
Replaced raw token output in debug logs with clearer, non-sensitive success messages for Strava, Peloton, and Zwift authentication flows. This enhances log readability and security by avoiding direct token exposure in debug output.
2025-07-04 10:34:18 +02:00
Roberto Viola
d2f8ed8c01 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-07-04 09:49:36 +02:00
Roberto Viola
60a9d7cb0f fixing description in the stagesbike log 2025-07-04 09:49:10 +02:00
Roberto Viola
4c0793c785 Reverting wahoo protocol to 2.17, creating a setting to revert this (#3489)
* reverting to eb540dc579/src/devices/wahookickrsnapbike/wahookickrsnapbike.cpp

* Update wahookickrsnapbike.h

* Update homeform.cpp

* fixing build

* trying to get the right issue

* trying to restore thing

* Update project.pbxproj

* adding the settngs, but need to use the new setting in the wahookickrsnapbike.cpp

* watt gain issue!

* Update wahookickrsnapbike.h

* Update project.pbxproj

* using the new settings (not tested, just to compare on github web)

* trying to improve readability

* cleaning

* splitting the 2 logic in the update. not tested yet

* trying to align the logic

* fixing description

* Update project.pbxproj
2025-07-04 09:39:55 +02:00
Roberto Viola
5fc377f648 Update main.yml 2025-07-03 15:28:29 +02:00
Roberto Viola
0d6f207991 Update main.yml 2025-07-03 14:35:35 +02:00
Roberto Viola
051f296913 Unify app startup wait time in CI workflow
Replaces conditional sleep based on Android API level with a fixed 60-second wait after starting the app. Simplifies the workflow and ensures consistent wait time across all API levels.
2025-07-03 13:02:45 +02:00
Roberto Viola
45a4d6d0b1 Fix shell script syntax in workflow app start step
Replaces multi-line if-else block with single-line commands using backslashes to ensure correct execution in the GitHub Actions workflow when waiting for the app to start based on Android API level.
2025-07-03 12:28:33 +02:00
Roberto Viola
d2612ad03f Improve Android CI app launch and debugging steps
Enhanced the workflow to use a longer wait time for older Android API levels, added more robust process detection for the app, and included additional debugging output such as logcat and package info. Also, logcat outputs are now saved as artifacts for easier analysis.
2025-07-03 11:51:20 +02:00
Roberto Viola
6bb4d99f29 Improve app process check for Android versions
Updated the workflow to use a fallback ps command for compatibility with different Android versions when verifying if the app is running.
2025-07-03 10:15:50 +02:00
Roberto Viola
c3dbce9ea8 Add matrix strategy for Android emulator tests
Introduces a matrix build to run emulator tests across multiple Android API levels and architectures. Updates emulator configuration and artifact naming to reflect the tested Android version, improving test coverage and traceability.
2025-07-03 09:02:40 +02:00
Roberto Viola
989315fb5e fixing android emulator test 2025-07-03 08:59:09 +02:00
Roberto Viola
ce3782f80b Elite Rampa + MyWoosh compatibility
email from M.Carter from 02/07/2025
2025-07-03 08:51:02 +02:00
Roberto Viola
4ee77b392e Update AndroidManifest.xml 2025-07-02 22:01:28 +02:00
Roberto Viola
03896d7384 Update InAppPurchase.java (#3523) 2025-07-02 22:00:23 +02:00
Roberto Viola
9258bf6af2 Update AndroidManifest.xml 2025-07-02 20:45:26 +02:00
Roberto Viola
7a0a990eb8 Add Android 16 API 36 compatibility with WindowInsetsController
- Create CustomQtActivity extending QtActivity for Android 16 support
- Replace deprecated setSystemUiVisibility with WindowInsetsController
- Maintain backward compatibility with older Android versions
- Fix header toolbar visibility issues on Android 16

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-02 20:44:17 +02:00
Roberto Viola
aecb0c97df Add individual mode for HR time-in-zone tiles
Introduces a new setting to toggle between cumulative and individual time display for heart rate zone tiles. Updates the UI and logic in homeform.cpp to reflect the selected mode, adds the setting to QZSettings and QML, and documents the change.
2025-07-02 14:37:35 +02:00
Roberto Viola
eae3f59e4a build fix 2025-07-02 09:55:04 +02:00
Roberto Viola
9ad0137190 build fix 2025-07-02 09:52:06 +02:00
Roberto Viola
f62548ac60 Kinetic trainer (T-6200) resistance and inclination fix standalone 2025-07-02 09:22:57 +02:00
Roberto Viola
986bc2252e Elite Rampa + MyWoosh compatibility
email from M.Carter from 02/07/2025
2025-07-02 09:17:35 +02:00
Roberto Viola
7d744ee874 Update InAppPurchase.java 2025-07-02 08:45:11 +02:00
Roberto Viola
8f87074e69 targetSdkVersion 36 2025-07-02 08:19:10 +02:00
Roberto Viola
acd141d32b 2.19.2 2025-07-02 08:02:42 +02:00
Roberto Viola
b4fb3e339a fixing linux build 2025-07-01 10:39:16 +02:00
Roberto Viola
1336460297 added setting for the garmin fit file 2025-07-01 09:22:21 +02:00
Roberto Viola
3a45b64d51 FITSDK 21.171.00 2025-07-01 08:07:56 +02:00
Roberto Viola
49be559ae8 Update eslinkertreadmill.cpp 2025-06-30 13:51:06 +02:00
Roberto Viola
e3e15bf24d MacOs closes unexpectedly when treadmill Bluetooth connects (Issue #3518) 2025-06-30 13:19:46 +02:00
Roberto Viola
b9a7ddcaa0 rogue bike fix 2025-06-29 20:41:24 +02:00
Roberto Viola
bfd6de1d49 rogue echo bike
mail from a. morgan on 28/06/2025
2025-06-29 20:23:36 +02:00
Roberto Viola
447cb04376 Update project.pbxproj 2025-06-29 08:27:26 +02:00
Roberto Viola
29dd4cf10a kinetic handling erg and inclination and resistance (not tested) 2025-06-29 08:26:36 +02:00
Roberto Viola
c4ea190370 The Runna App are not able to controll my Kingsmith Treadmill (Issue #3519) 2025-06-29 07:45:29 +02:00
Roberto Viola
7e51db80e6 fixing crash on watchos 2025-06-28 08:34:49 +02:00
Roberto Viola
d475e50489 runna compatibility with kingsmith treadmills 2025-06-28 07:13:21 +02:00
Roberto Viola
1c4a22041d bowflex integration not quite right #3421 2025-06-27 15:17:55 +02:00
Roberto Viola
b07ffac325 Update project.pbxproj 2025-06-26 15:23:43 +02:00
Roberto Viola
d1dab0cd79 training effect in the fit file fixed
thanks to https://github.com/jat255/Fit-File-Faker
2025-06-26 15:16:12 +02:00
Roberto Viola
f5a55a253e Support for ProForm 995i #482 2025-06-26 12:58:16 +02:00
Roberto Viola
8f5b5bd5b7 Tacx neo new FW virtual shifting test (Issue #3511) 2025-06-25 12:53:00 +02:00
Roberto Viola
9d946dd1c5 Update homeform.cpp 2025-06-25 12:47:20 +02:00
Roberto Viola
d6c65dd7d8 Estimated FTP -1W (Issue #3510) 2025-06-25 12:07:09 +02:00
Roberto Viola
8d607ca0ba T900A settings renamed to T900 2025-06-25 11:49:56 +02:00
Roberto Viola
253c00f014 Create CLAUDE.md 2025-06-24 15:40:19 +02:00
Roberto Viola
a032a5d51c fixing build 2025-06-24 15:26:17 +02:00
Roberto Viola
b0c2fa5b17 Update project.pbxproj 2025-06-24 14:51:35 +02:00
Roberto Viola
7cf05276e9 BODY BIKE Smart+ integration 2025-06-24 11:32:41 +02:00
Roberto Viola
708f28fffb 2.19.1 2025-06-24 10:38:20 +02:00
Roberto Viola
c50b7655ba Heart Rate from Apple Watch not flowing to iPad #3498 2025-06-24 09:13:08 +02:00
Roberto Viola
9db2b8c235 Concept2 Rowers connecting but tiles do not update. #3503 2025-06-24 08:16:27 +02:00
Roberto Viola
134a228473 Revert "fixing github msv2019 build?"
This reverts commit c28a16d8d2.
2025-06-23 14:44:34 +02:00
Roberto Viola
f1ad62ce4a Update main.yml 2025-06-23 14:44:25 +02:00
Roberto Viola
c28a16d8d2 fixing github msv2019 build? 2025-06-23 14:18:31 +02:00
Roberto Viola
83e229e55f Update project.pbxproj 2025-06-23 12:21:12 +02:00
Roberto Viola
bd9bbdb236 3D maps start and stop points (#3502)
* Update maps.htm

* Update maps.htm
2025-06-23 10:53:47 +02:00
Roberto Viola
24095fc8cc Mqtt control (#3501)
* mqtt control

* mqtt discovery

* removed file
2025-06-23 10:44:39 +02:00
Roberto Viola
a199ba3c2b Update nordictrackifitadbbike.cpp 2025-06-19 11:23:34 +02:00
Roberto Viola
bf07764bca Proform TDF 10.0 adb resistance
mail from M B of 18/06/2025
2025-06-18 15:35:14 +02:00
Roberto Viola
1f7ce9b724 fixing watt gain for wahoo bikes 2025-06-18 13:44:48 +02:00
Roberto Viola
51ea12782d ADB Android Log verbose (#3486)
* first commit

* adding more logs

* Update QZAdbRemote.java

* trying a patch

* removing crypto thread

* adding logs
2025-06-18 10:36:35 +02:00
Roberto Viola
d7c2339783 bowflex integration not quite right #3421 2025-06-18 09:34:44 +02:00
Adam Sharpe
0bed0ca76c feat: Align FTMS elapsed time and fix cadence/resistance mapping (Swift & C++) (#3493)
This commit introduces elapsed time transmission for the virtual treadmill's FTMS characteristic in the Swift implementation, aligning it with the recent C++ changes.

Additionally, it refines the C++ to Swift updateFTMS call to ensure arguments align with the Swift function's expected parameters, including a noted discrepancy around cadence/resistance/wattage mapping.

**Important Note:**
The Swift changes () could not be compiled or fully tested with an iOS device or Zwift due to the lack of a macOS development environment (Xcode). Community assistance in validating the Swift FTMS broadcast is greatly appreciated.
2025-06-18 08:55:39 +02:00
Roberto Viola
eb4ae28fa7 Elite Rampa
mail 17/06/2025 from Matt C.
2025-06-17 05:35:27 +02:00
Roberto Viola
d1827a48ee version 2.19.0 2025-06-17 05:21:55 +02:00
Roberto Viola
f5c1b175a5 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-06-17 03:15:55 +00:00
Roberto Viola
76b14cccb4 Nordictrack Elite 800 not accepting speed/incline changes from QZ (Issue #3393) 2025-06-17 03:15:45 +00:00
Adam Sharpe
19efaf6878 feat: Add elapsed time to FTMS virtual treadmill characteristic (#3491) 2025-06-17 04:59:05 +02:00
Roberto Viola
61a96dbc63 Update proformtreadmill.cpp 2025-06-16 15:23:27 +02:00
Roberto Viola
656f2b366b Nordictrack Elite 800 not accepting speed/incline changes from QZ (Issue #3393) 2025-06-16 15:03:15 +02:00
Roberto Viola
ee336436e7 Help with a Expert SX9 #3436 2025-06-16 12:28:58 +02:00
Roberto Viola
739270b944 Help with a Expert SX9 #3436 2025-06-16 11:55:59 +02:00
Roberto Viola
bcaf466fa1 Mobvoi Treadmill Plus 2025-06-16 10:50:04 +02:00
Roberto Viola
196f77d22a Think Rider X5 compability (Issue #3487) 2025-06-13 11:26:17 +02:00
Roberto Viola
077c995a08 Update project.pbxproj 2025-06-13 10:36:56 +02:00
Roberto Viola
6db0aba44d Proform Treadmill Carbon TLS
Mail 13/6/2025 from Sonny
2025-06-13 10:23:38 +02:00
Roberto Viola
80fc8b8d46 Core temperature sensor tiles (#3484) 2025-06-12 08:02:37 +02:00
Roberto Viola
fb2676ef37 Update project.pbxproj 2025-06-11 19:32:41 +02:00
Roberto Viola
9281e91f67 Update project.pbxproj 2025-06-11 10:38:30 +02:00
Roberto Viola
540108a207 Core temperature sensor tiles (#3484)
* first commit

* adding settings

* ready to deploy

* Update homeform.cpp
2025-06-11 10:25:46 +02:00
Roberto Viola
27299a7805 Adding compensation when there is a power meter (Issue #3483) 2025-06-11 09:26:01 +02:00
Roberto Viola
52957c61a4 Connecting a NordicTrack Ultra LE treadmill to Zwift
mail from 7/6/2025
2025-06-11 08:22:09 +02:00
Roberto Viola
b06ac89f6e Sperax Walking Pad treadmill #3480 2025-06-10 09:17:06 +02:00
Roberto Viola
55921ed367 Help with a Expert SX9 (Issue #3436) 2025-06-10 08:53:00 +02:00
Roberto Viola
e89c668a96 adding device info mesg 2025-06-10 08:13:15 +02:00
Roberto Viola
43ca65baf2 Help with a Expert SX9 (Issue #3436) 2025-06-09 14:39:58 +02:00
Roberto Viola
eaba07cafd reverting Connection #3466 2025-06-09 13:11:27 +02:00
Roberto Viola
5b57f2c8aa Help with a Expert SX9 (Issue #3436) 2025-06-09 10:39:08 +02:00
Roberto Viola
5fea12e6bc Merach S26B2 2025-06-09 08:55:33 +02:00
Roberto Viola
dcdd4fdbb6 Update speraxtreadmill.cpp 2025-06-07 16:03:40 +02:00
Roberto Viola
aac9f7a20e Update speraxtreadmill.cpp 2025-06-07 15:35:33 +02:00
Roberto Viola
5146352494 FTMS Bike Ant Sender (#3467) 2025-06-07 15:15:48 +02:00
Roberto Viola
f666c4cc55 fixing drive letter in the github actions? 2025-06-07 06:13:07 +02:00
Roberto Viola
882778b9ff Update project.pbxproj 2025-06-06 18:17:31 +02:00
Roberto Viola
783b832805 Core Sensor Support #3347 2025-06-06 18:16:26 +02:00
Roberto Viola
defb177f53 Update project.pbxproj 2025-06-06 09:43:48 +02:00
Roberto Viola
d511df4186 Sperax Treadmill 2025-06-06 09:43:13 +02:00
Roberto Viola
ffe79b434a Core Sensor Support #3347 2025-06-06 09:30:50 +02:00
Roberto Viola
9050be6063 reverting Connection #3466 2025-06-06 08:45:44 +02:00
Roberto Viola
744d2d138f Sperax Treadmill 2025-06-06 08:25:50 +02:00
Roberto Viola
612acf3610 Connection (Issue #3466) 2025-06-05 15:00:58 +02:00
Roberto Viola
a84e4ca84d Update project.pbxproj 2025-06-05 09:14:31 +02:00
Roberto Viola
e4b4dd943e Losing Connection #3439 2025-06-05 09:13:40 +02:00
Roberto Viola
121a046bb8 DKN EnduRun data not showing in QZ #3468 2025-06-05 09:05:17 +02:00
Roberto Viola
21e0a7edc8 Sperax Treadmill
Sperax treadmill compatibility email from 21/05/2025 and QZ Fitness support for Sperax Walking Pad treadmill email from 4/06/2025
2025-06-05 09:01:56 +02:00
Roberto Viola
83e1c136e4 Update project.pbxproj 2025-06-04 08:16:02 +02:00
Roberto Viola
9a19956b1c Magene Gravat2 bike trainer compatibility #3460 2025-06-04 08:03:55 +02:00
Roberto Viola
18571131ca Update qdomyos-zwift.pri 2025-06-03 11:01:05 +02:00
Roberto Viola
eba9a2c8d5 Update speraxtreadmill.cpp 2025-06-03 11:00:54 +02:00
Roberto Viola
dbf5b7005d Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-06-03 10:17:42 +02:00
Roberto Viola
64476f7e61 Sperax Treadmill 2025-06-03 10:17:35 +02:00
Roberto Viola
f111e9c4e4 Nordictrack Elite 800 not accepting speed/incline changes from QZ #3393 2025-06-03 07:13:25 +00:00
Roberto Viola
0f7b240f4a Mobvoi Treadmill Plus Elapsed Time / Moving Time #3404 (#3425) 2025-05-30 21:25:52 +02:00
Roberto Viola
63ee0f6d71 Update project.pbxproj 2025-05-30 16:00:39 +02:00
Roberto Viola
e38b00b735 Update ftmsbike.cpp 2025-05-30 15:58:09 +02:00
Roberto Viola
97a5de93d8 Help with a Expert SX9 (Issue #3436) 2025-05-30 15:40:37 +02:00
Roberto Viola
9aabaddf6e Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-05-30 14:13:26 +02:00
Roberto Viola
2fb9d6043a Core Sensor Support #3347 2025-05-30 14:13:21 +02:00
Roberto Viola
d1afc5c6b3 Adding Tunturi T80 2025-05-29 10:04:25 +02:00
Roberto Viola
2270d93419 Update project.pbxproj 2025-05-28 10:43:58 +02:00
Roberto Viola
4e3e6de6c1 2.18 (1087) causes my treadmill not to function properly #3456 2025-05-28 08:50:07 +02:00
Roberto Viola
201877f214 version 2.18.25 2025-05-27 13:50:27 +02:00
Roberto Viola
0941d3f218 Floating Window display (Discussion #3402) (#3426)
* Floating Window display (Discussion #3402)

* adding setting

* Update homeform.cpp

* verify jni

* Update FloatingHandler.java

* Update main.yml

* Update main.yml
2025-05-24 06:59:17 +02:00
Roberto Viola
7f8876d021 Update project.pbxproj 2025-05-23 09:24:48 +02:00
Roberto Viola
62c7e2125a PELOTON: cool down classes fix #2606
email from aaron 22/05/2025
2025-05-23 09:17:57 +02:00
Roberto Viola
13b2d6a18b Update project.pbxproj 2025-05-22 16:33:29 +02:00
Roberto Viola
2e854d8f1f PELOTON: cool down classes fix #2606
email from aaron 21/05/2025
2025-05-22 08:42:04 +02:00
606 changed files with 42826 additions and 8609 deletions

File diff suppressed because it is too large Load Diff

3
.gitignore vendored
View File

@@ -1,3 +1,5 @@
src/qdomyos-zwift.pro.user
.idea/
src/Makefile
@@ -50,3 +52,4 @@ src/inner_templates/googlemaps/cesium-key.js
.vscode/settings.json
/tst/Devices/.vs
src/inner_templates/googlemaps/cesium-key.js
src/qdomyos-zwift.pro.user.49de507

374
CLAUDE.md Normal file
View File

@@ -0,0 +1,374 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
QDomyos-Zwift is a Qt-based application that bridges fitness equipment (treadmills, bikes, ellipticals, rowers) with virtual training platforms like Zwift. It acts as a Bluetooth intermediary, connecting physical equipment to fitness apps while providing enhanced features like Peloton integration, power zone training, and workout programs.
## Build System & Commands
### Build Commands
```bash
# Build entire project (use subdirs TEMPLATE)
qmake
make
# Build specific configurations
qmake -r # Recursive build
make debug # Debug build
make release # Release build
# Clean build
make clean
make distclean
```
### Platform-Specific Builds
```bash
# Android
qmake -spec android-clang
make
# iOS
qmake -spec macx-ios-clang
make
# Windows (MinGW)
qmake -spec win32-g++
make
```
### Testing
```bash
# Build and run tests (requires main app built first)
cd tst
qmake
make
./qdomyos-zwift-tests
# Run with XML output for CI
GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests
```
### No-GUI Mode
```bash
# Run application without GUI
sudo ./qdomyos-zwift -no-gui
```
## Architecture Overview
### Device Architecture
The application follows a hierarchical device architecture:
1. **Base Class**: `bluetoothdevice` - Abstract base for all fitness devices
- Manages Bluetooth connectivity via Qt's QLowEnergyController
- Defines common metrics (speed, cadence, heart rate, power, distance)
- Integrates with virtual devices for app connectivity
2. **Device Type Classes**: Inherit from `bluetoothdevice`
- `bike` - Bike-specific features (resistance, gears, power zones)
- `treadmill` - Treadmill features (speed control, inclination, pace)
- `elliptical` - Combined bike/treadmill features
- `rower` - Rowing metrics (stroke count, 500m pace)
- `stairclimber` - Step counting and climbing metrics
- `jumprope` - Jump sequence tracking
3. **Concrete Implementations**: Inherit from device type classes
- Located in `src/devices/[devicename]/` folders
- Examples: `domyosbike`, `pelotonbike`, `ftmsbike`
### Virtual Device System
- `virtualdevice` - Abstract base for virtual representations
- `virtualbike`, `virtualtreadmill`, etc. - Advertise to external apps
- Enables bidirectional communication between physical and virtual devices
### Bluetooth Management
- `bluetooth` class acts as device factory and connection manager
- `discoveryoptions` configures device discovery process
- Supports multiple connection types (Bluetooth LE, TCP, UDP)
## Key Development Areas
### Adding New Device Support
1. Create device folder in `src/devices/[devicename]/`
2. Implement device class inheriting from appropriate base type
3. Add device detection logic to `bluetooth.cpp`
4. Update `qdomyos-zwift.pri` with new source files
5. Add tests in `tst/Devices/` following existing patterns
### Characteristics & Protocols
- Bluetooth characteristics handlers in `src/characteristics/`
- FTMS (Fitness Machine Service) protocol support
- ANT+ integration for sensors
- Custom protocol implementations for specific brands
### UI & QML
- QML-based UI with Qt Quick Controls 2
- Main QML files in `src/` (main.qml, settings.qml, etc.)
- Platform-specific UI adaptations (iOS, Android, desktop)
### Integration Features
- Peloton workout/resistance integration (`peloton.cpp`)
- Zwift workout parsing (`zwiftworkout.cpp`)
- GPX file support for route following (`gpx.cpp`)
- Training program support (ZWO, XML formats)
## Platform-Specific Notes
### iOS
- Swift bridge files in `src/ios/`
- Apple Watch integration via `WatchKitConnection.swift`
- HealthKit integration for fitness data
- ConnectIQ SDK for Garmin devices
### Android
- Java bridge files in `src/android/src/`
- ANT+ integration via Android ANT SDK
- Foreground service for background operation
- USB serial support for wired connections
### Windows
- ADB integration for Nordic Track iFit devices
- PaddleOCR integration for Zwift workout detection
- Windows-specific networking features
## File Structure Patterns
### Device Files
```
src/devices/[devicename]/
├── [devicename].h # Header file
├── [devicename].cpp # Implementation
└── README.md # Device-specific documentation (optional)
```
### Test Files
```
tst/Devices/
├── DeviceTestData.h # Test data definitions
├── Test[DeviceName].h # Device-specific test cases
└── TestBluetooth.cpp # Main device detection test suite
```
## Testing Framework
- Uses Google Test (gtest) with Google Mock
- Comprehensive device detection testing
- Configuration-based test scenarios
- XML output support for CI/CD integration
- Tests must be built after main application (links against libqdomyos-zwift.a)
## Configuration & Settings
- Settings managed via `qzsettings.cpp` (QSettings wrapper)
- Platform-specific configuration paths
- Profile system for multiple users/devices
- Extensive customization options for device behavior
## External Dependencies
- Qt 5.15.2+ (Bluetooth, WebSockets, Charts, Quick, etc.)
- Google Test (submodule for testing)
- Platform SDKs (Android ANT+, iOS HealthKit, Windows ADB)
- Protocol Buffers for Zwift API integration
- MQTT client for IoT integration
- Various fitness platform APIs (Strava, Garmin Connect, etc.)
## Adding New ProForm Treadmill Models
This section provides a complete guide for adding new ProForm treadmill models to the codebase, based on the ProForm 995i implementation.
### Prerequisites
1. **Bluetooth Frame Capture File**: A file containing raw Bluetooth frames from the target treadmill
2. **Frame Analysis**: Understanding of which frames are initialization vs. sendPoll frames
3. **BLE Header Knowledge**: Each frame has an 11-byte BLE header that must be removed
### Step-by-Step Implementation Process
#### 1. Process Bluetooth Frames
First, process the raw Bluetooth frames by removing the first 11 bytes (BLE header) from each frame:
```bash
# Example: if you have "proform_model.c" with raw frames
# Process each frame by removing first 11 bytes
# Separate initialization frames from sendPoll frames
```
**Key Requirements:**
- Remove exactly 11 bytes from each frame (BLE header)
- Identify the boundary between initialization and sendPoll frames
- Initialization frames come first, sendPoll frames follow
- Document which packet number starts the sendPoll sequence
#### 2. Add Boolean Flag to Header File
Add the new model flag to `src/devices/proformtreadmill/proformtreadmill.h`:
```cpp
// Add before #ifdef Q_OS_IOS section
bool proform_treadmill_newmodel = false;
```
#### 3. Add Settings Support
Update the following files for settings integration:
**In `src/qzsettings.h`:**
```cpp
static const QString proform_treadmill_newmodel;
static constexpr bool default_proform_treadmill_newmodel = false;
```
**In `src/qzsettings.cpp`:**
```cpp
const QString QZSettings::proform_treadmill_newmodel = QStringLiteral("proform_treadmill_newmodel");
```
* Update the `allSettingsCount` in `qzsettings.cpp`
#### 4. Update QML Settings UI
**In `src/settings.qml`:**
1. Add property at the END of properties list:
```qml
property bool proform_treadmill_newmodel: false
```
2. Update ComboBox model array:
```qml
model: ["Disabled", "Proform New Model", ...]
```
3. Add case selection logic (find next available case number):
```qml
currentIndex: settings.proform_treadmill_newmodel ? XX : 0;
```
4. Add reset logic:
```qml
settings.proform_treadmill_newmodel = false;
```
5. Add switch case:
```qml
case XX: settings.proform_treadmill_newmodel = true; break;
```
#### 5. Implement Device Logic
**In `src/devices/proformtreadmill/proformtreadmill.cpp`:**
1. **Load Settings** (in constructor):
```cpp
proform_treadmill_newmodel = settings.value(QZSettings::proform_treadmill_newmodel, QZSettings::default_proform_treadmill_newmodel).toBool();
```
2. **Add Initialization Case** (in `btinit()` method):
```cpp
} else if (proform_treadmill_newmodel) {
// ALL initialization frames go here
uint8_t initData1[] = {0x00, 0xfe, 0x02, 0x08, 0x02};
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
// ... continue with ALL init frames from capture file
// Use frames from beginning until sendPoll boundary
}
```
3. **Add SendPoll Case** (in `sendPoll()` method):
```cpp
} else if (proform_treadmill_newmodel) {
switch (counterPoll) {
case 0:
// First sendPoll frame
break;
case 1:
// Second sendPoll frame
break;
// ... continue with pattern from sendPoll frames
default:
// Reset counter and cycle
counterPoll = -1;
break;
}
}
```
4. **Update Force Functions** - Add flag to conditional checks in `forceIncline()` and `forceSpeed()`:
```cpp
} else if (proform_treadmill_8_0 || ... || proform_treadmill_newmodel) {
write[14] = write[11] + write[12] + 0x12;
}
```
### Implementation Requirements
#### Frame Processing Rules
- **Exactly 11 bytes** must be removed from each frame (BLE header)
- **All initialization frames** must be included in the btinit() case
- **All sendPoll frames** must be included in the sendPoll() switch statement
- **Frame order** must be preserved exactly as captured
#### Settings Integration Rules
- **Property placement**: Always add new properties at the END of the properties list in settings.qml
- **Case numbering**: Find the next available case number in the ComboBox switch statement
- **Naming convention**: Use descriptive names following existing patterns
#### Code Organization Rules
- **Initialization**: All init frames go in btinit() method
- **Communication**: All sendPoll frames go in sendPoll() method with switch/case structure
- **Force functions**: Add new model flag to existing conditional chains
### Common Pitfalls and Solutions
#### Incorrect Byte Removal
- **Problem**: Removing wrong number of bytes (12 instead of 11)
- **Solution**: Always remove exactly 11 bytes (BLE header)
#### Wrong SendPoll Boundary
- **Problem**: Using initialization frames in sendPoll logic
- **Solution**: Identify exact packet number where sendPoll starts
#### Incomplete Initialization
- **Problem**: Missing initialization frames
- **Solution**: Include ALL frames from start until sendPoll boundary
#### Settings Placement
- **Problem**: Adding property in wrong location in settings.qml
- **Solution**: Always add at END of properties list
### Verification Checklist
- [ ] All 11 bytes removed from each frame
- [ ] Initialization frames correctly identified and included
- [ ] SendPoll frames correctly identified and implemented
- [ ] Settings properly integrated in all required files
- [ ] ComboBox updated with new model option
- [ ] Force functions updated with new model flag
- [ ] Property added at END of settings.qml properties list
### Example Reference
The ProForm 995i implementation serves as the reference example:
- 25 initialization frames (pkt4658-pkt4756)
- 33 sendPoll frames (pkt4761-pkt4897)
- 6-case sendPoll switch statement with cycling logic
- Complete settings integration across all required files
## Development Tips
- Use Qt Creator for development with proper project file support
- The project uses Qt's signal/slot mechanism extensively
- Device implementations should follow existing patterns for consistency
- Add comprehensive logging using the project's logging framework
- Test device detection thoroughly using the existing test infrastructure
- Consider platform differences when adding new features
## Additional Memories
- When adding a new setting in QML (setting-tiles.qml), you must:
* Add the property at the END of the properties list

View File

@@ -0,0 +1,84 @@
//
// QZWidget.swift
// QZWidget
//
// Created by Roberto Viola on 04/10/25.
//
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "😀")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, emoji: "😀")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
// func relevances() async -> WidgetRelevances<Void> {
// // Generate a list containing the contexts this widget is relevant in.
// }
}
struct SimpleEntry: TimelineEntry {
let date: Date
let emoji: String
}
struct QZWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Time:")
Text(entry.date, style: .time)
Text("Emoji:")
Text(entry.emoji)
}
}
}
struct QZWidget: Widget {
let kind: String = "QZWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
if #available(iOS 17.0, *) {
QZWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
QZWidgetEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
#Preview(as: .systemSmall) {
QZWidget()
} timeline: {
SimpleEntry(date: .now, emoji: "😀")
SimpleEntry(date: .now, emoji: "🤩")
}

View File

@@ -0,0 +1,18 @@
//
// QZWidgetBundle.swift
// QZWidget
//
// Created by Roberto Viola on 04/10/25.
//
import WidgetKit
import SwiftUI
@main
struct QZWidgetBundle: WidgetBundle {
var body: some Widget {
QZWidget()
QZWidgetControl()
QZWidgetLiveActivity()
}
}

View File

@@ -0,0 +1,54 @@
//
// QZWidgetControl.swift
// QZWidget
//
// Created by Roberto Viola on 04/10/25.
//
import AppIntents
import SwiftUI
import WidgetKit
struct QZWidgetControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "org.cagnulein.qdomyoszwift.QZWidget",
provider: Provider()
) { value in
ControlWidgetToggle(
"Start Timer",
isOn: value,
action: StartTimerIntent()
) { isRunning in
Label(isRunning ? "On" : "Off", systemImage: "timer")
}
}
.displayName("Timer")
.description("A an example control that runs a timer.")
}
}
extension QZWidgetControl {
struct Provider: ControlValueProvider {
var previewValue: Bool {
false
}
func currentValue() async throws -> Bool {
let isRunning = true // Check if the timer is running
return isRunning
}
}
}
struct StartTimerIntent: SetValueIntent {
static let title: LocalizedStringResource = "Start a timer"
@Parameter(title: "Timer is running")
var value: Bool
func perform() async throws -> some IntentResult {
// Start / stop the timer based on `value`.
return .result()
}
}

View File

@@ -0,0 +1,174 @@
//
// QZWidgetLiveActivity.swift
// QDomyos-Zwift Live Activity Widget
//
// Displays workout metrics on Dynamic Island and Lock Screen
//
import ActivityKit
import WidgetKit
import SwiftUI
// QZWorkoutAttributes is defined in QZWorkoutAttributes.swift (shared file)
// MARK: - Live Activity Widget
@available(iOS 16.1, *)
struct QZWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: QZWorkoutAttributes.self) { context in
// Lock screen/banner UI
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading, spacing: 4) {
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
Label("\(Int(speed)) \(speedUnit)", systemImage: "speedometer")
.font(.caption)
Label("\(context.state.heartRate) bpm", systemImage: "heart.fill")
.font(.caption)
.foregroundColor(.red)
}
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing, spacing: 4) {
Label("\(Int(context.state.power)) W", systemImage: "bolt.fill")
.font(.caption)
.foregroundColor(.yellow)
Label("\(Int(context.state.cadence)) rpm", systemImage: "arrow.clockwise")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.center) {
// Empty or can add more info
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
let distanceKm = context.state.distance / 1000.0
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
Label(String(format: "%.2f \(distanceUnit)", distance), systemImage: "map")
Spacer()
Label("\(Int(context.state.kcal)) kcal", systemImage: "flame.fill")
.foregroundColor(.orange)
}
.font(.caption)
.padding(.horizontal)
}
} compactLeading: {
// Compact leading (left side of Dynamic Island)
HStack(spacing: 2) {
Image(systemName: "heart.fill")
.foregroundColor(.red)
Text("\(context.state.heartRate)")
.font(.caption2)
}
} compactTrailing: {
// Compact trailing (right side of Dynamic Island)
HStack(spacing: 2) {
Image(systemName: "bolt.fill")
.foregroundColor(.yellow)
Text("\(Int(context.state.power))")
.font(.caption2)
}
} minimal: {
// Minimal view (when multiple activities)
Image(systemName: "figure.run")
}
}
}
}
// MARK: - Lock Screen View
@available(iOS 16.1, *)
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<QZWorkoutAttributes>
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "figure.indoor.cycle")
.foregroundColor(.blue)
Text(context.attributes.deviceName)
.font(.headline)
Spacer()
}
HStack(spacing: 16) {
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
MetricView(icon: "speedometer", value: String(format: "%.1f", speed), unit: speedUnit)
MetricView(icon: "heart.fill", value: "\(context.state.heartRate)", unit: "bpm", color: .red)
MetricView(icon: "bolt.fill", value: "\(Int(context.state.power))", unit: "W", color: .yellow)
}
HStack(spacing: 16) {
let distanceKm = context.state.distance / 1000.0
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
MetricView(icon: "arrow.clockwise", value: "\(Int(context.state.cadence))", unit: "rpm")
MetricView(icon: "map", value: String(format: "%.2f", distance), unit: distanceUnit)
MetricView(icon: "flame.fill", value: "\(Int(context.state.kcal))", unit: "kcal", color: .orange)
}
}
.padding()
}
}
// MARK: - Metric View Component
struct MetricView: View {
let icon: String
let value: String
let unit: String
var color: Color = .primary
var body: some View {
VStack(spacing: 2) {
Image(systemName: icon)
.foregroundColor(color)
.font(.caption)
Text(value)
.font(.system(.body, design: .rounded))
.fontWeight(.semibold)
Text(unit)
.font(.caption2)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - Preview
@available(iOS 16.1, *)
struct QZWidgetLiveActivity_Previews: PreviewProvider {
static let attributes = QZWorkoutAttributes(deviceName: "QZ Bike", useMiles: false)
static let contentState = QZWorkoutAttributes.ContentState(
speed: 25.5,
cadence: 85,
power: 200,
heartRate: 145,
distance: 12500, // meters (will be displayed as 12.50 km or 7.77 mi)
kcal: 320,
useMiles: false
)
static var previews: some View {
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Island Compact")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Island Expanded")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Minimal")
attributes
.previewContext(contentState, viewKind: .content)
.previewDisplayName("Lock Screen")
}
}

View File

@@ -0,0 +1,41 @@
//
// QZWorkoutAttributes.swift
// QDomyos-Zwift
//
// Shared attributes for Live Activities
// MUST be included in both qdomyoszwift and QZWidget targets
//
import Foundation
import ActivityKit
@available(iOS 16.1, *)
public struct QZWorkoutAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
public var speed: Double
public var cadence: Double
public var power: Double
public var heartRate: Int
public var distance: Double
public var kcal: Double
public var useMiles: Bool
public init(speed: Double, cadence: Double, power: Double, heartRate: Int, distance: Double, kcal: Double, useMiles: Bool) {
self.speed = speed
self.cadence = cadence
self.power = power
self.heartRate = heartRate
self.distance = distance
self.kcal = kcal
self.useMiles = useMiles
}
}
public var deviceName: String
public var useMiles: Bool
public init(deviceName: String, useMiles: Bool) {
self.deviceName = deviceName
self.useMiles = useMiles
}
}

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -130,6 +130,7 @@
87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */; };
870A5DB32CEF8FB100839641 /* moc_technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */; };
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB42CEF8FD200839641 /* technogymbike.cpp */; };
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870C72632E91565E00DC8A84 /* ios_liveactivity.mm */; };
8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706B29C48AEA0094D0F3 /* handleurl.cpp */; };
8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */; };
8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710707229C4A5E70094D0F3 /* GarminConnect.swift */; };
@@ -286,6 +287,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 +343,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 +393,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 */; };
@@ -399,6 +407,8 @@
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */; };
878A331A25AB4FF800BD13E1 /* yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331725AB4FF800BD13E1 /* yesoulbike.cpp */; };
878A331D25AB50C300BD13E1 /* moc_yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */; };
878C9DC92DF01C16001114D5 /* speraxtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */; };
878C9DCA2DF01C16001114D5 /* moc_speraxtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */; };
878C9E6928B77E7C00669129 /* nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */; };
878C9E6B28B77E9800669129 /* moc_nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */; };
878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878D83732A1F33C600D7F004 /* bkoolbike.cpp */; };
@@ -452,6 +462,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 */; };
@@ -578,8 +590,20 @@
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 */; };
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87EFC5652E918D35005BB573 /* WidgetKit.framework */; };
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */; };
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */; };
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
87EFE45B27A51901006EA1C3 /* moc_nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */; };
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
@@ -687,6 +711,13 @@
remoteGlobalIDString = 876E4E102594747F00BD5714;
remoteInfo = watchkit;
};
87EFC5742E918D38005BB573 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 6DB9C3763D02B1415CD9D565 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 87EFC5632E918D35005BB573;
remoteInfo = QZWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -712,6 +743,17 @@
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
99542592E9780B9225F24AA8 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -957,6 +999,8 @@
87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusbike.cpp; sourceTree = "<group>"; };
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymbike.cpp; sourceTree = "<group>"; };
870A5DB42CEF8FD200839641 /* technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = technogymbike.cpp; path = ../src/devices/technogymbike/technogymbike.cpp; sourceTree = SOURCE_ROOT; };
870C72622E91565E00DC8A84 /* ios_liveactivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_liveactivity.h; path = ../src/ios/ios_liveactivity.h; sourceTree = SOURCE_ROOT; };
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_liveactivity.mm; path = ../src/ios/ios_liveactivity.mm; sourceTree = SOURCE_ROOT; };
8710706A29C48AE90094D0F3 /* handleurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = handleurl.h; path = ../src/handleurl.h; sourceTree = "<group>"; };
8710706B29C48AEA0094D0F3 /* handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = handleurl.cpp; path = ../src/handleurl.cpp; sourceTree = "<group>"; };
8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_handleurl.cpp; sourceTree = "<group>"; };
@@ -1194,6 +1238,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>"; };
@@ -1280,6 +1327,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>"; };
@@ -1363,6 +1413,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>"; };
@@ -1382,6 +1436,9 @@
878A331725AB4FF800BD13E1 /* yesoulbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = yesoulbike.cpp; path = ../src/devices/yesoulbike/yesoulbike.cpp; sourceTree = "<group>"; };
878A331825AB4FF800BD13E1 /* yesoulbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = yesoulbike.h; path = ../src/devices/yesoulbike/yesoulbike.h; sourceTree = "<group>"; };
878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_yesoulbike.cpp; sourceTree = "<group>"; };
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_speraxtreadmill.cpp; sourceTree = "<group>"; };
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = speraxtreadmill.h; path = ../src/devices/speraxtreadmill/speraxtreadmill.h; sourceTree = SOURCE_ROOT; };
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = speraxtreadmill.cpp; path = ../src/devices/speraxtreadmill/speraxtreadmill.cpp; sourceTree = SOURCE_ROOT; };
878C9E6728B77E7B00669129 /* nordictrackifitadbbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbbike.h; path = ../src/devices/nordictrackifitadbbike/nordictrackifitadbbike.h; sourceTree = "<group>"; };
878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbbike.cpp; path = ../src/devices/nordictrackifitadbbike/nordictrackifitadbbike.cpp; sourceTree = "<group>"; };
878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbbike.cpp; sourceTree = "<group>"; };
@@ -1466,6 +1523,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>"; };
@@ -1648,9 +1708,23 @@
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>"; };
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QZWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
87EFC5652E918D35005BB573 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveActivityBridge.swift; path = ../src/ios/LiveActivityBridge.swift; sourceTree = SOURCE_ROOT; };
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QZWidgetExtension.entitlements; sourceTree = "<group>"; };
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QZWorkoutAttributes.swift; path = QZWidget/QZWorkoutAttributes.swift; sourceTree = "<group>"; };
87EFE45727A518F5006EA1C3 /* nautiluselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nautiluselliptical.h; path = ../src/devices/nautiluselliptical/nautiluselliptical.h; sourceTree = "<group>"; };
87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nautiluselliptical.cpp; path = ../src/devices/nautiluselliptical/nautiluselliptical.cpp; sourceTree = "<group>"; };
87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nautiluselliptical.cpp; sourceTree = "<group>"; };
@@ -1887,6 +1961,20 @@
FF5BDAB0076F3391B219EA52 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = "<absolute>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
87EFC5682E918D35005BB573 /* QZWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = QZWidget; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
876E4E172594748000BD5714 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -1895,10 +1983,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5612E918D35005BB573 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */,
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D1C883685E82D5676953459A /* Link Binary With Libraries */ = {
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 */,
@@ -2235,6 +2333,34 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */,
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */,
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 */,
87F1BD6E2DC0D59600416506 /* coresensor.h */,
87F1BD6F2DC0D59600416506 /* coresensor.cpp */,
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */,
@@ -2932,6 +3058,7 @@
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
isa = PBXGroup;
children = (
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */,
8768C9262BBC12D10099DBE1 /* libcrypto.a */,
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */,
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */,
@@ -3063,6 +3190,7 @@
4D765E1B1EA6C757220C63E7 /* CoreFoundation.framework */,
FCC237CA5AD60B9BA4447615 /* Foundation.framework */,
344F66310C19536DB4886D8F /* qtpcre2 */,
87EFC5652E918D35005BB573 /* WidgetKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -3325,6 +3453,7 @@
E8C543AB96796ECAA2E65C57 /* qdomyoszwift */ = {
isa = PBXGroup;
children = (
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */,
8768C8CE2BBC12170099DBE1 /* adb */,
87BAC3BE2BA497160003E925 /* PrivacyInfo.xcprivacy */,
8745B2752AFCB4A300991A39 /* android */,
@@ -3335,6 +3464,7 @@
74B182DB50CB5611B5C1C297 /* Supporting Files */,
876E4E122594747F00BD5714 /* watchkit */,
876E4E1E2594748000BD5714 /* watchkit Extension */,
87EFC5682E918D35005BB573 /* QZWidget */,
AF39DD055C3EF8226FBE929D /* Frameworks */,
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
FE0A091FDBFB3E9C31B7A1BD /* Products */,
@@ -3349,6 +3479,7 @@
040B10E2EF2CEF79F2205FE2 /* qdomyoszwift.app */,
876E4E112594747F00BD5714 /* watchkit.app */,
876E4E1A2594748000BD5714 /* watchkit Extension.appex */,
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -3365,11 +3496,13 @@
30414803F31797EB689AE508 /* Copy Bundle Resources */,
99542592E9780B9225F24AA8 /* Embed Frameworks */,
876E4E332594748100BD5714 /* Embed Watch Content */,
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
876E4E312594748100BD5714 /* PBXTargetDependency */,
87EFC5752E918D38005BB573 /* PBXTargetDependency */,
);
name = qdomyoszwift;
packageProductDependencies = (
@@ -3415,6 +3548,28 @@
productReference = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */;
productType = "com.apple.product-type.watchkit2-extension";
};
87EFC5632E918D35005BB573 /* QZWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */;
buildPhases = (
87EFC5602E918D35005BB573 /* Sources */,
87EFC5612E918D35005BB573 /* Frameworks */,
87EFC5622E918D35005BB573 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
87EFC5682E918D35005BB573 /* QZWidget */,
);
name = QZWidgetExtension;
packageProductDependencies = (
);
productName = QZWidgetExtension;
productReference = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -3422,7 +3577,7 @@
isa = PBXProject;
attributes = {
DefaultBuildSystemTypeForWorkspace = Original;
LastSwiftUpdateCheck = 1220;
LastSwiftUpdateCheck = 2600;
TargetAttributes = {
799833E5566DEFFC37E4BF1E = {
DevelopmentTeam = 6335M7T29D;
@@ -3438,6 +3593,9 @@
DevelopmentTeam = 6335M7T29D;
ProvisioningStyle = Automatic;
};
87EFC5632E918D35005BB573 = {
CreatedOnToolsVersion = 26.0.1;
};
};
};
buildConfigurationList = DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */;
@@ -3459,6 +3617,7 @@
799833E5566DEFFC37E4BF1E /* qdomyoszwift */,
876E4E102594747F00BD5714 /* watchkit */,
876E4E192594748000BD5714 /* watchkit Extension */,
87EFC5632E918D35005BB573 /* QZWidgetExtension */,
);
};
/* End PBXProject section */
@@ -3505,6 +3664,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5622E918D35005BB573 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -3522,10 +3688,19 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5602E918D35005BB573 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F7E50F631C51CD5B5DC0BC43 /* Compile Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */,
8738249627E646E3004F1B46 /* characteristicnotifier2acd.cpp in Compile Sources */,
8738249127E646E3004F1B46 /* dirconpacket.cpp in Compile Sources */,
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
@@ -3672,12 +3847,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 */,
@@ -3736,6 +3915,8 @@
8720890E2CE65451008C2C17 /* qmqttsubscriptionproperties.cpp in Compile Sources */,
8720890F2CE65451008C2C17 /* qmqttconnectionproperties.cpp in Compile Sources */,
872089102CE65451008C2C17 /* qmqttconnection.cpp in Compile Sources */,
878C9DC92DF01C16001114D5 /* speraxtreadmill.cpp in Compile Sources */,
878C9DCA2DF01C16001114D5 /* moc_speraxtreadmill.cpp in Compile Sources */,
872089112CE65451008C2C17 /* qmqttsubscription.cpp in Compile Sources */,
872089122CE65451008C2C17 /* qmqttclient.cpp in Compile Sources */,
872089132CE65451008C2C17 /* qmqtttopicfilter.cpp in Compile Sources */,
@@ -3768,6 +3949,7 @@
87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */,
87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */,
ACB47DC464A2BC9D39C544AD /* gpx.cpp in Compile Sources */,
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */,
6361329E515248BB41640C07 /* homeform.cpp in Compile Sources */,
87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */,
87C5F0D026285E7E0067A1B5 /* moc_smtpclient.cpp in Compile Sources */,
@@ -3795,6 +3977,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 */,
@@ -3854,6 +4042,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 */,
@@ -3870,6 +4060,9 @@
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 */,
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift 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 */,
@@ -4048,6 +4241,11 @@
target = 876E4E102594747F00BD5714 /* watchkit */;
targetProxy = 876E4E302594748100BD5714 /* PBXContainerItemProxy */;
};
87EFC5752E918D38005BB573 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
targetProxy = 87EFC5742E918D38005BB573 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -4371,7 +4569,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1088;
CURRENT_PROJECT_VERSION = 1206;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4407,6 +4606,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,
@@ -4452,8 +4652,9 @@
/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.18;
MARKETING_VERSION = 2.20;
OTHER_CFLAGS = (
"-pipe",
"-g",
@@ -4546,7 +4747,11 @@
QMAKE_PKGINFO_TYPEINFO = "????";
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = "";
REGISTER_APP_GROUPS = YES;
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";
@@ -4565,8 +4770,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1088;
CURRENT_PROJECT_VERSION = 1206;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = 0;
@@ -4603,6 +4809,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,
@@ -4648,8 +4855,9 @@
/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.18;
MARKETING_VERSION = 2.20;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"-pipe",
@@ -4743,7 +4951,11 @@
QMAKE_PKGINFO_TYPEINFO = "????";
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = _debug;
REGISTER_APP_GROUPS = YES;
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";
@@ -4795,7 +5007,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1088;
CURRENT_PROJECT_VERSION = 1206;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4820,7 +5032,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.20;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4846,7 +5058,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 5.0;
WATCHOS_DEPLOYMENT_TARGET = 6.0;
};
name = Debug;
};
@@ -4891,7 +5103,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1088;
CURRENT_PROJECT_VERSION = 1206;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -4912,7 +5124,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.20;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4938,7 +5150,7 @@
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
VALIDATE_PRODUCT = YES;
WATCHOS_DEPLOYMENT_TARGET = 5.0;
WATCHOS_DEPLOYMENT_TARGET = 6.0;
};
name = Release;
};
@@ -4983,7 +5195,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1088;
CURRENT_PROJECT_VERSION = 1206;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5028,7 +5240,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.20;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -5054,7 +5266,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 5.0;
WATCHOS_DEPLOYMENT_TARGET = 6.0;
};
name = Debug;
};
@@ -5099,7 +5311,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1088;
CURRENT_PROJECT_VERSION = 1206;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5140,7 +5352,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.20;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -5167,7 +5379,185 @@
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
VALIDATE_PRODUCT = YES;
WATCHOS_DEPLOYMENT_TARGET = 5.0;
WATCHOS_DEPLOYMENT_TARGET = 6.0;
};
name = Release;
};
87EFC5792E918D38005BB573 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QZWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
SwiftUI,
"-framework",
WidgetKit,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
87EFC57A2E918D38005BB573 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1206;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QZWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
SwiftUI,
"-framework",
WidgetKit,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
@@ -5201,6 +5591,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
87EFC5792E918D38005BB573 /* Debug */,
87EFC57A2E918D38005BB573 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */ = {
isa = XCConfigurationList;
buildConfigurations = (

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
@@ -54,20 +55,26 @@ extension WorkoutTracking {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .distanceCycling):
let distanceUnit = HKUnit.mile()
let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
delegate?.didReceiveHealthKitDistanceCycling(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
let energyUnit = HKUnit.kilocalorie()
let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
delegate?.didReceiveHealthKitActiveEnergyBurned(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
delegate?.didReceiveHealthKitHeartRate(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .stepCount):
@@ -160,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)!,
@@ -179,6 +187,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.workoutType()
])
}
@@ -217,23 +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 sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: workoutSession.startDate!,
end: Date())
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
workoutBuilder.add([sample]) {(success, error) in}
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
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,
@@ -249,7 +265,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: workoutSession.startDate!,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -265,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")
}
}
}
@@ -281,7 +301,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleSteps = HKCumulativeQuantitySeriesSample(
type: quantityTypeSteps,
quantity: stepsQuantity,
start: workoutSession.startDate!,
start: startDate,
end: Date())
// Add the steps sample to workout builder
@@ -310,7 +330,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: typeDistance,
quantity: quantityMiles,
start: workoutSession.startDate!,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -326,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")
}
}
}
@@ -343,7 +367,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleSteps = HKCumulativeQuantitySeriesSample(
type: quantityTypeSteps,
quantity: stepsQuantity, // Use your steps quantity here
start: workoutSession.startDate!,
start: startDate,
end: Date())
// Add the steps sample to workout builder
@@ -375,7 +399,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: workoutSession.startDate!,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -391,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/

4
src/CLAUDE.md Normal file
View File

@@ -0,0 +1,4 @@
when you add a setting remember:
- you have to add always as the last settings declared in the settings.qml
- if you have to add a setting also on another qml file, you need also to declare it there always putting as the last one
- in the qzsettings.cpp there is a allsettingscount that must be updated if you add a setting

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

@@ -92,7 +92,7 @@ class BluetoothHandler : public QObject
void onKeyPressed(int keyCode)
{
qDebug() << "Key pressed:" << keyCode;
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == bluetoothdevice::BIKE) {
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == BIKE) {
if (keyCode == 115) // up
((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() + 1);
else if (keyCode == 114) // down

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

View File

@@ -72,7 +72,19 @@ HomeForm {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("New lap started!")
}
}
}
}
MessageDialog {
id: stopConfirmationDialog
text: qsTr("Stop Workout")
informativeText: qsTr("Do you really want to stop the current workout?")
buttons: (MessageDialog.Yes | MessageDialog.No)
onYesClicked: {
close();
inner_stop();
}
onNoClicked: close()
}
Timer {
@@ -141,7 +153,11 @@ HomeForm {
start.onClicked: { start_clicked(); }
stop.onClicked: {
inner_stop();
if (rootItem.confirmStopEnabled()) {
stopConfirmationDialog.open();
} else {
inner_stop();
}
}
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }

View File

@@ -165,7 +165,7 @@ Page {
width: parent.width
anchors.top: row1.bottom
anchors.topMargin: 30
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) bluetooth and bluetooth permission MUST be on<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) both Bluetooth and Bluetooth permissions MUST be enabled<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
wrapMode: Label.WordWrap
visible: rootItem.labelHelp
}

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.18.24" android:versionCode="1061" 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.12" android:versionCode="1204" 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 -->
@@ -10,7 +10,7 @@
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon" android:usesCleartextTraffic="true">
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="QZ" android:launchMode="singleTop">
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.cagnulen.qdomyoszwift.CustomQtActivity" android:label="QZ" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -120,7 +120,7 @@
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="36" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>

View File

@@ -44,7 +44,7 @@ dependencies {
def appcompat_version = "1.3.1"
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "com.android.billingclient:billing:6.0.1"
implementation "com.android.billingclient:billing:8.0.0"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation "androidx.appcompat:appcompat:$appcompat_version"
@@ -129,7 +129,7 @@ android {
resConfig "en"
compileSdkVersion 33
minSdkVersion = 21
targetSdkVersion = 34
targetSdkVersion = 36
}
tasks.all { task ->

Binary file not shown.

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");
@@ -152,4 +158,15 @@ public class Ant {
QLog.v(TAG, "isBikeConnected");
return mChannelService.isBikeConnected();
}
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if(mChannelService == null)
return;
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
inclination);
}
}

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

@@ -0,0 +1,651 @@
/*
* Copyright 2012 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import org.cagnulen.qdomyoszwift.QLog;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
import com.dsi.ant.channel.IAntChannelEventHandler;
import com.dsi.ant.message.ChannelId;
import com.dsi.ant.message.ChannelType;
import com.dsi.ant.message.EventCode;
import com.dsi.ant.message.fromant.AcknowledgedDataMessage;
import com.dsi.ant.message.fromant.ChannelEventMessage;
import com.dsi.ant.message.fromant.MessageFromAntType;
import com.dsi.ant.message.ipc.AntMessageParcel;
import android.os.RemoteException;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.Random;
/**
* ANT+ Bike Transmitter Controller
* Follows exactly the same pattern as PowerChannelController but for Fitness Equipment
*/
public class BikeTransmitterController {
public static final int FITNESS_EQUIPMENT_SENSOR_ID = 0x9e3d4b67; // Different from power sensor
// The device type and transmission type to be part of the channel ID message
private static final int CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE = 17; // Fitness Equipment
private static final int CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE = 5;
// The period and frequency values the channel will be configured to
private static final int CHANNEL_FITNESS_EQUIPMENT_PERIOD = 8192; // 4 Hz for FE
private static final int CHANNEL_FITNESS_EQUIPMENT_FREQUENCY = 57;
private static final String TAG = BikeTransmitterController.class.getSimpleName();
// ANT+ Data Page IDs for Fitness Equipment
private static final byte DATA_PAGE_GENERAL_FE = 0x10;
private static final byte DATA_PAGE_BIKE_DATA = 0x19;
private static final byte DATA_PAGE_TRAINER_DATA = 0x1A;
private static final byte DATA_PAGE_GENERAL_SETTINGS = 0x11;
private static Random randGen = new Random();
// Current bike metrics to transmit
int currentCadence = 0; // Current cadence in RPM
int currentPower = 0; // Current power in watts
double currentSpeedKph = 0.0; // Current speed in km/h
long totalDistance = 0; // Total distance in meters
int currentHeartRate = 0; // Heart rate in BPM
double elapsedTimeSeconds = 0.0; // Elapsed time in seconds
int currentResistance = 0; // Current resistance level (0-100)
double currentInclination = 0.0; // Current inclination in percentage
// Control commands received from ANT+ devices
private int requestedResistance = -1; // Requested resistance from controller
private int requestedPower = -1; // Requested power from controller
private double requestedInclination = -100; // Requested inclination from controller
private AntChannel mAntChannel;
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
private boolean mIsOpen;
// Callbacks for control commands
public interface ControlCommandListener {
void onResistanceChangeRequested(int resistance);
void onPowerChangeRequested(int power);
void onInclinationChangeRequested(double inclination);
}
private ControlCommandListener controlListener = null;
public BikeTransmitterController(AntChannel antChannel) {
mAntChannel = antChannel;
openChannel();
}
/**
* Set the listener for control commands received from ANT+ devices
*/
public void setControlCommandListener(ControlCommandListener listener) {
this.controlListener = listener;
}
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
QLog.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type
ChannelId channelId = new ChannelId(FITNESS_EQUIPMENT_SENSOR_ID & 0xFFFF,
CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE, CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE);
try {
// Setting the channel event handler so that we can receive messages from ANT
mAntChannel.setChannelEventHandler(mChannelEventCallback);
// Performs channel assignment by assigning the type to the channel
mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER);
// Configures the channel ID, messaging period and rf frequency after assigning,
// then opening the channel.
mAntChannel.setChannelId(channelId);
mAntChannel.setPeriod(CHANNEL_FITNESS_EQUIPMENT_PERIOD);
mAntChannel.setRfFrequency(CHANNEL_FITNESS_EQUIPMENT_FREQUENCY);
mAntChannel.open();
mIsOpen = true;
QLog.d(TAG, "Opened fitness equipment channel with device number: " + FITNESS_EQUIPMENT_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
// This will release, and therefore unassign if required
channelError("Open failed", e);
}
}
} else {
QLog.w(TAG, "No channel available");
}
return mIsOpen;
}
public boolean startTransmission() {
return openChannel();
}
public void stopTransmission() {
close();
}
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
QLog.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
StringBuilder logString;
if (e.getResponseMessage() != null) {
String initiatingMessageId = "0x" + Integer.toHexString(
e.getResponseMessage().getInitiatingMessageId());
String rawResponseCode = "0x" + Integer.toHexString(
e.getResponseMessage().getRawResponseCode());
logString = new StringBuilder(error)
.append(". Command ")
.append(initiatingMessageId)
.append(" failed with code ")
.append(rawResponseCode);
} else {
String attemptedMessageId = "0x" + Integer.toHexString(
e.getAttemptedMessageType().getMessageId());
String failureReason = e.getFailureReason().toString();
logString = new StringBuilder(error)
.append(". Command ")
.append(attemptedMessageId)
.append(" failed with reason ")
.append(failureReason);
}
QLog.e(TAG, logString.toString());
mAntChannel.release();
}
public void close() {
if (null != mAntChannel) {
mIsOpen = false;
// Releasing the channel to make it available for others.
// After releasing, the AntChannel instance cannot be reused.
mAntChannel.release();
mAntChannel = null;
}
QLog.e(TAG, "Fitness Equipment Channel Closed");
}
// Setter methods for updating bike metrics from the main application
public void setCadence(int cadence) {
this.currentCadence = Math.max(0, cadence);
}
public void setPower(int power) {
this.currentPower = Math.max(0, power);
}
public void setSpeedKph(double speedKph) {
this.currentSpeedKph = Math.max(0, speedKph);
}
public void setDistance(long distance) {
this.totalDistance = Math.max(0, distance);
}
public void setHeartRate(int heartRate) {
this.currentHeartRate = Math.max(0, Math.min(255, heartRate));
}
public void setElapsedTime(double timeSeconds) {
this.elapsedTimeSeconds = Math.max(0, timeSeconds);
}
public void setResistance(int resistance) {
this.currentResistance = Math.max(0, Math.min(100, resistance));
}
public void setInclination(double inclination) {
this.currentInclination = Math.max(-100, Math.min(100, inclination));
}
// Getter methods for the last requested control values
public int getRequestedResistance() {
return requestedResistance;
}
public int getRequestedPower() {
return requestedPower;
}
public double getRequestedInclination() {
return requestedInclination;
}
public void clearControlRequests() {
requestedResistance = -1;
requestedPower = -1;
requestedInclination = -100;
}
public boolean isTransmitting() {
return mIsOpen;
}
public String getTransmissionInfo() {
if (!mIsOpen) {
return "Transmission: STOPPED";
}
return String.format("Transmission: ACTIVE - Cadence: %drpm, Power: %dW, " +
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%",
currentCadence, currentPower, currentSpeedKph,
currentResistance, currentInclination);
}
/**
* Helper method to convert byte array to hex string for debugging
*/
private String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02X ", b & 0xFF));
}
return hex.toString().trim();
}
/**
* Implements the Channel Event Handler Interface following PowerChannelController pattern
*/
public class ChannelEventCallback implements IAntChannelEventHandler {
int cnt = 0;
int eventCount = 0;
int eventPowerCount = 0;
int cumulativeDistance = 0;
int cumulativeWatt = 0;
int accumulatedTorque32 = 0;
Timer carousalTimer = null;
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
QLog.e(TAG, "Fitness Equipment Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
byte[] payload = new byte[8];
// Start unsolicited transmission timer like PowerChannelController
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
QLog.d(TAG, "Tx Unsolicited Fitness Equipment Data");
byte[] payload = new byte[8];
String debugString = "";
eventCount = (eventCount + 1) & 0xFF;
cumulativeDistance = (cumulativeDistance + (int)(currentSpeedKph / 3.6)) & 0xFFFF; // rough distance calc
cnt += 1;
// Cycle through different data pages like PowerChannelController
if (cnt % 5 == 0) {
// General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
} else if (cnt % 5 == 1) {
// Bike Data Page (0x19)
debugString = buildBikeDataPage(payload);
} else if (cnt % 5 == 2) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
} else if (cnt % 5 == 3) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}
// Log the hex data and parsed values
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
QLog.d(TAG, debugString);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
}
}, 0, 250); // Every 250ms for 4Hz
}
// Switching on message type to handle different types of messages
switch (messageType) {
case BROADCAST_DATA:
// Rx Data
break;
case ACKNOWLEDGED_DATA:
// Handle control commands
payload = new AcknowledgedDataMessage(antParcel).getPayload();
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
handleControlCommand(payload);
break;
case CHANNEL_EVENT:
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
case TX:
cnt += 1;
String debugString = "";
// Cycle through different data pages like PowerChannelController
if (cnt % 16 == 1) {
// General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
} else if (cnt % 16 == 5) {
// Bike Data Page (0x19)
debugString = buildBikeDataPage(payload);
} else if (cnt % 16 == 9) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
} else if (cnt % 16 == 13) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}
// Log the hex data and parsed values
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
QLog.d(TAG, debugString);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
break;
case CHANNEL_COLLISION:
cnt += 1;
break;
case RX_SEARCH_TIMEOUT:
QLog.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:
case RX_FAIL_GO_TO_SEARCH:
case TRANSFER_RX_FAILED:
case TRANSFER_TX_COMPLETED:
case TRANSFER_TX_FAILED:
case TRANSFER_TX_START:
case UNKNOWN:
// TODO More complex communication will need to handle these events
break;
}
break;
case ANT_VERSION:
case BURST_TRANSFER_DATA:
case CAPABILITIES:
case CHANNEL_ID:
case CHANNEL_RESPONSE:
case CHANNEL_STATUS:
case SERIAL_NUMBER:
case OTHER:
// TODO More complex communication will need to handle these message types
break;
}
}
/**
* Build General Fitness Equipment Data Page (0x10) - Page 16
* Following Table 8-7 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildGeneralFEDataPage(byte[] payload) {
payload[0] = 0x10; // Data Page Number = 0x10 (Page 16)
// Byte 1: Equipment Type Bit Field (Refer to Table 8-8)
payload[1] = 0x19; // Equipment type: Bike (stationary bike = 0x19)
// Byte 2: Elapsed Time (0.25 seconds resolution, rollover at 64s)
int elapsedTime025s = (int) (elapsedTimeSeconds * 4) & 0xFF;
payload[2] = (byte) elapsedTime025s;
// Byte 3: Distance Traveled (1 meter resolution, rollover at 256m)
int distanceMeters = (int) (totalDistance) & 0xFF;
payload[3] = (byte) distanceMeters;
// Bytes 4-5: Speed (0.001 m/s resolution, 0xFFFF = invalid)
int speedMms = (int) (currentSpeedKph / 3.6 * 1000);
if (speedMms > 65534) speedMms = 65534; // Max valid value
payload[4] = (byte) (speedMms & 0xFF); // Speed LSB
payload[5] = (byte) ((speedMms >> 8) & 0xFF); // Speed MSB
// Byte 6: Heart Rate (0xFF = invalid)
payload[6] = (byte) (currentHeartRate == 0 ? 0xFF : currentHeartRate);
// Byte 7: Capabilities Bit Field (4 bits 0:3) + FE State Bit Field (4 bits 4:7)
payload[7] = 0x00; // Set to 0x00 for now (refer to Tables 8-9 and 8-10)
// Create debug string
return String.format(Locale.US,
"General FE Data Page (0x10): " +
"Page=0x%02X, Equipment=0x%02X(Bike), " +
"ElapsedTime=0x%02X(%.1fs), Distance=0x%02X(%dm), " +
"Speed=0x%02X%02X(%.1fkm/h), HeartRate=0x%02X(%s), " +
"Capabilities=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF,
payload[2] & 0xFF, elapsedTimeSeconds,
payload[3] & 0xFF, distanceMeters,
payload[5] & 0xFF, payload[4] & 0xFF, currentSpeedKph,
payload[6] & 0xFF, currentHeartRate == 0 ? "Invalid" : currentHeartRate + "bpm",
payload[7] & 0xFF);
}
/**
* Build Specific Trainer/Stationary Bike Data Page (0x19) - Page 25
* Following Table 8-25 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildBikeDataPage(byte[] payload) {
payload[0] = 0x19; // Data Page Number = 0x19 (Page 25)
// Byte 1: Update Event Count (increments with each information update)
eventPowerCount = (eventPowerCount + 1) & 0xFF;
payload[1] = (byte) eventPowerCount;
// Byte 2: Instantaneous Cadence (RPM, 0xFF = invalid)
payload[2] = (byte) (currentCadence == 0 ? 0xFF : currentCadence);
// Bytes 3-4: Accumulated Power (1 watt resolution, rollover at 65536W)
// This is cumulative power, not instantaneous
cumulativeWatt = (cumulativeWatt + currentPower);
payload[3] = (byte) (cumulativeWatt & 0xFF); // Accumulated Power LSB
payload[4] = (byte) ((cumulativeWatt >> 8) & 0xFF); // Accumulated Power MSB
// Bytes 5-6: Instantaneous Power (1.5 bytes, 0xFFF = invalid for both fields)
if (currentPower > 4094) {
// 0xFFF indicates BOTH instantaneous and accumulated power fields are invalid
payload[5] = (byte) 0xFF; // Instantaneous Power LSB
payload[6] = (byte) 0xFF; // Instantaneous Power MSB (bits 0-3) + Trainer Status (bits 4-7)
} else {
payload[5] = (byte) (currentPower & 0xFF); // Instantaneous Power LSB
payload[6] = (byte) ((currentPower >> 8) & 0x0F); // Instantaneous Power MSN (bits 0-3)
// Bits 4-7 of byte 6: Trainer Status Bit Field (refer to Table 8-27)
payload[6] |= 0x00; // Trainer status = 0 for now
}
// Byte 7: Flags Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
// Create debug string
String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "rpm";
String powerStr = currentPower > 4094 ? "Invalid" : currentPower + "W";
return String.format(Locale.US,
"Bike Data Page (0x19): " +
"Page=0x%02X, EventCount=0x%02X(%d), " +
"Cadence=0x%02X(%s), AccumPower=0x%02X%02X(%dW), " +
"InstPower=0x%X%02X(%s), Flags=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF, eventCount,
payload[2] & 0xFF, cadenceStr,
payload[4] & 0xFF, payload[3] & 0xFF, cumulativeWatt,
(payload[6] & 0x0F), payload[5] & 0xFF, powerStr,
payload[7] & 0xFF);
}
/**
* Build General Settings Page (0x11) - Page 17
* Following Table 8-11 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildGeneralSettingsPage(byte[] payload) {
payload[0] = 0x11; // Data Page Number = 0x11 (Page 17)
// Byte 1: Reserved (0xFF - Do not interpret)
payload[1] = (byte) 0xFF;
// Byte 2: Reserved (0xFF - Do not interpret)
payload[2] = (byte) 0xFF;
// Byte 3: Cycle length (0.01 meters resolution, 0xFF = invalid)
// Length of one 'cycle' - for bike this could be wheel circumference
int cycleLengthCm = 210; // 2.1m wheel circumference = 210cm
payload[3] = (byte) (cycleLengthCm & 0xFF);
// Bytes 4-5: Incline Percentage (signed integer, 0.01% resolution, 0x7FFF = invalid)
int inclinePercent001 = (int) (currentInclination * 100); // Convert to 0.01% units
if (inclinePercent001 < -10000) inclinePercent001 = -10000; // Min -100.00%
if (inclinePercent001 > 10000) inclinePercent001 = 10000; // Max +100.00%
payload[4] = (byte) (inclinePercent001 & 0xFF); // Incline LSB
payload[5] = (byte) ((inclinePercent001 >> 8) & 0xFF); // Incline MSB
// Byte 6: Resistance Level (0.5% resolution, percentage of maximum applicable resistance)
int resistanceLevel05 = (int) (currentResistance * 2); // Convert to 0.5% units
if (resistanceLevel05 > 200) resistanceLevel05 = 200; // Max 100% = 200 in 0.5% units
payload[6] = (byte) (resistanceLevel05 & 0xFF);
// Byte 7: Capabilities Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
// Create debug string
return String.format(Locale.US,
"General Settings Page (0x11): " +
"Page=0x%02X, Reserved1=0x%02X, Reserved2=0x%02X, " +
"CycleLength=0x%02X(%.2fm), Incline=0x%02X%02X(%.2f%%), " +
"Resistance=0x%02X(%d%%), Capabilities=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF, payload[2] & 0xFF,
payload[3] & 0xFF, cycleLengthCm / 100.0,
payload[5] & 0xFF, payload[4] & 0xFF, currentInclination,
payload[6] & 0xFF, currentResistance,
payload[7] & 0xFF);
}
/**
* Handle incoming control commands
*/
private void handleControlCommand(byte[] data) {
if (data.length < 8) return;
byte pageNumber = data[0];
QLog.d(TAG, "Received control command page: 0x" + String.format("%02X", pageNumber));
QLog.d(TAG, "Control Command HEX: " + bytesToHex(data));
// Handle control command pages
switch (pageNumber) {
case 0x30: // Basic Resistance
handleBasicResistanceCommand(data);
break;
case 0x31: // Target Power
handleTargetPowerCommand(data);
break;
case 0x33: // Track Resistance
handleTrackResistanceCommand(data);
break;
default:
QLog.d(TAG, "Unknown control page: 0x" + String.format("%02X", pageNumber));
break;
}
}
private void handleBasicResistanceCommand(byte[] data) {
int resistance = data[7] & 0xFF; // Resistance in 0.5% increments
double resistancePercent = resistance * 0.5;
QLog.d(TAG, String.format(Locale.US,
"Basic Resistance Command (0x30): Resistance=0x%02X(%.1f%%)",
resistance, resistancePercent));
if (resistancePercent != requestedResistance && controlListener != null) {
requestedResistance = (int) resistancePercent;
controlListener.onResistanceChangeRequested(requestedResistance);
}
}
private void handleTargetPowerCommand(byte[] data) {
int targetPower = ((data[7] & 0xFF) << 8) | (data[6] & 0xFF);
targetPower = targetPower / 4;
QLog.d(TAG, String.format(Locale.US,
"Target Power Command (0x31): Power=0x%02X%02X(%dW)",
data[7] & 0xFF, data[6] & 0xFF, targetPower));
if (targetPower != requestedPower && controlListener != null) {
requestedPower = targetPower;
controlListener.onPowerChangeRequested(targetPower);
}
}
private void handleTrackResistanceCommand(byte[] data) {
// Grade is in 0.01% increments, signed 16-bit
int gradeRaw = ((data[6] & 0xFF) << 8) | (data[5] & 0xFF);
if (gradeRaw > 32767) gradeRaw -= 65536; // Convert to signed
double grade = (gradeRaw - 0x4E20) * 0.01;
QLog.d(TAG, String.format(Locale.US,
"Track Resistance Command (0x33): Grade=0x%02X%02X(%.2f%%)",
data[6] & 0xFF, data[5] & 0xFF, grade));
if (Math.abs(grade - requestedInclination) > 0.1 && controlListener != null) {
requestedInclination = grade;
controlListener.onInclinationChangeRequested(grade);
}
}
}
}

View File

@@ -49,11 +49,16 @@ public class ChannelService extends Service {
private AntChannelProvider mAntChannelProvider = null;
private boolean mAllowAddChannel = false;
public static native void nativeSetResistance(int resistance);
public static native void nativeSetPower(int power);
public static native void nativeSetInclination(double inclination);
HeartChannelController heartChannelController = null;
PowerChannelController powerChannelController = null;
SpeedChannelController speedChannelController = null;
SDMChannelController sdmChannelController = null;
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
@Override
@@ -118,12 +123,20 @@ public class ChannelService extends Service {
if (null != sdmChannelController) {
sdmChannelController.speed = speed;
}
// Update bike transmitter with speed data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setSpeedKph(speed);
}
}
void setPower(int power) {
if (null != powerChannelController) {
powerChannelController.power = power;
}
// Update bike transmitter with power data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setPower(power);
}
}
void setCadence(int cadence) {
@@ -136,6 +149,10 @@ public class ChannelService extends Service {
if (null != sdmChannelController) {
sdmChannelController.cadence = cadence;
}
// Update bike transmitter with cadence data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setCadence(cadence);
}
}
int getHeart() {
@@ -182,6 +199,114 @@ public class ChannelService extends Service {
return (bikeChannelController != null && bikeChannelController.isConnected());
}
// ========== BIKE TRANSMITTER METHODS ==========
/**
* Start the bike transmitter (only available if not treadmill)
*/
boolean startBikeTransmitter() {
QLog.v(TAG, "ChannelServiceComm.startBikeTransmitter");
if (Ant.treadmill) {
QLog.w(TAG, "Bike transmitter not available in treadmill mode");
return false;
}
if (bikeTransmitterController != null) {
return bikeTransmitterController.startTransmission();
}
QLog.w(TAG, "Bike transmitter controller is null");
return false;
}
/**
* Stop the bike transmitter
*/
void stopBikeTransmitter() {
QLog.v(TAG, "ChannelServiceComm.stopBikeTransmitter");
if (bikeTransmitterController != null) {
bikeTransmitterController.stopTransmission();
}
}
/**
* Check if bike transmitter is active (only if not treadmill)
*/
boolean isBikeTransmitterActive() {
if (Ant.treadmill) {
return false;
}
return (bikeTransmitterController != null && bikeTransmitterController.isTransmitting());
}
/**
* Update bike transmitter with extended metrics (only if not treadmill)
*/
void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.setDistance(distanceMeters);
bikeTransmitterController.setHeartRate(heartRate);
bikeTransmitterController.setElapsedTime(elapsedTimeSeconds);
bikeTransmitterController.setResistance(resistance);
bikeTransmitterController.setInclination(inclination);
}
}
/**
* Get the last requested resistance from ANT+ controller (only if not treadmill)
*/
int getRequestedResistanceFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedResistance();
}
return -1;
}
/**
* Get the last requested power from ANT+ controller (only if not treadmill)
*/
int getRequestedPowerFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedPower();
}
return -1;
}
/**
* Get the last requested inclination from ANT+ controller (only if not treadmill)
*/
double getRequestedInclinationFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedInclination();
}
return -100.0;
}
/**
* Clear any pending control requests (only if not treadmill)
*/
void clearAntControlRequests() {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.clearControlRequests();
}
}
/**
* Get transmission info for debugging (only if not treadmill)
*/
String getBikeTransmitterInfo() {
if (Ant.treadmill) {
return "Bike transmitter disabled in treadmill mode";
}
if (bikeTransmitterController != null) {
return bikeTransmitterController.getTransmissionInfo();
}
return "Bike transmitter not initialized";
}
/**
* Closes all channels currently added.
*/
@@ -192,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) {
@@ -203,9 +328,70 @@ public class ChannelService extends Service {
}
}
// Add initialization for BikeChannelController
// 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
if (!Ant.treadmill && bikeTransmitterController == null) {
QLog.v(TAG, "Initializing BikeTransmitterController (not treadmill mode)");
try {
// Acquire channel like other controllers
AntChannel transmitterChannel = acquireChannel();
if (transmitterChannel != null) {
bikeTransmitterController = new BikeTransmitterController(transmitterChannel);
// Set up control command listener to handle requests from ANT+ devices
bikeTransmitterController.setControlCommandListener(new BikeTransmitterController.ControlCommandListener() {
@Override
public void onResistanceChangeRequested(int resistance) {
QLog.d(TAG, "ChannelService: ANT+ Resistance change requested: " + resistance);
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_RESISTANCE_CHANGE");
intent.putExtra("resistance", resistance);
nativeSetResistance(resistance);
sendBroadcast(intent);
}
@Override
public void onPowerChangeRequested(int power) {
QLog.d(TAG, "ChannelService: ANT+ Power change requested: " + power + "W");
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_POWER_CHANGE");
intent.putExtra("power", power);
nativeSetPower(power);
sendBroadcast(intent);
}
@Override
public void onInclinationChangeRequested(double inclination) {
QLog.d(TAG, "ChannelService: ANT+ Inclination change requested: " + inclination + "%");
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_INCLINATION_CHANGE");
intent.putExtra("inclination", inclination);
nativeSetInclination(inclination);
sendBroadcast(intent);
}
});
QLog.i(TAG, "BikeTransmitterController initialized successfully (bike mode)");
// Start the bike transmitter immediately after initialization
boolean transmissionStarted = bikeTransmitterController.startTransmission();
if (transmissionStarted) {
QLog.i(TAG, "BikeTransmitterController transmission started automatically");
} else {
QLog.w(TAG, "Failed to start BikeTransmitterController transmission");
}
} else {
QLog.e(TAG, "Failed to acquire channel for BikeTransmitterController");
}
} catch (Exception e) {
QLog.e(TAG, "Failed to initialize BikeTransmitterController: " + e.getMessage());
bikeTransmitterController = null;
}
}
}
@@ -220,12 +406,16 @@ public class ChannelService extends Service {
sdmChannelController.close();
if (bikeChannelController != null) // Added closing bikeChannelController
bikeChannelController.close();
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
bikeTransmitterController.close(); // Use close() method like other controllers
}
heartChannelController = null;
powerChannelController = null;
speedChannelController = null;
sdmChannelController = null;
bikeChannelController = null; // Added nullifying bikeChannelController
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
}
AntChannel acquireChannel() throws ChannelNotAvailableException {
@@ -364,4 +554,4 @@ public class ChannelService extends Service {
QLog.e(TAG, "DIE: " + error);
}
}
}

View File

@@ -0,0 +1,91 @@
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.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);
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

@@ -29,32 +29,26 @@ public class FloatingHandler {
static public int _width;
static public int _height;
static public int _alpha;
static public String _htmlPage = "floating.htm";
public static void show(Context context, int port, int width, int height, int transparency) {
_context = context;
_port = port;
_width = width;
_height = height;
_alpha = transparency;
public static void show(Context context, int port, int width, int height, int transparency, String htmlPage) {
_context = context;
_port = port;
_width = width;
_height = height;
_alpha = transparency;
_htmlPage = htmlPage;
// First it confirms whether the
// 'Display over other apps' permission in given
if (checkOverlayDisplayPermission()) {
if(_intent == null)
_intent = new Intent(context, FloatingWindowGFG.class);
// FloatingWindowGFG service is started
context.startService(_intent);
// The MainActivity closes here
//finish();
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
// This method will start the intent. It takes two parameter, one is the Intent and the other is
// an requestCode Integer. Here it is -1.
Activity a = (Activity)_context;
a.startActivityForResult(intent, -1);
}
}
if (checkOverlayDisplayPermission()) {
if (_intent == null)
_intent = new Intent(context, FloatingWindowGFG.class);
context.startService(_intent);
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
Activity a = (Activity) _context;
a.startActivityForResult(intent, -1);
}
}
public static void hide() {
if(_intent != null)

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/floating.htm");
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

@@ -7,9 +7,13 @@ import android.content.IntentFilter;
import android.media.AudioManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
public class MediaButtonReceiver extends BroadcastReceiver {
private static MediaButtonReceiver instance;
private static final int TARGET_VOLUME = 7; // Middle volume value for infinite gear changes
private static boolean restoringVolume = false; // Flag to prevent recursion
@Override
public void onReceive(Context context, Intent intent) {
@@ -21,8 +25,30 @@ public class MediaButtonReceiver extends BroadcastReceiver {
int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1);
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Previous: " + previousVolume + ", Max: " + maxVolume + ", Restoring: " + restoringVolume);
// If we're restoring volume, skip processing and reset flag
if (restoringVolume) {
QLog.d("MediaButtonReceiver", "Volume restore completed");
restoringVolume = false;
return;
}
// Process the gear change
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
// Auto-restore volume to middle value after a short delay to enable infinite gear changes
if (currentVolume != TARGET_VOLUME) {
final AudioManager am = audioManager;
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
QLog.d("MediaButtonReceiver", "Auto-restoring volume to: " + TARGET_VOLUME);
restoringVolume = true;
am.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
}
}, 100); // 100ms delay to ensure gear change is processed first
}
}
}
@@ -54,7 +80,25 @@ public class MediaButtonReceiver extends BroadcastReceiver {
}
}
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
// Initialize volume to target value for gear control
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager != null) {
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (currentVolume != TARGET_VOLUME) {
QLog.d("MediaButtonReceiver", "Initializing volume to: " + TARGET_VOLUME);
restoringVolume = true;
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
// Reset flag after initialization
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
restoringVolume = false;
}
}, 200);
}
}
} catch (IllegalArgumentException e) {
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
} catch (Exception e) {

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

@@ -43,6 +43,8 @@ public class QZAdbRemote implements DeviceConnectionListener {
private static final String LOG_TAG = "QZ:AdbRemote";
private static String lastCommand = "";
private static boolean ADBConnected = false;
private static boolean cryptoReady = false;
private static final Object cryptoLock = new Object();
private static String _address = "127.0.0.1";
private static Context _context;
@@ -62,31 +64,46 @@ public class QZAdbRemote implements DeviceConnectionListener {
@Override
public void notifyConnectionEstablished(DeviceConnection devConn) {
QLog.d(LOG_TAG, "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = true;
QLog.i(LOG_TAG, "notifyConnectionEstablished" + lastCommand);
QLog.i(LOG_TAG, "notifyConnectionEstablished - CONNECTED=true, lastCommand=" + lastCommand);
QLog.d(LOG_TAG, "notifyConnectionEstablished - END: ADBConnected=" + ADBConnected);
}
@Override
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
QLog.d(LOG_TAG, "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = false;
QLog.e(LOG_TAG, e.getMessage());
QLog.e(LOG_TAG, "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
if (e != null) {
QLog.e(LOG_TAG, "notifyConnectionFailed - STACK_TRACE: ", e);
}
}
@Override
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
QLog.d(LOG_TAG, "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = false;
QLog.e(LOG_TAG, e.getMessage());
QLog.e(LOG_TAG, "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
if (e != null) {
QLog.e(LOG_TAG, "notifyStreamFailed - STACK_TRACE: ", e);
}
}
@Override
public void notifyStreamClosed(DeviceConnection devConn) {
QLog.d(LOG_TAG, "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = false;
QLog.e(LOG_TAG, "notifyStreamClosed");
QLog.e(LOG_TAG, "notifyStreamClosed - ADBConnected=" + ADBConnected);
}
@Override
public AdbCrypto loadAdbCrypto(DeviceConnection devConn) {
return AdbUtils.readCryptoConfig(_context.getFilesDir());
QLog.d(LOG_TAG, "loadAdbCrypto - START: devConn=" + devConn + ", context=" + _context);
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
QLog.d(LOG_TAG, "loadAdbCrypto - RESULT: crypto=" + (crypto != null ? "valid" : "null"));
return crypto;
}
@Override
@@ -111,92 +128,128 @@ public class QZAdbRemote implements DeviceConnectionListener {
private DeviceConnection startConnection(String host, int port) {
QLog.d(LOG_TAG, "startConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
/* Create the connection object */
DeviceConnection conn = binder.createConnection(host, port);
QLog.d(LOG_TAG, "startConnection - CONNECTION_CREATED: conn=" + conn);
/* Add this activity as a connection listener */
binder.addListener(conn, this);
QLog.d(LOG_TAG, "startConnection - LISTENER_ADDED: this=" + this);
/* Begin the async connection process */
QLog.d(LOG_TAG, "startConnection - STARTING_CONNECT: about to call conn.startConnect()");
conn.startConnect();
QLog.d(LOG_TAG, "startConnection - END: startConnect() called, returning conn=" + conn);
return conn;
}
private DeviceConnection connectOrLookupConnection(String host, int port) {
QLog.d(LOG_TAG, "connectOrLookupConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
DeviceConnection conn = binder.findConnection(host, port);
QLog.d(LOG_TAG, "connectOrLookupConnection - EXISTING_CONN: conn=" + (conn != null ? "found" : "null"));
if (conn == null) {
/* No existing connection, so start the connection process */
QLog.d(LOG_TAG, "connectOrLookupConnection - NEW_CONNECTION: starting new connection");
conn = startConnection(host, port);
}
else {
/* Add ourselves as a new listener of this connection */
QLog.d(LOG_TAG, "connectOrLookupConnection - REUSE_CONNECTION: adding listener to existing connection");
binder.addListener(conn, this);
}
QLog.d(LOG_TAG, "connectOrLookupConnection - END: returning conn=" + conn);
return conn;
}
public ServiceConnection serviceConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName arg0, IBinder arg1) {
QLog.d(LOG_TAG, "onServiceConnected - START: componentName=" + arg0 + ", binder=" + arg1 + ", _address=" + _address);
binder = (ShellService.ShellServiceBinder)arg1;
QLog.d(LOG_TAG, "onServiceConnected - BINDER_SET: binder=" + binder + ", existing_connection=" + connection);
if (connection != null) {
QLog.d(LOG_TAG, "onServiceConnected - REMOVING_OLD_LISTENER: connection=" + connection);
binder.removeListener(connection, QZAdbRemote.getInstance());
}
QLog.d(LOG_TAG, "onServiceConnected - CONNECTING: about to call connectOrLookupConnection");
connection = connectOrLookupConnection(_address, 5555);
QLog.d(LOG_TAG, "onServiceConnected - END: connection=" + connection);
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
QLog.d(LOG_TAG, "onServiceDisconnected - START: componentName=" + arg0 + ", old_binder=" + binder);
binder = null;
QLog.d(LOG_TAG, "onServiceDisconnected - END: binder set to null");
}
};
static public void createConnection(String ip, Context context) {
QLog.d(LOG_TAG, "createConnection - START: ip=" + ip + ", context=" + context + ", existing_binder=" + binder);
_address = ip;
_context = context;
QLog.d(LOG_TAG, "createConnection - PARAMS_SET: _address=" + _address + ", _context=" + _context);
/* If we have old RSA keys, just use them */
QLog.d(LOG_TAG, "createConnection - CHECKING_CRYPTO: reading existing crypto config");
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
QLog.d(LOG_TAG, "createConnection - CRYPTO_CHECK: crypto=" + (crypto != null ? "exists" : "null"));
if (crypto == null)
{
/* We need to make a new pair */
QLog.i(LOG_TAG,
"This will only be done once.");
new Thread(new Runnable() {
@Override
public void run() {
AdbCrypto crypto;
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
if (crypto == null)
{
QLog.e(LOG_TAG,
"Unable to generate and save RSA key pair");
return;
}
}
}).start();
QLog.d(LOG_TAG, "createConnection - GENERATING_CRYPTO: synchronously generating crypto keys");
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
if (crypto == null) {
QLog.e(LOG_TAG, "Unable to generate and save RSA key pair");
cryptoReady = false;
return;
}
QLog.d(LOG_TAG, "createConnection - CRYPTO_GENERATED: crypto keys generated successfully");
synchronized (cryptoLock) {
cryptoReady = true;
}
} else {
QLog.d(LOG_TAG, "createConnection - CRYPTO_EXISTS: marking crypto as ready");
synchronized (cryptoLock) {
cryptoReady = true;
}
}
QLog.d(LOG_TAG, "createConnection - SERVICE_CHECK: binder=" + (binder != null ? "exists" : "null"));
if (binder == null) {
QLog.i(LOG_TAG, "createConnection - STARTING_SERVICE: Starting ShellService.class");
service = new Intent(_context, ShellService.class);
service.putExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
QLog.d(LOG_TAG, "createConnection - SERVICE_INTENT: service=" + service);
/* Bind the service if we're not bound already. After binding, the callback will
* perform the initial connection. */
QLog.d(LOG_TAG, "createConnection - BINDING_SERVICE: about to bind service");
_context.bindService(service, QZAdbRemote.getInstance().serviceConn, Service.BIND_AUTO_CREATE);
QLog.d(LOG_TAG, "createConnection - SERVICE_BOUND: bindService called");
QLog.d(LOG_TAG, "createConnection - STARTING_SERVICE: SDK_INT=" + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
QLog.d(LOG_TAG, "createConnection - FOREGROUND_SERVICE: starting foreground service");
_context.startForegroundService(service);
}
else {
QLog.d(LOG_TAG, "createConnection - REGULAR_SERVICE: starting regular service");
_context.startService(service);
}
QLog.d(LOG_TAG, "createConnection - SERVICE_STARTED: service start completed");
} else {
QLog.d(LOG_TAG, "createConnection - SKIP_SERVICE: binder already exists, skipping service creation");
}
QLog.d(LOG_TAG, "createConnection - END: method completed");
}
static public void sendCommand(String command) {

View File

@@ -9,6 +9,7 @@ import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.Socket;
import java.util.HashMap;
import org.cagnulen.qdomyoszwift.QLog;
/**
* This class represents an ADB connection.
@@ -124,10 +125,13 @@ public class AdbConnection implements Closeable {
try {
/* Read and parse a message off the socket's input stream */
AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream);
QLog.d("AdbConnection", "connectionThread - Received packet: command=0x" + Integer.toHexString(msg.command) + ", arg0=" + msg.arg0 + ", arg1=" + msg.arg1);
/* Verify magic and checksum */
if (!AdbProtocol.validateMessage(msg))
if (!AdbProtocol.validateMessage(msg)) {
QLog.w("AdbConnection", "connectionThread - Invalid message, dropping packet");
continue;
}
switch (msg.command)
{
@@ -175,21 +179,25 @@ public class AdbConnection implements Closeable {
break;
case AdbProtocol.CMD_AUTH:
QLog.d("AdbConnection", "connectionThread - Received AUTH packet, type=" + msg.arg0);
byte[] packet;
if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN)
{
/* This is an authentication challenge */
QLog.d("AdbConnection", "connectionThread - AUTH_TYPE_TOKEN challenge, sentSignature=" + conn.sentSignature);
if (conn.sentSignature)
{
/* We've already tried our signature, so send our public key */
QLog.d("AdbConnection", "connectionThread - Sending RSA public key");
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC,
conn.crypto.getAdbPublicKeyPayload());
}
else
{
/* We'll sign the token */
QLog.d("AdbConnection", "connectionThread - Signing token with private key");
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE,
conn.crypto.signAdbTokenPayload(msg.payload));
conn.sentSignature = true;
@@ -198,16 +206,22 @@ public class AdbConnection implements Closeable {
/* Write the AUTH reply */
conn.outputStream.write(packet);
conn.outputStream.flush();
QLog.d("AdbConnection", "connectionThread - AUTH response sent");
}
else {
QLog.w("AdbConnection", "connectionThread - Unhandled AUTH type: " + msg.arg0);
}
break;
case AdbProtocol.CMD_CNXN:
QLog.d("AdbConnection", "connectionThread - Received CNXN packet! maxData=" + msg.arg1);
synchronized (conn) {
/* We need to store the max data size */
conn.maxData = msg.arg1;
/* Mark us as connected and unwait anyone waiting on the connection */
conn.connected = true;
QLog.d("AdbConnection", "connectionThread - Connection established! Notifying waiting threads");
conn.notifyAll();
}
break;
@@ -219,6 +233,7 @@ public class AdbConnection implements Closeable {
} catch (Exception e) {
/* The cleanup is taken care of by a combination of this thread
* and close() */
QLog.e("AdbConnection", "connectionThread - Exception in connection thread: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
break;
}
}
@@ -270,23 +285,32 @@ public class AdbConnection implements Closeable {
if (connected)
throw new IllegalStateException("Already connected");
QLog.d("AdbConnection", "connect() - Starting ADB connection");
/* Write the CONNECT packet */
outputStream.write(AdbProtocol.generateConnect());
outputStream.flush();
QLog.d("AdbConnection", "connect() - CONNECT packet sent, starting connection thread");
/* Start the connection thread to respond to the peer */
connectAttempted = true;
connectionThread.start();
QLog.d("AdbConnection", "connect() - Connection thread started, waiting for connection...");
/* Wait for the connection to go live */
synchronized (this) {
if (!connected)
if (!connected) {
QLog.d("AdbConnection", "connect() - Waiting for connection to complete...");
wait();
QLog.d("AdbConnection", "connect() - Wait completed, connected=" + connected);
}
if (!connected) {
QLog.e("AdbConnection", "connect() - Connection failed after wait");
throw new IOException("Connection failed");
}
}
QLog.d("AdbConnection", "connect() - Successfully connected!");
}
/**

View File

@@ -11,6 +11,7 @@ import com.cgutman.adblib.AdbConnection;
import com.cgutman.adblib.AdbCrypto;
import com.cgutman.adblib.AdbStream;
import com.cgutman.androidremotedebugger.AdbUtils;
import org.cagnulen.qdomyoszwift.QLog;
public class DeviceConnection implements Closeable {
private static final int CONN_TIMEOUT = 5000;
@@ -59,42 +60,58 @@ public class DeviceConnection implements Closeable {
}
public void startConnect() {
QLog.d("DeviceConnection", "startConnect - START: host=" + host + ", port=" + port + ", listener=" + listener);
new Thread(new Runnable() {
@Override
public void run() {
QLog.d("DeviceConnection", "startConnect.run - THREAD_START: host=" + host + ", port=" + port);
boolean connected = false;
Socket socket = new Socket();
AdbCrypto crypto;
/* Load the crypto config */
QLog.d("DeviceConnection", "startConnect.run - LOADING_CRYPTO: calling loadAdbCrypto");
crypto = listener.loadAdbCrypto(DeviceConnection.this);
if (crypto == null) {
QLog.e("DeviceConnection", "startConnect.run - CRYPTO_FAILED: crypto is null, returning");
return;
}
QLog.d("DeviceConnection", "startConnect.run - CRYPTO_LOADED: crypto=" + crypto);
try {
/* Establish a connect to the remote host */
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECT: connecting to " + host + ":" + port + " with timeout=" + CONN_TIMEOUT);
socket.connect(new InetSocketAddress(host, port), CONN_TIMEOUT);
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECTED: socket connected successfully");
} catch (IOException e) {
QLog.e("DeviceConnection", "startConnect.run - SOCKET_FAILED: connection failed", e);
listener.notifyConnectionFailed(DeviceConnection.this, e);
return;
}
try {
/* Establish the application layer connection */
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTION: creating AdbConnection");
connection = AdbConnection.create(socket, crypto);
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECT: calling connection.connect()");
connection.connect();
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTED: ADB connection established");
/* Open the shell stream */
QLog.d("DeviceConnection", "startConnect.run - SHELL_STREAM: opening shell stream");
shellStream = connection.open("shell:");
QLog.d("DeviceConnection", "startConnect.run - SHELL_OPENED: shell stream opened successfully");
connected = true;
} catch (IOException e) {
QLog.e("DeviceConnection", "startConnect.run - ADB_IO_ERROR: IOException during ADB connection", e);
listener.notifyConnectionFailed(DeviceConnection.this, e);
} catch (InterruptedException e) {
QLog.e("DeviceConnection", "startConnect.run - ADB_INTERRUPTED: InterruptedException during ADB connection", e);
listener.notifyConnectionFailed(DeviceConnection.this, e);
} finally {
/* Cleanup if the connection failed */
if (!connected) {
QLog.d("DeviceConnection", "startConnect.run - CLEANUP: connection failed, cleaning up");
AdbUtils.safeClose(shellStream);
/* The AdbConnection object will close the underlying socket
@@ -112,12 +129,16 @@ public class DeviceConnection implements Closeable {
}
/* Notify the listener that the connection is complete */
QLog.d("DeviceConnection", "startConnect.run - NOTIFY_SUCCESS: calling listener.notifyConnectionEstablished");
listener.notifyConnectionEstablished(DeviceConnection.this);
QLog.d("DeviceConnection", "startConnect.run - NOTIFIED: notifyConnectionEstablished called");
/* Start the receive thread */
QLog.d("DeviceConnection", "startConnect.run - START_RECEIVE: starting receive thread");
startReceiveThread();
/* Enter the blocking send loop */
QLog.d("DeviceConnection", "startConnect.run - SEND_LOOP: entering send loop");
sendLoop();
}
}).start();
@@ -148,23 +169,32 @@ public class DeviceConnection implements Closeable {
}
private void startReceiveThread() {
QLog.d("DeviceConnection", "startReceiveThread - START: creating receive thread");
new Thread(new Runnable() {
@Override
public void run() {
QLog.d("DeviceConnection", "startReceiveThread.run - THREAD_START: receive thread started");
try {
while (!shellStream.isClosed()) {
QLog.d("DeviceConnection", "startReceiveThread.run - READING: waiting for data from shellStream");
byte[] data = shellStream.read();
QLog.d("DeviceConnection", "startReceiveThread.run - DATA_RECEIVED: " + data.length + " bytes received");
listener.receivedData(DeviceConnection.this, data, 0, data.length);
}
QLog.d("DeviceConnection", "startReceiveThread.run - STREAM_CLOSED: shellStream is closed");
listener.notifyStreamClosed(DeviceConnection.this);
} catch (IOException e) {
QLog.e("DeviceConnection", "startReceiveThread.run - IO_ERROR: IOException in receive thread", e);
listener.notifyStreamFailed(DeviceConnection.this, e);
} catch (InterruptedException e) {
QLog.d("DeviceConnection", "startReceiveThread.run - INTERRUPTED: receive thread interrupted");
} finally {
QLog.d("DeviceConnection", "startReceiveThread.run - CLEANUP: cleaning up receive thread");
AdbUtils.safeClose(DeviceConnection.this);
}
}
}).start();
QLog.d("DeviceConnection", "startReceiveThread - END: receive thread started");
}
public boolean isClosed() {

View File

@@ -48,14 +48,20 @@ public class ShellService extends Service implements DeviceConnectionListener {
public class ShellServiceBinder extends Binder {
public DeviceConnection createConnection(String host, int port) {
QLog.d("ShellService", "createConnection - START: host=" + host + ", port=" + port + ", listener=" + listener);
DeviceConnection conn = new DeviceConnection(listener, host, port);
QLog.d("ShellService", "createConnection - CONNECTION_CREATED: conn=" + conn);
listener.addListener(conn, ShellService.this);
QLog.d("ShellService", "createConnection - LISTENER_ADDED: returning conn=" + conn);
return conn;
}
public DeviceConnection findConnection(String host, int port) {
String connStr = host+":"+port;
return currentConnectionMap.get(connStr);
QLog.d("ShellService", "findConnection - SEARCH: connStr=" + connStr + ", mapSize=" + currentConnectionMap.size());
DeviceConnection found = currentConnectionMap.get(connStr);
QLog.d("ShellService", "findConnection - RESULT: found=" + (found != null ? "exists" : "null"));
return found;
}
public void notifyPausingActivity(DeviceConnection devConn) {
@@ -76,68 +82,95 @@ public class ShellService extends Service implements DeviceConnectionListener {
}
public void addListener(DeviceConnection conn, DeviceConnectionListener listener) {
QLog.d("ShellService", "addListener - START: conn=" + conn + ", listener=" + listener);
ShellService.this.listener.addListener(conn, listener);
QLog.d("ShellService", "addListener - END: listener added");
}
public void removeListener(DeviceConnection conn, DeviceConnectionListener listener) {
QLog.d("ShellService", "removeListener - START: conn=" + conn + ", listener=" + listener);
ShellService.this.listener.removeListener(conn, listener);
QLog.d("ShellService", "removeListener - END: listener removed");
}
}
@Override
public IBinder onBind(Intent arg0) {
QLog.d("ShellService", "onBind - START: intent=" + arg0 + ", binder=" + binder);
QLog.d("ShellService", "onBind - END: returning binder");
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
QLog.d("ShellService", "onUnbind - START: intent=" + intent + ", connections=" + currentConnectionMap.size());
/* Stop the service if no connections remain */
if (currentConnectionMap.isEmpty()) {
QLog.d("ShellService", "onUnbind - STOPPING_SERVICE: no connections remain");
stopSelf();
} else {
QLog.d("ShellService", "onUnbind - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
}
QLog.d("ShellService", "onUnbind - END: returning false");
return false;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
QLog.d("ShellService", "onStartCommand - START: intent=" + intent + ", flags=" + flags + ", startId=" + startId + ", foregroundId=" + foregroundId);
if (foregroundId == 0) {
try {
int serviceType = intent.getIntExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
// If we're not already running in the foreground, use a placeholder
// notification until a real connection is established. After connection
// establishment, the real notification will replace this one.
QLog.d("ShellService", "onStartCommand - FOREGROUND_START: serviceType=" + serviceType + ", SDK_INT=" + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
QLog.d("ShellService", "onStartCommand - FOREGROUND_Q+: starting with service type");
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification(), serviceType);
} else {
QLog.d("ShellService", "onStartCommand - FOREGROUND_LEGACY: starting without service type");
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification());
}
QLog.d("ShellService", "onStartCommand - FOREGROUND_SUCCESS: foreground service started");
} catch (Exception e) {
QLog.e("ForegroundService", "Failed to start foreground service", e);
return START_NOT_STICKY;
}
} else {
QLog.d("ShellService", "onStartCommand - SKIP_FOREGROUND: already running in foreground with id=" + foregroundId);
}
// Don't restart if we've been killed. We will have already lost our connections
// when we died, so we'll just be running doing nothing if the OS restarted us.
QLog.d("ShellService", "onStartCommand - END: returning START_NOT_STICKY");
return Service.START_NOT_STICKY;
}
@Override
public void onCreate() {
QLog.d("ShellService", "onCreate - START: initializing service");
super.onCreate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: creating notification channel");
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Connection Info", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: channel created");
}
QLog.d("ShellService", "onCreate - WIFI_LOCK: creating wifi lock");
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wlanLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL, "RemoteADBShell:ShellService");
QLog.d("ShellService", "onCreate - WIFI_LOCK: wlanLock=" + wlanLock);
QLog.d("ShellService", "onCreate - WAKE_LOCK: creating wake lock");
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "RemoteADBShell:ShellService");
QLog.d("ShellService", "onCreate - WAKE_LOCK: wakeLock=" + wakeLock);
QLog.d("ShellService", "onCreate - END: service initialization complete");
}
@Override
@@ -248,44 +281,76 @@ public class ShellService extends Service implements DeviceConnectionListener {
}
private synchronized void addNewConnection(DeviceConnection devConn) {
QLog.d("ShellService", "addNewConnection - START: devConn=" + devConn + ", currentSize=" + currentConnectionMap.size());
if (currentConnectionMap.isEmpty()) {
QLog.d("ShellService", "addNewConnection - ACQUIRING_LOCKS: first connection, acquiring locks");
wakeLock.acquire();
wlanLock.acquire();
QLog.d("ShellService", "addNewConnection - LOCKS_ACQUIRED: wakeLock and wlanLock acquired");
}
currentConnectionMap.put(getConnectionString(devConn), devConn);
String connString = getConnectionString(devConn);
QLog.d("ShellService", "addNewConnection - ADDING: connString=" + connString);
currentConnectionMap.put(connString, devConn);
QLog.d("ShellService", "addNewConnection - END: connection added, newSize=" + currentConnectionMap.size());
}
private synchronized void removeConnection(DeviceConnection devConn) {
currentConnectionMap.remove(getConnectionString(devConn));
String connString = getConnectionString(devConn);
QLog.d("ShellService", "removeConnection - START: devConn=" + devConn + ", connString=" + connString + ", currentSize=" + currentConnectionMap.size());
currentConnectionMap.remove(connString);
QLog.d("ShellService", "removeConnection - REMOVED: newSize=" + currentConnectionMap.size());
/* Stop the service if no connections remain */
if (currentConnectionMap.isEmpty()) {
QLog.d("ShellService", "removeConnection - STOPPING_SERVICE: no connections remain");
stopSelf();
} else {
QLog.d("ShellService", "removeConnection - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
}
QLog.d("ShellService", "removeConnection - END");
}
@Override
public void notifyConnectionEstablished(DeviceConnection devConn) {
QLog.d("ShellService", "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
addNewConnection(devConn);
QLog.d("ShellService", "notifyConnectionEstablished - CONNECTION_ADDED: updating notification");
updateNotification(devConn, true);
QLog.d("ShellService", "notifyConnectionEstablished - END: connection established successfully");
}
@Override
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
QLog.d("ShellService", "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
QLog.e("ShellService", "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
if (e != null) {
QLog.e("ShellService", "notifyConnectionFailed - STACK_TRACE: ", e);
}
/* No notification is displaying here */
QLog.d("ShellService", "notifyConnectionFailed - END");
}
@Override
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
QLog.d("ShellService", "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
QLog.e("ShellService", "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
if (e != null) {
QLog.e("ShellService", "notifyStreamFailed - STACK_TRACE: ", e);
}
updateNotification(devConn, false);
QLog.d("ShellService", "notifyStreamFailed - NOTIFICATION_UPDATED: removing connection");
removeConnection(devConn);
QLog.d("ShellService", "notifyStreamFailed - END");
}
@Override
public void notifyStreamClosed(DeviceConnection devConn) {
QLog.d("ShellService", "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
updateNotification(devConn, false);
QLog.d("ShellService", "notifyStreamClosed - NOTIFICATION_UPDATED: removing connection");
removeConnection(devConn);
QLog.d("ShellService", "notifyStreamClosed - END");
}
@Override

View File

@@ -65,13 +65,16 @@ import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.PendingPurchasesParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchaseState;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchasesParams;
import com.android.billingclient.api.QueryProductDetailsResult;
/***********************************************************************
@@ -79,7 +82,7 @@ import com.android.billingclient.api.SkuDetailsResponseListener;
** Add Dependencies below to build.gradle file:
dependencies {
def billing_version = "4.0.0"
def billing_version = "8.0.0"
implementation "com.android.billingclient:billing:$billing_version"
}
@@ -97,8 +100,8 @@ public class InAppPurchase implements PurchasesUpdatedListener
public static final int RESULT_OK = BillingClient.BillingResponseCode.OK;
public static final int RESULT_USER_CANCELED = BillingClient.BillingResponseCode.USER_CANCELED;
public static final String TYPE_INAPP = BillingClient.SkuType.INAPP;
public static final String TYPE_SUBS = BillingClient.SkuType.SUBS;
public static final String TYPE_INAPP = BillingClient.ProductType.INAPP;
public static final String TYPE_SUBS = BillingClient.ProductType.SUBS;
public static final String TAG = "InAppPurchase";
// Should be in sync with InAppTransaction::FailureReason
@@ -120,8 +123,11 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void initializeConnection(){
QLog.w(TAG, "initializeConnection start");
PendingPurchasesParams pendingPurchasesParams = PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build();
billingClient = BillingClient.newBuilder(m_context)
.enablePendingPurchases()
.enablePendingPurchases(pendingPurchasesParams)
.setListener(this)
.build();
billingClient.startConnection(new BillingClientStateListener() {
@@ -146,18 +152,23 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
int responseCode = billingResult.getResponseCode();
QLog.d(TAG, "onPurchasesUpdated called. Response code: " + responseCode + ", Debug message: " + billingResult.getDebugMessage());
if (purchases == null) {
QLog.e(TAG, "Purchase failed: Data missing from result (purchases is null)");
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
return;
}
if (billingResult.getResponseCode() == RESULT_OK) {
QLog.d(TAG, "Purchase successful, handling " + purchases.size() + " purchases");
handlePurchase(purchases);
} else if (responseCode == RESULT_USER_CANCELED) {
QLog.d(TAG, "Purchase cancelled by user");
purchaseFailed(purchaseRequestCode, FAILUREREASON_USERCANCELED, "");
} else {
String errorString = getErrorString(responseCode);
QLog.e(TAG, "Purchase failed with error: " + errorString + " (code: " + responseCode + ")");
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, errorString);
}
}
@@ -209,31 +220,44 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
index += productIdList.size();
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(productIdList).setType(TYPE_SUBS);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
for (String productId : productIdList) {
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(TYPE_SUBS)
.build());
}
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(params,
(billingResult, productDetailsResult) -> {
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
int responseCode = billingResult.getResponseCode();
QLog.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
QLog.d(TAG, "onProductDetailsResponse: responseCode " + responseCode);
if (responseCode != RESULT_OK) {
QLog.e(TAG, "queryDetails: Couldn't retrieve sku details.");
QLog.e(TAG, "queryDetails: Couldn't retrieve product details.");
return;
}
if (skuDetailsList == null) {
if (productDetailsList == null || productDetailsList.isEmpty()) {
QLog.e(TAG, "queryDetails: No details list in response.");
return;
}
QLog.d(TAG, "onSkuDetailsResponse: skuDetailsList " + skuDetailsList);
for (SkuDetails skuDetails : skuDetailsList) {
QLog.d(TAG, "onProductDetailsResponse: productDetailsList " + productDetailsList);
for (ProductDetails productDetails : productDetailsList) {
try {
String queriedProductId = skuDetails.getSku();
String queriedPrice = skuDetails.getPrice();
String queriedTitle = skuDetails.getTitle();
String queriedDescription = skuDetails.getDescription();
String queriedProductId = productDetails.getProductId();
String queriedPrice = "";
String queriedTitle = productDetails.getTitle();
String queriedDescription = productDetails.getDescription();
// Get price from subscription offer details
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
queriedPrice = productDetails.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
}
registerProduct(m_nativePointer,
queriedProductId,
queriedPrice,
@@ -243,7 +267,6 @@ public class InAppPurchase implements PurchasesUpdatedListener
e.printStackTrace();
}
}
}
});
@@ -255,33 +278,51 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void launchBillingFlow(String identifier, final int requestCode){
purchaseRequestCode = requestCode;
List<String> skuList = new ArrayList<>();
skuList.add(identifier);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(TYPE_SUBS);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(identifier)
.setProductType(TYPE_SUBS)
.build());
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(params,
(billingResult, productDetailsResult) -> {
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
if (billingResult.getResponseCode() != RESULT_OK) {
QLog.e(TAG, "Unable to launch Google Play purchase screen");
QLog.e(TAG, "Unable to launch Google Play purchase screen. Response code: " + billingResult.getResponseCode() + ", Debug message: " + billingResult.getDebugMessage());
String errorString = getErrorString(requestCode);
purchaseFailed(requestCode, FAILUREREASON_ERROR, errorString);
return;
}
else if (skuDetailsList == null){
else if (productDetailsList == null || productDetailsList.isEmpty()){
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
return;
}
ProductDetails productDetails = productDetailsList.get(0);
BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails);
// For subscriptions, we need to set the offer token
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();
QLog.d(TAG, "Setting offer token for subscription: " + offerToken);
productDetailsParamsBuilder.setOfferToken(offerToken);
} else {
QLog.w(TAG, "No subscription offer details found for product: " + identifier);
}
BillingFlowParams.ProductDetailsParams productDetailsParams = productDetailsParamsBuilder.build();
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetailsList.get(0))
.setProductDetailsParamsList(java.util.Arrays.asList(productDetailsParams))
.build();
//Results will be delivered to onPurchasesUpdated
billingClient.launchBillingFlow((Activity) m_context, purchaseParams);
}
});
}
@@ -321,18 +362,21 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void queryPurchasedProducts(final List<String> productIdList) {
billingClient.queryPurchasesAsync(TYPE_INAPP, new PurchasesResponseListener() {
QueryPurchasesParams queryPurchasesParams = QueryPurchasesParams.newBuilder()
.setProductType(TYPE_SUBS)
.build();
billingClient.queryPurchasesAsync(queryPurchasesParams, new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> list) {
for (Purchase purchase : list) {
if (productIdList.contains(purchase.getSkus().get(0))) {
if (productIdList.contains(purchase.getProducts().get(0))) {
registerPurchased(m_nativePointer,
purchase.getSkus().get(0),
purchase.getProducts().get(0),
purchase.getSignature(),
purchase.getOriginalJson(),
purchase.getPurchaseToken(),
purchase.getDeveloperPayload(),
"", // getDeveloperPayload() is deprecated
purchase.getPurchaseTime());
}
}

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

@@ -0,0 +1,6 @@
#ifndef BLUETOOTHDEVICETYPE_H
#define BLUETOOTHDEVICETYPE_H
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
#endif // BLUETOOTHDEVICETYPE_H

View File

@@ -5,7 +5,7 @@ CharacteristicNotifier2A53::CharacteristicNotifier2A53(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2a53, parent), Bike(Bike) {}
int CharacteristicNotifier2A53::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
value.append(0x02); // total distance
uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256;
uint32_t distance = Bike->odometer() * 10000.0;

View File

@@ -8,7 +8,7 @@ int CharacteristicNotifier2A63::notify(QByteArray &value) {
if (normalizeWattage < 0)
normalizeWattage = 0;
if (Bike->deviceType() == bluetoothdevice::BIKE) {
if (Bike->deviceType() == BIKE) {
/*
// set measurement
measurement[2] = power & 0xFF;

View File

@@ -1,15 +1,17 @@
#include "characteristicnotifier2acd.h"
#include "devices/treadmill.h"
#include <qmath.h>
#include <QTime> // Include QTime for Bike->elapsedTime()
CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QObject *parent)
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
int CharacteristicNotifier2ACD::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == TREADMILL || dt == ELLIPTICAL) {
value.append(0x0C); // Inclination available and distance for peloton
value.append((char)0x01); // heart rate available
//value.append((char)0x01); // heart rate available
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
char a = (normalizeSpeed >> 8) & 0XFF;
@@ -44,7 +46,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclination /= gain;
}
if (dt == bluetoothdevice::TREADMILL)
if (dt == TREADMILL)
normalizeIncline = (uint32_t)qRound(inclination * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
@@ -52,7 +54,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclineBytes.append(b);
inclineBytes.append(a);
double ramp = 0;
if (dt == bluetoothdevice::TREADMILL)
if (dt == TREADMILL)
ramp = qRadiansToDegrees(qAtan(inclination / 100));
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
@@ -61,6 +63,18 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
rampBytes.append(b);
rampBytes.append(a);
// Get session elapsed time - makes Runna calculations work
QTime sessionElapsedTime = Bike->elapsedTime();
double elapsed_time_seconds =
(double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
QByteArray elapsedBytes;
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
value.append(speedBytes); // Actual value.
value.append(distanceBytes); // Actual value.
@@ -70,6 +84,9 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
value.append(rampBytes); // ramp angle
value.append(Bike->currentHeart().value()); // current heart rate
value.append(elapsedBytes); // Elapsed Time
return CN_OK;
} else
return CN_INVALID;

View File

@@ -8,12 +8,12 @@ CharacteristicNotifier2AD2::CharacteristicNotifier2AD2(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2ad2, parent), Bike(Bike) {}
int CharacteristicNotifier2AD2::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
QSettings settings;
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
bool rowerAsABike = !virtual_device_rower && dt == bluetoothdevice::ROWING;
bool rowerAsABike = !virtual_device_rower && dt == ROWING;
bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool();
double cadence_multiplier = 2.0;
if (double_cadence)
@@ -24,7 +24,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
if (normalizeWattage < 0)
normalizeWattage = 0;
if (dt == bluetoothdevice::BIKE || rowerAsABike) {
if (dt == BIKE || rowerAsABike) {
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
@@ -44,7 +44,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
value.append(char(Bike->currentHeart().value())); // Actual value.
value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix
return CN_OK;
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL || dt == bluetoothdevice::ROWING) {
} else if (dt == TREADMILL || dt == ELLIPTICAL || dt == ROWING) {
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
@@ -53,11 +53,11 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
uint16_t cadence = 0;
if (dt == bluetoothdevice::ELLIPTICAL)
if (dt == ELLIPTICAL)
cadence = ((elliptical *)Bike)->currentCadence().value();
else if (dt == bluetoothdevice::TREADMILL)
else if (dt == TREADMILL)
cadence = ((treadmill *)Bike)->currentCadence().value();
else if (dt == bluetoothdevice::ROWING)
else if (dt == ROWING)
cadence = ((rower *)Bike)->currentCadence().value();
value.append((char)((uint16_t)(cadence * cadence_multiplier) & 0xFF)); // cadence

View File

@@ -10,7 +10,7 @@ CharacteristicWriteProcessor::CharacteristicWriteProcessor(double bikeResistance
void CharacteristicWriteProcessor::changePower(uint16_t power) { Bike->changePower(power); }
void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr, uint8_t cw) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
@@ -64,7 +64,7 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
qDebug() << "changeSlope CRR = " << fCRR << CRR_offset << "CW = " << fCW;
if (dt == bluetoothdevice::BIKE) {
if (dt == BIKE) {
// if the bike doesn't have the inclination by hardware, i'm simulating inclination with the value received
// from Zwift
@@ -82,9 +82,9 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
Bike->changeResistance((resistance_t)(round(resistance * bikeResistanceGain)) + bikeResistanceOffset + 1 +
CRR_offset + CW_offset); // resistance start from 1
}
} else if (dt == bluetoothdevice::TREADMILL) {
} else if (dt == TREADMILL) {
emit changeInclination(grade, percentage);
} else if (dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == ELLIPTICAL) {
bool inclinationAvailableByHardware = ((elliptical *)Bike)->inclinationAvailableByHardware();
qDebug() << "inclinationAvailableByHardware" << inclinationAvailableByHardware << "erg_mode" << erg_mode;
emit changeInclination(grade, percentage);

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

@@ -13,8 +13,8 @@ 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) {
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == BIKE || dt == ROWING) {
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
@@ -63,6 +63,12 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_START_RESUME);
reply.append((quint8)FTMS_SUCCESS);
} else if (cmd == FTMS_STOP_PAUSE) {
qDebug() << QStringLiteral("stop/pause simulation! ignoring it");
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_STOP_PAUSE);
reply.append((quint8)FTMS_SUCCESS);
} else if (cmd == FTMS_REQUEST_CONTROL) {
qDebug() << QStringLiteral("control requested");
@@ -76,7 +82,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
reply.append((quint8)cmd);
reply.append((quint8)FTMS_NOT_SUPPORTED);
}
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
char a, b;
if ((char)data.at(0) == 0x02) {
// Set Target Speed
@@ -85,7 +91,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
uint16_t uspeed = a + (((uint16_t)b) << 8);
double requestSpeed = (double)uspeed / 100.0;
if (dt == bluetoothdevice::TREADMILL) {
if (dt == TREADMILL) {
((treadmill *)Bike)->changeSpeed(requestSpeed);
}
qDebug() << QStringLiteral("new requested speed ") + QString::number(requestSpeed);
@@ -97,10 +103,10 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
int16_t sincline = a + (((int16_t)b) << 8);
double requestIncline = (double)sincline / 10.0;
if (dt == bluetoothdevice::TREADMILL)
if (dt == TREADMILL)
((treadmill *)Bike)->changeInclination(requestIncline, requestIncline);
// Resistance as incline on Sole E95s Elliptical #419
else if (dt == bluetoothdevice::ELLIPTICAL) {
else if (dt == ELLIPTICAL) {
if(((elliptical *)Bike)->inclinationAvailableByHardware())
((elliptical *)Bike)->changeInclination(requestIncline, requestIncline);
else

View File

@@ -13,8 +13,8 @@ CharacteristicWriteProcessorE005::CharacteristicWriteProcessorE005(double bikeRe
int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
if (data.size()) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::BIKE) {
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == BIKE) {
char cmd = data.at(0);
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(), data);
if (cmd == wahookickrsnapbike::_setSimMode && data.count() >= 7) {
@@ -35,7 +35,7 @@ int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArra
qDebug() << "erg mode" << watts;
changePower(watts);
}
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
}
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)data.at(0));

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
activiotreadmill::activiotreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -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

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
antbike::antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -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

@@ -14,7 +14,7 @@ using namespace std::chrono_literals;
apexbike::apexbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -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

@@ -19,7 +19,7 @@ using namespace std::chrono_literals;
bhfitnesselliptical::bhfitnesselliptical(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -70,6 +70,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 +120,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) {
@@ -173,7 +211,7 @@ resistance_t bike::resistanceFromPowerRequest(uint16_t power) { return power / 1
void bike::cadenceSensor(uint8_t cadence) { Cadence.setValue(cadence); }
void bike::powerSensor(uint16_t power) { m_watt.setValue(power, false); }
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
BLUETOOTH_TYPE bike::deviceType() { return BIKE; }
void bike::clearStats() {
@@ -343,6 +381,8 @@ uint8_t bike::metrics_override_heartrate() {
bool bike::inclinationAvailableByHardware() { return false; }
bool bike::inclinationAvailableBySoftware() { return false; }
uint16_t bike::wattFromHR(bool useSpeedAndCadence) {
QSettings settings;
double watt = 0;

View File

@@ -31,7 +31,7 @@ class bike : public bluetoothdevice {
virtual resistance_t resistanceFromPowerRequest(uint16_t power);
virtual uint16_t powerFromResistanceRequest(resistance_t requestResistance);
virtual bool ergManagedBySS2K() { return false; }
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
BLUETOOTH_TYPE deviceType() override;
metric pelotonResistance();
void clearStats() override;
void setLap() override;
@@ -51,7 +51,9 @@ class bike : public bluetoothdevice {
*/
metric currentSteeringAngle() { return m_steeringAngle; }
virtual bool inclinationAvailableByHardware();
virtual bool inclinationAvailableBySoftware();
bool ergModeSupportedAvailableByHardware() { return ergModeSupported; }
virtual bool ergModeSupportedAvailableBySoftware() { return ergModeSupported; }
public Q_SLOTS:
void changeResistance(resistance_t res) override;

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
bkoolbike::bkoolbike(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
@@ -49,7 +49,12 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
if (gattWriteCharCustomId.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
}
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
@@ -61,19 +66,28 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
void bkoolbike::changePower(int32_t power) {
RequestedPower = power;
/*
if (power < 0)
power = 0;
uint8_t p[] = {0xa4, 0x09, 0x4e, 0x05, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x02, 0x00};
p[10] = (uint8_t)((power * 4) & 0xFF);
p[11] = (uint8_t)((power * 4) >> 8);
for (uint8_t i = 0; i < sizeof(p) - 1; i++) {
p[12] ^= p[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(p, sizeof(p), QStringLiteral("changePower"), false, false);*/
if (power < 0) {
power = 0;
}
qDebug() << QStringLiteral("Changepower not implemented");
forcePower(power);
}
void bkoolbike::forcePower(int32_t power) {
// FE-C "Set Target Power" command (page 0x31)
// Power is sent in 1/4 watt units (0.25W resolution)
// Bytes: [0x31][0x25][0xFF][0xFF][0xFF][0xFF][power_low][power_high]
uint16_t power_quarter_watts = (uint16_t)(power * 4);
uint8_t power_cmd[] = {0x31, 0x25, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00};
power_cmd[6] = (uint8_t)(power_quarter_watts & 0xFF); // Low byte
power_cmd[7] = (uint8_t)((power_quarter_watts >> 8) & 0xFF); // High byte
writeCharacteristic(power_cmd, sizeof(power_cmd),
QStringLiteral("forcePower ") + QString::number(power) + QStringLiteral("W"),
false, false);
}
void bkoolbike::forceInclination(double inclination) {
@@ -115,13 +129,27 @@ void bkoolbike::update() {
uint8_t init1[] = {0x30, 0x25, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
uint8_t init2[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x1e, 0x7f, 0x00};
uint8_t init3[] = {0x33, 0x25, 0xff, 0xff, 0xff, 0x20, 0x4e, 0x00};
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21};
writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, false);
writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, false);
writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false);
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
if (bkool_fitness_bike) {
// BKOOLFITNESSBIKE specific init packets
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
uint8_t init5[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
uint8_t init6[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
uint8_t init7[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x25, 0x7f, 0x00};
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
writeCharacteristic(init6, sizeof(init6), QStringLiteral("init6"), false, true);
writeCharacteristic(init7, sizeof(init7), QStringLiteral("init7"), false, false);
} else {
// BKOOLSMARTPRO init packets
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21};
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
}
} else if (bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState //&&
@@ -137,6 +165,13 @@ void bkoolbike::update() {
// updateDisplay(elapsed);
}
// Send poll command for BKOOLFITNESSBIKE
/*
if (bkool_fitness_bike) {
uint8_t poll[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
writeCharacteristic(poll, sizeof(poll), QStringLiteral("poll"), false, false);
}*/
if (requestResistance != -1) {
if (requestResistance != currentResistance().value() || lastGearValue != gears()) {
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
@@ -146,10 +181,15 @@ void bkoolbike::update() {
requestInclination = requestResistance / 10.0;
}
// forceResistance(requestResistance);;
}
lastGearValue = gears();
}
requestResistance = -1;
}
if(lastGearValue != gears() && requestInclination == -100) {
// if only gears changed, we need to update the inclination to match the gears
requestInclination = lastRawRequestedInclinationValue;
}
if (requestInclination != -100) {
emit debug(QStringLiteral("writing inclination ") + QString::number(requestInclination));
forceInclination(requestInclination + gears()); // since this bike doesn't have the concept of resistance,
@@ -157,6 +197,8 @@ void bkoolbike::update() {
requestInclination = -100;
}
lastGearValue = gears();
if (requestPower != -1) {
changePower(requestPower);
requestPower = -1;
@@ -677,6 +719,12 @@ void bkoolbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
{
bluetoothDevice = device;
// Check if this is BKOOLFITNESSBIKE model
if (device.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) {
bkool_fitness_bike = true;
emit debug(QStringLiteral("BKOOLFITNESSBIKE model detected"));
}
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &bkoolbike::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &bkoolbike::serviceScanDone);

View File

@@ -45,6 +45,7 @@ class bkoolbike : public bike {
bool wait_for_response = false);
void startDiscover();
void forceInclination(double inclination);
void forcePower(int32_t power);
uint16_t watts() override;
double bikeResistanceToPeloton(double resistance);
@@ -70,6 +71,8 @@ class bkoolbike : public bike {
bool noWriteResistance = false;
bool noHeartService = false;
bool bkool_fitness_bike = false;
uint16_t pollCounter = 0;
uint16_t oldLastCrankEventTime = 0;
uint16_t oldCrankRevs = 0;

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();
@@ -655,7 +685,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
filter = (b.name().compare(filterDevice, Qt::CaseInsensitive) == 0);
}
if (b.name().startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
const QString deviceName = b.name();
const QString upperDeviceName = deviceName.toUpper();
bool isTrxAppGateUsbBikeTC = false;
if (upperDeviceName.startsWith(QStringLiteral("TC")) && deviceName.length() == 5) {
isTrxAppGateUsbBikeTC = true;
for (int idx = 2; idx < deviceName.length(); ++idx) {
if (!deviceName.at(idx).isDigit()) {
isTrxAppGateUsbBikeTC = false;
break;
}
}
}
if (deviceName.startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
if (m3ibike::isCorrectUnit(b)) {
this->setLastBluetoothDevice(b);
@@ -890,6 +932,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) {
@@ -957,7 +1013,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(domyosRower);
} else if ((b.name().startsWith(QStringLiteral("Domyos-Bike")) && (!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool())) &&
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) {
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
@@ -972,7 +1028,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(domyosBike);
} else if (b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower &&
} else if ((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower)) &&
!trxappgateusbRower && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -991,7 +1048,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,
@@ -1026,13 +1083,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(domyosElliptical);
} else if ((b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) ||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
b.name().toUpper().startsWith(QStringLiteral("SCH411/510E")) ||
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 && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
ypooElliptical =
@@ -1049,7 +1110,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(ypooElliptical);
} else if ((b.name().toUpper().startsWith(QStringLiteral("NAUTILUS E")) ||
b.name().toUpper().startsWith(QStringLiteral("NAUTILUS M"))) &&
b.name().toUpper().startsWith(QStringLiteral("NAUTILUS M")) ||
b.name().toUpper().startsWith(QStringLiteral("NAUTILUS 616"))) && // actually this is a bike that uses the same Bluetooth characteristics of the elliptical
!nautilusElliptical && // NAUTILUS E616
filter) {
this->setLastBluetoothDevice(b);
@@ -1182,6 +1244,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(soleElliptical);
} else if (b.name().startsWith(QStringLiteral("Domyos")) &&
!b.name().startsWith(QStringLiteral("DomyosBr")) &&
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKE-")) &&
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKING-")) && !domyos && !domyosElliptical && b.name().compare(ftms_treadmill, Qt::CaseInsensitive) &&
!domyosBike && !domyosRower && !ftmsBike && !horizonTreadmill &&
(!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts).toBool()) &&
@@ -1328,6 +1391,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();
@@ -1351,6 +1415,26 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(soleF80);
} else if (b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM01")) && !speraXTreadmill && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
speraXTreadmill = new speraxtreadmill(this->pollDeviceTime, noConsole, noHeartService);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
stateFileRead();
#endif
emit deviceConnected(b);
connect(speraXTreadmill, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
// connect(speraXTreadmill, SIGNAL(disconnected()), this, SLOT(restart()));
connect(speraXTreadmill, &speraxtreadmill::debug, this, &bluetooth::debug);
connect(speraXTreadmill, &speraxtreadmill::speedChanged, this, &bluetooth::speedChanged);
connect(speraXTreadmill, &speraxtreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
speraXTreadmill->deviceDiscovered(b);
connect(this, &bluetooth::searchingStop, speraXTreadmill, &speraxtreadmill::searchingStop);
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(speraXTreadmill);
} else if ((b.name().toUpper().startsWith(QStringLiteral("LF")) && (b.name().length() == 18 || b.name().length() == 15)) &&
!lifefitnessTreadmill && filter) {
this->setLastBluetoothDevice(b);
@@ -1418,18 +1502,25 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("AB300S-")) ||
b.name().toUpper().startsWith(QStringLiteral("TF04-")) || // Sport Synology Z5 Treadmill #2415
(b.name().toUpper().startsWith(QStringLiteral("FIT-")) && !b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) || // FIT-1596 and sports tech f37s treadmill #2412
b.name().toUpper().startsWith(QStringLiteral("FIT-TM-")) || // FIT-TM- treadmill with real inclination
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
(b.name().toUpper().startsWith("SCHWINN 810")) ||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
(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("TP1")) && b.name().length() == 3) ||
(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))) ||
(b.name().toUpper().startsWith(QStringLiteral("XT485")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
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 T90-")) || // FTMS
@@ -1437,12 +1528,13 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("CITYSPORTS-LINKER")) ||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15 && ftms_bike.contains(QZSettings::default_ftms_bike)) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("S77")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F89")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) // FTMS
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) // FTMS
) &&
!horizonTreadmill && filter) {
this->setLastBluetoothDevice(b);
@@ -1538,6 +1630,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((b.name().toUpper().startsWith("TACX ") ||
b.name().toUpper().startsWith(QStringLiteral("THINK X")) ||
b.name().toUpper().startsWith(QStringLiteral("THINK-")) ||
b.name().toUpper().startsWith(QStringLiteral("THINK_")) ||
b.name().toUpper().startsWith(QStringLiteral("53997-")) ||
(b.name().toUpper().startsWith("VANRYSEL-HT")) ||
b.address() == QBluetoothAddress("C1:14:D9:9C:FB:01") || // specific TACX NEO 2 #1707
(b.name().toUpper().startsWith("TACX SMART BIKE"))) &&
@@ -1634,6 +1728,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("KETTLERBLE")) ||
(b.name().toUpper().startsWith("JAS_C3")) ||
(b.name().toUpper().startsWith("SCH_190U")) ||
(b.name().toUpper().startsWith("SCH_290R")) ||
(b.name().toUpper().startsWith("RAVE WHITE")) ||
(b.name().toUpper().startsWith("DOMYOS-BIKING-")) ||
(b.name().startsWith(QStringLiteral("Domyos-Bike")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && !settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool()) ||
@@ -1674,24 +1769,32 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("LYDSTO")) ||
(b.name().toUpper().startsWith("CYCLO_")) ||
(b.name().toUpper().startsWith("SL010-")) ||
(b.name().toUpper().startsWith("EXPERT-SX9")) ||
(b.name().toUpper().startsWith("LCR")) ||
(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("ORLAUF_ARES")) ||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
(b.name().toUpper().startsWith("XCX-")) ||
(b.name().toUpper().startsWith("D500V2")) ||
(b.name().toUpper().startsWith("NEO BIKE PLUS ")) ||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) && !b.name().toUpper().endsWith(QStringLiteral("SKI")) && !b.name().toUpper().endsWith(QStringLiteral("ROW"))) ||
(b.name().toUpper().startsWith("L-") && b.name().length() == 11) ||
(b.name().toUpper().startsWith("DMASUN-") && b.name().toUpper().endsWith("-BIKE")) ||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1
(b.name().toUpper().startsWith("FS-YK-")) ||
(b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) ||
(b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) ||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
(b.name().toUpper().startsWith("DIRETO X")) || (b.name().toUpper().startsWith("MERACH-667-")) ||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
!ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) {
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
ftms_rower.contains(QZSettings::default_ftms_rower) &&
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
ftmsBike = new ftmsbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
@@ -1705,6 +1808,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) {
@@ -1719,7 +1823,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();
@@ -1766,9 +1871,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
((b.name().toUpper().startsWith("KU")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith("ELITETRAINER")) ||
(b.name().toUpper().startsWith("TOUR 600")) ||
(b.name().toUpper().startsWith("SMART+ #")) ||
(b.name().toUpper().startsWith(QStringLiteral("QD")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("RM")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("DR")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("DFC")) && b.name().length() == 3) ||
(b.name().toUpper().startsWith(QStringLiteral("THINK A")) && b.name().length() == 18) ||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
!stagesBike && !ftmsBike && filter) {
@@ -1824,6 +1932,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")) ||
@@ -1980,7 +2089,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(apexBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
apexBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(apexBike);
} else if (b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) && !bkoolBike && filter) {
} else if ((b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) ||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) && !bkoolBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
bkoolBike = new bkoolbike(noWriteResistance, noHeartService);
@@ -2212,6 +2322,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
coreSensor = new coresensor();
connect(coreSensor, &coresensor::debug, this, &bluetooth::debug);
coreSensor->deviceDiscovered(b);
connect(coreSensor, &coresensor::coreBodyTemperatureChanged, this->device(), &bluetoothdevice::coreBodyTemperature);
connect(coreSensor, &coresensor::skinTemperatureChanged, this->device(), &bluetoothdevice::skinTemperature);
connect(coreSensor, &coresensor::heatStrainIndexChanged, this->device(), &bluetoothdevice::heatStrainIndex);
} else if (b.name().toUpper().startsWith(QStringLiteral("BOWFLEX T")) && !bowflexT216Treadmill && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -2357,16 +2470,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(activioTreadmill, &activiotreadmill::debug, this, &bluetooth::debug);
activioTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(activioTreadmill);
} else if (((b.name().startsWith(QStringLiteral("TOORX"))) ||
(b.name().startsWith(QStringLiteral("V-RUN"))) ||
(b.name().toUpper().startsWith(QStringLiteral("K80_"))) ||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) ||
(b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) ||
(b.name().toUpper().startsWith(QStringLiteral("I-RUNNING"))) ||
(b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) ||
(b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) ||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) &&
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower &&
} else if (((deviceName.startsWith(QStringLiteral("TOORX"))) ||
(deviceName.startsWith(QStringLiteral("V-RUN"))) ||
(upperDeviceName.startsWith(QStringLiteral("K80_"))) ||
(upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+"))) ||
(upperDeviceName.startsWith(QStringLiteral("ICONSOLE+"))) ||
(upperDeviceName.startsWith(QStringLiteral("I-RUNNING"))) ||
(upperDeviceName.startsWith(QStringLiteral("DKN RUN"))) ||
(upperDeviceName.startsWith(QStringLiteral("ADIDAS "))) ||
(upperDeviceName.startsWith(QStringLiteral("REEBOK")))) &&
!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();
@@ -2378,24 +2492,25 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(trxappgateusb, &trxappgateusbtreadmill::debug, this, &bluetooth::debug);
trxappgateusb->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(trxappgateusb);
} else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) ||
b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) ||
b.name().toUpper().startsWith(QStringLiteral("FIT HI WAY")) ||
b.name().toUpper().startsWith(QStringLiteral("BIKZU_")) ||
b.name().toUpper().startsWith(QStringLiteral("PASYOU-")) ||
b.name().toUpper().startsWith(QStringLiteral("VIRTUFIT")) ||
b.name().toUpper().startsWith(QStringLiteral("IBIKING+")) ||
((b.name().startsWith(QStringLiteral("TOORX")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) ||
b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+")) ||
b.name().toUpper().startsWith(QStringLiteral("VIFHTR2.1")) ||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK"))) ||
b.name().toUpper().contains(QStringLiteral("CR011R")) ||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
} else if (isTrxAppGateUsbBikeTC ||
(upperDeviceName.startsWith(QStringLiteral("TUN ")) ||
upperDeviceName.startsWith(QStringLiteral("FITHIWAY")) ||
upperDeviceName.startsWith(QStringLiteral("FIT HI WAY")) ||
upperDeviceName.startsWith(QStringLiteral("BIKZU_")) ||
upperDeviceName.startsWith(QStringLiteral("PASYOU-")) ||
upperDeviceName.startsWith(QStringLiteral("VIRTUFIT")) ||
upperDeviceName.startsWith(QStringLiteral("IBIKING+")) ||
((deviceName.startsWith(QStringLiteral("TOORX")) ||
upperDeviceName.startsWith(QStringLiteral("I-CONSOIE+")) ||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
upperDeviceName.startsWith(QStringLiteral("ICONSOLE+")) ||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
upperDeviceName.contains(QStringLiteral("CR011R")) ||
(upperDeviceName.startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
upperDeviceName.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 =
@@ -2431,6 +2546,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
keepBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(keepBike);
} else if ((b.name().toUpper().startsWith(QStringLiteral("LCB")) ||
b.name().toUpper().startsWith("LCR") ||
b.name().toUpper().startsWith(QStringLiteral("R92"))) &&
!soleBike && filter) {
this->setLastBluetoothDevice(b);
@@ -2498,7 +2614,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
b.name().startsWith(QStringLiteral("MRK-"))) &&
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && filter) {
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
fitPlusBike =
@@ -2530,6 +2646,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) {
@@ -2644,11 +2761,11 @@ void bluetooth::connectedAndDiscovered() {
settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
// only at the first very connection, setting the user default resistance
if (device() && firstConnected && device()->deviceType() == bluetoothdevice::BIKE &&
if (device() && firstConnected && device()->deviceType() == BIKE &&
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() != 1) {
qobject_cast<bike *>(device())->changeResistance(
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt());
} else if (device() && firstConnected && device()->deviceType() == bluetoothdevice::ELLIPTICAL &&
} else if (device() && firstConnected && device()->deviceType() == ELLIPTICAL &&
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() !=
1) {
qobject_cast<elliptical *>(device())->changeResistance(
@@ -2816,7 +2933,7 @@ void bluetooth::connectedAndDiscovered() {
#else
settings.setValue(QZSettings::power_sensor_address, b.deviceUuid().toString());
#endif
if (device() && device()->deviceType() == bluetoothdevice::BIKE) {
if (device() && device()->deviceType() == BIKE) {
powerSensor = new stagesbike(false, false, true);
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2825,7 +2942,7 @@ void bluetooth::connectedAndDiscovered() {
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
&bluetoothdevice::cadenceSensor);
powerSensor->deviceDiscovered(b);
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
} else if (device() && device()->deviceType() == TREADMILL) {
powerSensorRun = new strydrunpowersensor(false, false, true);
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2882,7 +2999,7 @@ void bluetooth::connectedAndDiscovered() {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().startsWith(eliteSterzoSmartName))) && !eliteSterzoSmart &&
!eliteSterzoSmartName.startsWith(QStringLiteral("Disabled")) && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
settings.setValue(QZSettings::elite_sterzo_smart_lastdevice_name, b.name());
#ifndef Q_OS_IOS
@@ -2904,7 +3021,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("SRAM "))) && !sramAXSController && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
sramAXSController = new sramaxscontroller();
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2923,7 +3040,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("ZWIFT CLICK"))) && !zwiftClickRemote && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
if(b.manufacturerData(2378).size() > 0) {
qDebug() << "this should be 9. is it? " << int(b.manufacturerData(2378).at(0));
@@ -2948,7 +3065,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
eliteSquareController = new elitesquarecontroller(this->device());
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2968,7 +3085,7 @@ void bluetooth::connectedAndDiscovered() {
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE") || b.name().toUpper().startsWith("ZWIFT SF2")) && zwiftPlayDevice.size() < 2 && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
if(b.manufacturerData(2378).size() > 0) {
qDebug() << "this should be 3 or 2. is it? " << int(b.manufacturerData(2378).at(0));
@@ -3000,18 +3117,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());
device()->deviceType() == TREADMILL ||
device()->deviceType() == ELLIPTICAL,
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()) {
@@ -3159,6 +3281,11 @@ void bluetooth::restart() {
delete horizonTreadmill;
horizonTreadmill = nullptr;
}
if (speraXTreadmill) {
delete speraXTreadmill;
speraXTreadmill = nullptr;
}
if (lifefitnessTreadmill) {
delete lifefitnessTreadmill;
@@ -3294,6 +3421,11 @@ void bluetooth::restart() {
delete nordictrackifitadbElliptical;
nordictrackifitadbElliptical = nullptr;
}
if (nordictrackifitadbRower) {
delete nordictrackifitadbRower;
nordictrackifitadbRower = nullptr;
}
if (powerBike) {
delete powerBike;
@@ -3768,6 +3900,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) {
@@ -3824,6 +3958,8 @@ bluetoothdevice *bluetooth::device() {
return horizonTreadmill;
} else if (lifefitnessTreadmill) {
return lifefitnessTreadmill;
} else if (speraXTreadmill) {
return speraXTreadmill;
} else if (technogymmyrunTreadmill) {
return technogymmyrunTreadmill;
#ifndef Q_OS_IOS
@@ -4006,7 +4142,7 @@ void bluetooth::stateFileUpdate() {
if (!device()) {
return;
}
if (device()->deviceType() != bluetoothdevice::TREADMILL) {
if (device()->deviceType() != TREADMILL) {
return;
}

View File

@@ -21,6 +21,7 @@
#include "qzsettings.h"
#include "devices/activiotreadmill/activiotreadmill.h"
#include "devices/speraxtreadmill/speraxtreadmill.h"
#include "devices/antbike/antbike.h"
#include "devices/android_antbike/android_antbike.h"
#include "devices/apexbike/apexbike.h"
@@ -88,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"
@@ -206,6 +208,7 @@ class bluetooth : public QObject, public SignalHandler {
trxappgateusbtreadmill *trxappgateusb = nullptr;
spirittreadmill *spiritTreadmill = nullptr;
activiotreadmill *activioTreadmill = nullptr;
speraxtreadmill *speraXTreadmill = nullptr;
nautilusbike *nautilusBike = nullptr;
nautiluselliptical *nautilusElliptical = nullptr;
nautilustreadmill *nautilusTreadmill = nullptr;
@@ -220,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

@@ -20,7 +20,7 @@ bluetoothdevice::~bluetoothdevice() {
}
}
bluetoothdevice::BLUETOOTH_TYPE bluetoothdevice::deviceType() { return bluetoothdevice::UNKNOWN; }
BLUETOOTH_TYPE bluetoothdevice::deviceType() { return UNKNOWN; }
void bluetoothdevice::start() { requestStart = 1; lastStart = QDateTime::currentMSecsSinceEpoch(); }
void bluetoothdevice::stop(bool pause) {
requestStop = 1;
@@ -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) {
@@ -132,7 +177,20 @@ bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
}
bool bluetoothdevice::connected() { return false; }
metric bluetoothdevice::elevationGain() { return elevationAcc; }
void bluetoothdevice::heartRate(uint8_t heart) { Heart.setValue(heart); }
void bluetoothdevice::heartRate(uint8_t heart) {
Heart.setValue(heart);
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
// Write heart rate from Bluetooth to Apple Health during workout
lockscreen h;
if(heart > 0)
h.setHeartRate(heart);
#endif
#endif
}
void bluetoothdevice::coreBodyTemperature(double coreBodyTemperature) { CoreBodyTemperature.setValue(coreBodyTemperature); }
void bluetoothdevice::skinTemperature(double skinTemperature) { SkinTemperature.setValue(skinTemperature); }
void bluetoothdevice::heatStrainIndex(double heatStrainIndex) { HeatStrainIndex.setValue(heatStrainIndex); }
void bluetoothdevice::disconnectBluetooth() {
if (m_control) {
m_control->disconnectFromDevice();
@@ -189,7 +247,7 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts, const b
!power_as_bike && !power_as_treadmill)
watt_calc = false;
if(deviceType() == bluetoothdevice::BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
if(deviceType() == BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
_ergTable.collectData(Cadence.value(), m_watt.value(), Resistance.value());
if (!_firstUpdate && !paused) {
@@ -251,7 +309,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());
@@ -268,6 +336,16 @@ 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;
bool useMiles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value(), useMiles);
#endif
#endif
}
void bluetoothdevice::clearStats() {
@@ -276,6 +354,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);
@@ -288,6 +368,9 @@ void bluetoothdevice::clearStats() {
Cadence.clear(false);
for(int i=0; i<maxHeartZone(); i++) {
hrZonesSeconds[i].clear(false);
}
for(int i=0; i<maxHeatZone(); i++) {
heatZonesSeconds[i].clear(false);
}
}
@@ -298,6 +381,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);
@@ -309,6 +394,9 @@ void bluetoothdevice::setPaused(bool p) {
Cadence.setPaused(p);
for(int i=0; i<maxHeartZone(); i++) {
hrZonesSeconds[i].setPaused(p);
}
for(int i=0; i<maxHeatZone(); i++) {
heatZonesSeconds[i].setPaused(p);
}
}
@@ -318,6 +406,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);
@@ -329,6 +419,9 @@ void bluetoothdevice::setLap() {
Cadence.setLap(false);
for(int i=0; i<maxHeartZone(); i++) {
hrZonesSeconds[i].setLap(false);
}
for(int i=0; i<maxHeatZone(); i++) {
heatZonesSeconds[i].setLap(false);
}
}
@@ -493,9 +586,39 @@ void bluetoothdevice::setHeartZone(double hz) {
}
}
void bluetoothdevice::setHeatZone(double heatStrainIndex) {
// Determine heat zone based on Heat Strain Index values
uint8_t zone;
if (heatStrainIndex >= 0 && heatStrainIndex <= 1.99) {
zone = 1;
} else if (heatStrainIndex < 3.0) {
zone = 2;
} else if (heatStrainIndex < 7.0) {
zone = 3;
} else {
zone = 4;
}
HeatZone = zone;
if(isPaused() == false && heatStrainIndex > 0) {
// Convert to 0-based index for array access
uint8_t zoneIndex = zone - 1;
if(zoneIndex < maxHeatZone()) {
heatZonesSeconds[zoneIndex].setValue(heatZonesSeconds[zoneIndex].value() + 1);
}
}
}
uint32_t bluetoothdevice::secondsForHeartZone(uint8_t zone) {
if(zone < maxHeartZone()) {
return hrZonesSeconds[zone].value();
}
return 0;
}
uint32_t bluetoothdevice::secondsForHeatZone(uint8_t zone) {
if(zone < maxHeatZone()) {
return heatZonesSeconds[zone].value();
}
return 0;
}

View File

@@ -1,6 +1,7 @@
#ifndef BLUETOOTHDEVICE_H
#define BLUETOOTHDEVICE_H
#include "bluetoothdevicetype.h"
#include "definitions.h"
#include "metric.h"
#include "qzsettings.h"
@@ -108,11 +109,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
@@ -370,6 +379,24 @@ class bluetoothdevice : public QObject {
*/
uint32_t secondsForHeartZone(uint8_t zone);
/**
* @brief currentHeatZone Gets a metric object to get or set the current heat zone. Units: depends on
* implementation (based on Heat Strain Index: Zone 1: 0-1.99, Zone 2: 2-2.99, Zone 3: 3-6.99, Zone 4: 7+)
*/
metric currentHeatZone() { return HeatZone; }
/**
* @brief maxHeatZone Gets the maximum number of heat zones.
*/
uint8_t maxHeatZone() { return maxheatzone; }
/**
* @brief secondsForHeatZone Gets the number of seconds in the current heat zone.
*
* @param zone The heat zone.
*/
uint32_t secondsForHeatZone(uint8_t zone);
/**
* @brief currentPowerZone Gets a metric object to get or set the current power zome. Units: depends on
* implementation.
@@ -404,6 +431,13 @@ class bluetoothdevice : public QObject {
*/
void setHeartZone(double hz);
/**
* @brief setHeatZone Set the current heat zone based on Heat Strain Index.
* This is equivalent to currentHeatZone().setvalue(hz)
* @param heatStrainIndex The heat strain index to determine zone. Unit: depends on implementation.
*/
void setHeatZone(double heatStrainIndex);
/**
* @brief setPowerZone Set the current power zone.
* This is equivalent to currentPowerZone().setvalue(pz)
@@ -418,7 +452,6 @@ class bluetoothdevice : public QObject {
*/
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
/**
@@ -455,6 +488,9 @@ class bluetoothdevice : public QObject {
virtual void cadenceSensor(uint8_t cadence);
virtual void powerSensor(uint16_t power);
virtual void speedSensor(double speed);
virtual void coreBodyTemperature(double coreBodyTemperature);
virtual void skinTemperature(double skinTemperature);
virtual void heatStrainIndex(double heatStrainIndex);
virtual void inclinationSensor(double grade, double inclination);
virtual void changeResistance(resistance_t res);
virtual void changePower(int32_t power);
@@ -520,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
@@ -678,6 +716,11 @@ class bluetoothdevice : public QObject {
*/
metric HeartZone;
/**
* @brief HeatZone A metric to get and set the current heat zone. Unit: depends on implementation
*/
metric HeatZone;
/**
* @brief PowerZone A metric to get and set the current power zone. Unit: depends on implementation
*/
@@ -704,6 +747,12 @@ class bluetoothdevice : public QObject {
static const uint8_t maxhrzone = 5;
metric hrZonesSeconds[maxhrzone];
/**
* @brief Collect the number of seconds in each zone for the current heat strain index
*/
static const uint8_t maxheatzone = 4;
metric heatZonesSeconds[maxheatzone];
bluetoothdevice::WORKOUT_EVENT_STATE lastState;
/**

View File

@@ -23,7 +23,7 @@ bowflext216treadmill::bowflext216treadmill(uint32_t pollDeviceTime, bool noConso
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -285,9 +285,9 @@ double bowflext216treadmill::GetSpeedFromPacket(const QByteArray &packet) {
double data = (double)convertedData / 100.0f;
return data * 1.60934;
} else if(bowflex_T128) {
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(12)) + ((uint16_t)((uint8_t)packet.at(13)) << 8);
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(9)) + ((uint16_t)((uint8_t)packet.at(10)) << 8);
double data = (double)convertedData / 100.0f;
return data;
return data * 1.60934;
} else if (bowflex_t6 == false) {
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(6)) + (((uint16_t)((uint8_t)packet.at(7)) << 8) & 0xFF00);
double data = (double)convertedData / 100.0f;

View File

@@ -23,7 +23,7 @@ bowflextreadmill::bowflextreadmill(uint32_t pollDeviceTime, bool noConsole, bool
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
//#include <QtBluetooth/private/qlowenergyserviceprivate_p.h>
chronobike::chronobike(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
t_timeout = new QTimer(this);

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
computrainerbike::computrainerbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
QSettings settings;
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
target_watts.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

@@ -20,7 +20,7 @@
using namespace std::chrono_literals;
concept2skierg::concept2skierg(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -23,7 +23,7 @@ crossrope::crossrope(uint32_t pollDeviceTime, bool noConsole, bool noHeartServic
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -4,7 +4,7 @@ using namespace std::chrono_literals;
csafeelliptical::csafeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice,
int8_t bikeResistanceOffset, double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -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

@@ -3,7 +3,7 @@
using namespace std::chrono_literals;
csaferower::csaferower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -72,7 +72,14 @@ void cscbike::update() {
}
}
m_watt = wattFromHR(false);
bool rogue_echo_bike = settings.value(QZSettings::rogue_echo_bike, QZSettings::default_rogue_echo_bike).toBool();
if (rogue_echo_bike) {
double rpm = currentCadence().value();
m_watt = 0.000602337 * pow(rpm, 3.11762) + 32.6404;
} else {
m_watt = wattFromHR(false);
}
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
if (m_control->state() == QLowEnergyController::UnconnectedState) {

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
cycleopsphantombike::cycleopsphantombike(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
@@ -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

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
deerruntreadmill::deerruntreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -75,6 +75,46 @@ void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac
}
}
void deerruntreadmill::waitForAPacket() {
QEventLoop loop;
QTimer timeout;
connect(this, &deerruntreadmill::packetReceived, &loop, &QEventLoop::quit);
timeout.singleShot(3000, &loop, SLOT(quit()));
loop.exec();
}
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;
@@ -90,18 +130,70 @@ uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
return result;
}
uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
uint8_t result = 0;
if (size < 5) {
qDebug() << QStringLiteral("array too small for PitPat checksum");
return 0;
}
// For PitPat protocol:
// 1. XOR from byte 5 to byte (size - 3) for long messages (>= 7 bytes)
// or from byte 2 to byte (size - 3) for short messages (< 7 bytes)
// 2. XOR the result with byte 1
size_t startIdx = (size < 7) ? 2 : 5;
for (size_t i = startIdx; i <= size - 3; i++) {
result ^= arr[i];
}
// XOR with byte 1 (command byte)
result ^= arr[1];
return result;
}
void deerruntreadmill::forceSpeed(double requestSpeed) {
QSettings settings;
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
if (pitpat) {
// PitPat speed template
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x07, 0x6c, 0x01, 0x00, 0x8a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x2e, 0x0c, 0xc3, 0x43};
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
writeSpeed[7] = speed & 0xFF; // Low byte
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed), false, true);
} else if (superun_ba04) {
// Superun BA04 speed template
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed BA04 speed=") + QString::number(requestSpeed), false, false);
} else {
// Default speed template
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
}
}
void deerruntreadmill::forceIncline(double requestIncline) {
@@ -183,33 +275,40 @@ 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) {
forceSpeed(1.0);
} 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();
} else if (requestStop != -1) {
emit debug(QStringLiteral("stopping... ") + paused);
/*if (lastState == PAUSED) {
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
true);
} else*/ {
if (pitpat) {
uint8_t stop[] = {
0x6a, 0x17, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00,
0x8a, 0x00, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x12, 0x2e,
0x0c, 0xaa, 0x43};
writeCharacteristic(gattWriteCharacteristic, stop, sizeof(stop), QStringLiteral("stop"), false, true);
} else {
uint8_t stop[] = {0x4d, 0x00, 0x48, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x50, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd6, 0x43};
stop[2] = pollCounter;
@@ -219,11 +318,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 +369,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 +450,42 @@ 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);
} else if (superun_ba04) {
// Superun BA04 treadmill initialization sequence
// Wait for initial packet from treadmill before sending init
emit debug(QStringLiteral("BA04: waiting for initial packet..."));
waitForAPacket();
// Init 1: pollCounter = 0
uint8_t initData1[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
initData1[2] = 0; // pollCounter = 0
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("BA04 init 1"), false, true);
uint8_t initData2[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
initData1[2] = 1; // pollCounter = 0
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("BA04 init 2"), false, true);
// Init 2: pollCounter = 1
uint8_t initData3[] = {0x4d, 0x00, 0x01, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x05, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0x7c, 0x43};
initData3[2] = 2; // pollCounter = 1
writeCharacteristic(gattWriteCharacteristic, initData3, sizeof(initData3), QStringLiteral("BA04 init 3"), false, true);
// Start pollCounter from 2 after init
pollCounter = 3;
}
initDone = true;
}
@@ -352,11 +495,32 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xfff1);
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2);
QBluetoothUuid _pitpatWriteCharacteristicId((quint16)0xfba1);
QBluetoothUuid _pitpatNotifyCharacteristicId((quint16)0xfba2);
QBluetoothUuid _superunWriteCharacteristicId((quint16)0xff01);
QBluetoothUuid _superunNotifyCharacteristicId((quint16)0xff02);
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 +528,17 @@ 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 if (superun_ba04) {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_superunWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_superunNotifyCharacteristicId);
} else {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
}
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotifyCharacteristic.isValid());
@@ -403,6 +576,9 @@ void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha
void deerruntreadmill::serviceScanDone(void) {
QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0);
QBluetoothUuid _pitpatServiceId((quint16)0xfba0);
QBluetoothUuid _superunServiceId((quint16)0xffff);
QBluetoothUuid _unlockServiceId((quint16)0x1801);
emit debug(QStringLiteral("serviceScanDone"));
auto services_list = m_control->services();
@@ -411,7 +587,22 @@ 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 if (services_list.contains(_superunServiceId)) {
superun_ba04 = true;
pitpat = false;
emit debug(QStringLiteral("Detected Superun BA04 treadmill variant"));
gattCommunicationChannelService = m_control->createServiceObject(_superunServiceId);
} else {
pitpat = false;
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
}
if (gattCommunicationChannelService) {
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
&deerruntreadmill::stateChanged);
@@ -419,6 +610,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

@@ -41,6 +41,7 @@ class deerruntreadmill : public treadmill {
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
bool connected() override;
double minStepInclination() override;
double minStepSpeed() override { return 0.1; }
private:
void forceSpeed(double requestSpeed);
@@ -48,8 +49,11 @@ 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 waitForAPacket();
void startDiscover();
uint8_t calculateXOR(uint8_t arr[], size_t size);
uint8_t calculatePitPatChecksum(uint8_t arr[], size_t size);
bool noConsole = false;
bool noHeartService = false;
uint32_t pollDeviceTime = 200;
@@ -66,6 +70,12 @@ class deerruntreadmill : public treadmill {
QLowEnergyService *gattCommunicationChannelService = nullptr;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotifyCharacteristic;
QLowEnergyService *unlock_service = nullptr;
QLowEnergyCharacteristic unlock_characteristic;
bool pitpat = false;
bool superun_ba04 = false;
bool initDone = false;
bool initRequest = false;

View File

@@ -43,7 +43,7 @@ using namespace std::chrono_literals;
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0xE005, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_E005, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD2, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD3, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x00\x01"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
@@ -51,7 +51,7 @@ using namespace std::chrono_literals;
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2ACD, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, \
P3) \
@@ -171,9 +171,9 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
QSettings settings;
DirconProcessorService *service;
QList<DirconProcessorService *> services, proc_services;
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
bt = Bike;
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
uint8_t type = dt == TREADMILL || dt == ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
: DM_MACHINE_TYPE_BIKE;
qDebug() << "Building Dircom Manager";
uint16_t server_base_port =
@@ -236,7 +236,7 @@ double DirconManager::currentGear() {
QSettings settings;
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool() && writeP0003)
return writeP0003->currentGear();
else if(bt && bt->deviceType() == bluetoothdevice::BIKE)
else if(bt && bt->deviceType() == BIKE)
return ((bike*)bt)->gears();
return 0;
}

View File

@@ -96,11 +96,11 @@ int DirconPacket::parse(const QByteArray &buf, int last_seq_number) {
} else
return DPKT_PARSE_ERROR - rembuf;
} else if (this->Identifier == DPKT_MSGID_ENABLE_CHARACTERISTIC_NOTIFICATIONS) {
if (this->Length == 16 || this->Length == 17) {
if (this->Length >= 16) {
quint16 uuid = ((quint16)buf.at(DPKT_MESSAGE_HEADER_LENGTH + DPKT_POS_SH8)) << 8;
uuid |= ((quint16)buf.at(DPKT_MESSAGE_HEADER_LENGTH + DPKT_POS_SH0)) & 0x00FF;
this->uuid = uuid;
if (this->Length == 17) {
if (this->Length >= 17) {
this->isRequest = true;
this->additional_data = buf.mid(DPKT_MESSAGE_HEADER_LENGTH + 16, 1);
}
@@ -117,6 +117,12 @@ int DirconPacket::parse(const QByteArray &buf, int last_seq_number) {
return rembuf;
} else
return DPKT_PARSE_ERROR - rembuf;
} else if (this->Identifier == DPKT_MSGID_UNKNOWN_0x07) {
if (this->Length == 0) {
this->isRequest = this->checkIsRequest(last_seq_number);
return DPKT_MESSAGE_HEADER_LENGTH;
} else
return DPKT_PARSE_ERROR - rembuf;
} else
return DPKT_PARSE_ERROR - rembuf;
} else
@@ -182,6 +188,10 @@ QByteArray DirconPacket::encode(int last_seq_number) {
}
}
}
} else if (this->Identifier == DPKT_MSGID_UNKNOWN_0x07) {
// Unknown message 0x07 - always respond with empty payload
this->Length = 0;
byteout.append(2, 0);
} else if (this->Identifier == DPKT_MSGID_DISCOVER_CHARACTERISTICS && !this->isRequest) {
this->Length = 16 + this->uuids.size() * 17;
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));

View File

@@ -25,6 +25,7 @@
#define DPKT_MSGID_WRITE_CHARACTERISTIC 0x04
#define DPKT_MSGID_ENABLE_CHARACTERISTIC_NOTIFICATIONS 0x05
#define DPKT_MSGID_UNSOLICITED_CHARACTERISTIC_NOTIFICATION 0x06
#define DPKT_MSGID_UNKNOWN_0x07 0x07
#define DPKT_RESPCODE_SUCCESS_REQUEST 0x00
#define DPKT_RESPCODE_UNKNOWN_MESSAGE_TYPE 0x01
#define DPKT_RESPCODE_UNEXPECTED_ERROR 0x02

View File

@@ -187,7 +187,7 @@ DirconPacket DirconProcessor::processPacket(DirconProcessorClient *client, const
foreach (cc, service->chars) {
if (cc->uuid == pkt.uuid) {
cfound = true;
if (cc->type & DPKT_CHAR_PROP_FLAG_NOTIFY) {
if (cc->type & (DPKT_CHAR_PROP_FLAG_NOTIFY | DPKT_CHAR_PROP_FLAG_INDICATE)) {
int idx;
char notif = pkt.additional_data.at(0);
out.uuid = pkt.uuid;
@@ -208,6 +208,9 @@ DirconPacket DirconProcessor::processPacket(DirconProcessorClient *client, const
}
if (!cfound)
out.ResponseCode = DPKT_RESPCODE_CHARACTERISTIC_NOT_FOUND;
} else if (pkt.Identifier == DPKT_MSGID_UNKNOWN_0x07) {
// Unknown message 0x07 - respond with success
out.ResponseCode = DPKT_RESPCODE_SUCCESS_REQUEST;
}
}
return out;

View File

@@ -3,6 +3,8 @@
#include "keepawakehelper.h"
#endif
#include "virtualdevices/virtualbike.h"
#include "homeform.h"
#include "qzsettings.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
@@ -15,7 +17,7 @@ using namespace std::chrono_literals;
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
@@ -593,6 +595,22 @@ void domyosbike::serviceScanDone(void) {
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("49535343-fe7d-4ae5-8fa9-9fafd205e455"));
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
if(!gattCommunicationChannelService) {
// Main service not found, check if FTMS service is available
QBluetoothUuid ftmsServiceId((quint16)0x1826);
QLowEnergyService *ftmsService = m_control->createServiceObject(ftmsServiceId);
if(ftmsService) {
QSettings settings;
settings.setValue(QZSettings::ftms_bike, bluetoothDevice.name());
qDebug() << "forcing FTMS bike since it has FTMS service but not the main domyos service";
if(homeform::singleton())
homeform::singleton()->setToastRequested("FTMS bike found, restart the app to apply the change");
delete ftmsService;
}
return;
}
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &domyosbike::stateChanged);
gattCommunicationChannelService->discoverDetails();
}
@@ -601,6 +619,8 @@ void domyosbike::errorService(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
qDebug() << QStringLiteral("domyosbike::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString();
m_control->disconnectFromDevice();
}
void domyosbike::error(QLowEnergyController::Error err) {

View File

@@ -17,7 +17,7 @@ using namespace std::chrono_literals;
domyoselliptical::domyoselliptical(bool noWriteResistance, bool noHeartService, bool testResistance,
int8_t bikeResistanceOffset, double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

@@ -18,7 +18,7 @@ using namespace std::chrono_literals;
domyosrower::domyosrower(bool noWriteResistance, bool noHeartService, bool testResistance, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

@@ -56,7 +56,7 @@ domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool n
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -23,7 +23,7 @@ echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartSer
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -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

@@ -23,7 +23,7 @@ echelonrower::echelonrower(bool noWriteResistance, bool noHeartService, int8_t b
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
speedRaw.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

@@ -15,7 +15,7 @@ using namespace std::chrono_literals;
echelonstairclimber::echelonstairclimber(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -15,7 +15,7 @@ using namespace std::chrono_literals;
echelonstride::echelonstride(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -16,7 +16,7 @@
using namespace std::chrono_literals;
eliterizer::eliterizer(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -15,7 +15,7 @@
using namespace std::chrono_literals;
elitesterzosmart::elitesterzosmart(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -22,11 +22,15 @@ void elliptical::update_metrics(bool watt_calc, const double watts) {
WeightLoss = metric::calculateWeightLoss(KCal.value());
WattKg = m_watt.value() / settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
} else if (m_watt.value() > 0) {
m_watt = 0;
if (watt_calc) {
m_watt = 0;
}
WattKg = 0;
}
} else if (m_watt.value() > 0) {
m_watt = 0;
if (watt_calc) {
m_watt = 0;
}
WattKg = 0;
}
@@ -137,7 +141,7 @@ metric elliptical::currentInclination() { return Inclination; }
uint8_t elliptical::fanSpeed() { return FanSpeed; }
bool elliptical::connected() { return false; }
bluetoothdevice::BLUETOOTH_TYPE elliptical::deviceType() { return bluetoothdevice::ELLIPTICAL; }
BLUETOOTH_TYPE elliptical::deviceType() { return ELLIPTICAL; }
void elliptical::clearStats() {
moving.clear(true);

View File

@@ -24,7 +24,7 @@ class elliptical : public bluetoothdevice {
virtual int pelotonToEllipticalResistance(int pelotonResistance);
virtual bool inclinationAvailableByHardware();
virtual bool inclinationSeparatedFromResistance();
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
BLUETOOTH_TYPE deviceType() override;
void clearStats() override;
void setPaused(bool p) override;
void setLap() override;

View File

@@ -71,7 +71,7 @@ class CRC8
eslinkertreadmill::eslinkertreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService,
double forceInitSpeed, double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -106,8 +106,12 @@ void eslinkertreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, con
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
}
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
@@ -704,6 +708,8 @@ void eslinkertreadmill::stateChanged(QLowEnergyService::ServiceState state) {
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotifyCharacteristic.isValid());
qDebug() << (gattWriteCharacteristic.properties() & QLowEnergyService::WriteWithoutResponse);
// establish hook into notifications
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
&eslinkertreadmill::characteristicChanged);

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
fakebike::fakebike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -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

@@ -16,7 +16,7 @@
using namespace std::chrono_literals;
fakeelliptical::fakeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

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