Compare commits

...

346 Commits

Author SHA1 Message Date
Roberto Viola
777ccefc5e 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 19:50:13 +02:00
Roberto Viola
17a4c838b7 Merge branch 'master' into workout-history 2025-07-02 14:37:45 +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
9214ab63ad Update project.pbxproj 2025-07-02 11:58:44 +02:00
Roberto Viola
04f919fe86 Merge branch 'master' into workout-history 2025-07-02 11:44:18 +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
a106438fb0 Update project.pbxproj 2025-07-01 10:50:29 +02:00
Roberto Viola
c336043bb2 Merge branch 'master' into workout-history 2025-07-01 10:39:43 +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
f70f4717f7 fix build 2025-06-30 13:31:49 +02:00
Roberto Viola
f2da15c477 Merge branch 'master' into workout-history 2025-06-30 13:19:59 +02:00
Roberto Viola
e3e15bf24d MacOs closes unexpectedly when treadmill Bluetooth connects (Issue #3518) 2025-06-30 13:19:46 +02:00
Roberto Viola
83690df66e Update qdomyos-zwift-tests.pro 2025-06-30 08:23:19 +02:00
Roberto Viola
cafc785420 Update project.pbxproj 2025-06-30 06:45:16 +02:00
Roberto Viola
0b1e6606cb Update qfit.cpp 2025-06-30 06:44:44 +02:00
Roberto Viola
82d9e252ac Update qfit.cpp 2025-06-30 06:33:38 +02:00
Roberto Viola
b9a7ddcaa0 rogue bike fix 2025-06-29 20:41:24 +02:00
Roberto Viola
1a6ef489d0 Update project.pbxproj 2025-06-29 20:33:23 +02:00
Roberto Viola
d6028c33b6 rogue bike fix 2025-06-29 20:31:09 +02:00
Roberto Viola
66554e0a4a Merge branch 'master' into workout-history 2025-06-29 20:24:26 +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
aa1058f6be optimized! 2025-06-29 19:10:32 +02:00
Roberto Viola
33589f27d3 fixing summary 2025-06-29 13:35:22 +02:00
Roberto Viola
ae1147025e improving 2025-06-29 13:30:39 +02:00
Roberto Viola
189c27eeb0 it kind of works 2025-06-29 12:50:19 +02:00
Roberto Viola
dab74e4bfd claude fixes 2025-06-29 09:23:49 +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
8267661f70 build fix 2025-06-28 14:47:04 +02:00
Roberto Viola
204897680c build fix 2025-06-28 14:46:14 +02:00
Roberto Viola
b93df0d0c9 fix build 2025-06-28 14:41:45 +02:00
Roberto Viola
038c347cc2 fix build 2025-06-28 14:37:21 +02:00
Roberto Viola
4683084ed2 Merge branch 'master' into workout-history 2025-06-28 14:26:23 +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
Roberto Viola
093f5cbe33 Update project.pbxproj 2025-05-21 14:12:58 +02:00
Roberto Viola
b9da86d1ce 3G ELITE treadmill added 2025-05-21 08:39:48 +02:00
Roberto Viola
5ac449a178 PELOTON: skipping intro for warmup and cooldown rides 2025-05-21 08:37:26 +02:00
Roberto Viola
179e60b40b Help with a Expert SX9 #3436 2025-05-19 13:16:01 +02:00
Roberto Viola
5603dc4259 Help with a Expert SX9 #3436 2025-05-19 13:10:26 +02:00
Roberto Viola
b8d703d94f fixing build 2025-05-19 12:26:24 +02:00
Roberto Viola
8ce49beefa Help with a Expert SX9 #3436 2025-05-19 12:25:08 +02:00
Roberto Viola
2437c4c30c Echelon swift plus victory dircon (#3442)
* starting

* builds

* works with the simulator

* Update echelonconnectsport.cpp

* crash fixed

* fixing crash?

* fixing crash!

* Update echelonconnectsport.cpp

* build fix

* starting

* it's working for asking the UUID!

* i'm getting the 0003 but i need to notify the 0002

it doesn't enter into the sendCharacteristicNotification loop

* adding 0004 notifier

* kind of works (no unhandled frames)

* it works!

* wahoo rgt setting is not useful anymore

* dircon works perfectly on ios!

* improving wattage also for all bluetooth, but it's not perfect yet

* Horizon 5.0 Bike Compatibility #3001

* Update characteristicwriteprocessor0003.h

* Update dirconmanager.cpp

* Update fakebike.cpp

* simulating a fake cadence randomly

* handling unhandled case

* Log on Thread

* Update project.pbxproj

* fixing gears on startup alinged with zwift

* Update dirconmanager.cpp

https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2666126808

* Gears don't work for mid-work free ride segment (Issue #2897)

* Update project.pbxproj

* fixing bluetooth on ios with get gears from zwift enabled

* fixing bluetooth with get gears on on android? not tested

* fixing build

* Update project.pbxproj

* Update settings.qml

* Gears don't work for mid-work free ride segment (Issue #2897)

https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2692178928

* Gears don't work for mid-work free ride segment (Issue #2897)

https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2692370530

* Update project.pbxproj

* fixing memory leak

* Update project.pbxproj

* Update project.pbxproj

* build 1043

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* added gears UI from zwift directly if received

* fixing build

* fixing build

* Update project.pbxproj

* fixing zwift gears in the UI of qz

* always enabling 50ms on dircon

* Update project.pbxproj

* wahookickrsnapbike as well

* adding also zwiftclickremote

* fixing crash

* fixing crash

* Update project.pbxproj

* gear alignment between zwift and qz under a new setting

* Update project.pbxproj

* fixing wahoo swift implementation

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update ios_wahookickrsnapbike.mm

* Update project.pbxproj

* adding compensation when there is a power sensor and an ergModeSuppported bike

* Update project.pbxproj

* adding compensation when there is a power sensor and an ergModeSuppported bike

* Update project.pbxproj

* Update project.pbxproj

* Elite Drivio II

* rower distance on apple health?

* Update project.pbxproj

* avoiding crash

* Update project.pbxproj

* Update project.pbxproj

* Bluetooth issues (Issue #3420)

* fixing build #3420

* Update project.pbxproj

* Bluetooth issues (Issue #3420)

* Bluetooth issues #3420

* Revert "Bluetooth issues #3420"

This reverts commit f20f55c0f1.

* Revert "Bluetooth issues (Issue #3420)"

This reverts commit 74c15befaf.

* Revert "fixing build #3420"

This reverts commit 416d10698d.

* Revert "Bluetooth issues (Issue #3420)"

This reverts commit 5cd3efe559.

* merge

* Update echelonconnectsport.h

* Update project.pbxproj

* iOS v2.18 (1061) zwift play controllers disconnecting all the time (Issue #3378)

https://github.com/cagnulein/qdomyos-zwift/issues/3378#issuecomment-2859933867

* Update project.pbxproj

* Bluetooth issues (Issue #3420)

* Update project.pbxproj

* zwift custom characteristic only if get gears from zwift is enabled

https://github.com/cagnulein/qdomyos-zwift/issues/3419#issuecomment-2860215362

* adding max resistance for SCHWINN 190U

* Update qdomyos-zwift.pri

* Update project.pbxproj

* Bluetooth issues (Issue #3420)

* Update project.pbxproj
2025-05-19 11:40:28 +02:00
Roberto Viola
0029258fa5 Echelon Bike ObjectiveC for iOS Crashes (#1898)
* starting

* builds

* works with the simulator

* Update echelonconnectsport.cpp

* crash fixed

* fixing crash?

* fixing crash!

* Update echelonconnectsport.cpp

* build fix

* Update project.pbxproj

* Update project.pbxproj

* wahookickrsnapbike as well

* adding also zwiftclickremote

* fixing wahoo swift implementation

* added wahoo simulator for macos

* Update ios_wahookickrsnapbike.mm

* Bluetooth issues (Issue #3420)

* fixing build #3420

* Bluetooth issues (Issue #3420)

* merge

* Revert "merge"

This reverts commit 3946715856.

* Revert "Bluetooth issues (Issue #3420)"

This reverts commit e98152d9ec.

* Revert "fixing build #3420"

This reverts commit d19982f9df.

* Revert "Bluetooth issues (Issue #3420)"

This reverts commit 67318f1bc2.

* adding ios_btdevice_native

* Update echelonconnectsport.h

* iOS v2.18 (1061) zwift play controllers disconnecting all the time (Issue #3378)

https://github.com/cagnulein/qdomyos-zwift/issues/3378#issuecomment-2859933867

* Bluetooth issues (Issue #3420)

* Bluetooth issues (Issue #3420)
2025-05-19 11:37:25 +02:00
Roberto Viola
8541ec0242 Dircon doesn't work on android 13 and 14 (Issue #3325) (#3332)
* trying a fix

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update .gitmodules

* Update .gitmodules

* Update main.yml

* Revert "Update main.yml"

This reverts commit 034d6f2852.

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update .gitmodules

* Update main.yml
2025-05-16 15:26:12 +02:00
Roberto Viola
0c138d2c07 Help with a Expert SX9 (Issue #3436) 2025-05-16 03:59:45 +00:00
Roberto Viola
4f70b5d160 Help with a Expert SX9 (Issue #3436) 2025-05-16 03:44:04 +00:00
Roberto Viola
e0bcafa804 H9115 Lyon ftms bike 2025-05-15 06:04:29 +02:00
Roberto Viola
330e0b3725 Victory Dircon (#2961)
* starting

* it's working for asking the UUID!

* i'm getting the 0003 but i need to notify the 0002

it doesn't enter into the sendCharacteristicNotification loop

* adding 0004 notifier

* kind of works (no unhandled frames)

* it works!

* wahoo rgt setting is not useful anymore

* dircon works perfectly on ios!

* improving wattage also for all bluetooth, but it's not perfect yet

* Horizon 5.0 Bike Compatibility #3001

* Update characteristicwriteprocessor0003.h

* Update dirconmanager.cpp

* Update fakebike.cpp

* simulating a fake cadence randomly

* handling unhandled case

* Log on Thread

* Update project.pbxproj

* fixing gears on startup alinged with zwift

* Update dirconmanager.cpp

https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2666126808

* Gears don't work for mid-work free ride segment (Issue #2897)

* Update project.pbxproj

* fixing bluetooth on ios with get gears from zwift enabled

* fixing bluetooth with get gears on on android? not tested

* fixing build

* Update project.pbxproj

* Update settings.qml

* Gears don't work for mid-work free ride segment (Issue #2897)

https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2692178928

* Gears don't work for mid-work free ride segment (Issue #2897)

https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2692370530

* Update project.pbxproj

* fixing memory leak

* Update project.pbxproj

* Update project.pbxproj

* build 1043

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* added gears UI from zwift directly if received

* fixing build

* fixing zwift gears in the UI of qz

* always enabling 50ms on dircon

* fixing crash

* gear alignment between zwift and qz under a new setting

* avoiding crash

* zwift custom characteristic only if get gears from zwift is enabled

https://github.com/cagnulein/qdomyos-zwift/issues/3419#issuecomment-2860215362
2025-05-14 16:02:37 +02:00
Roberto Viola
46c12af44d fixing build 2025-05-14 09:56:39 +00:00
Roberto Viola
74db47ba16 Update qdomyos-zwift.pri 2025-05-14 08:11:41 +00:00
Roberto Viola
3bc848cdf4 adding inclinationresistancetable.h 2025-05-14 08:00:45 +00:00
Roberto Viola
a8d58a733e adding max resistance for SCHWINN 190U 2025-05-14 07:59:49 +00:00
Roberto Viola
37c6c03ada adding inclinationResistanceTable for the ftmsbike without an erg mode 2025-05-14 07:32:33 +00:00
Roberto Viola
f258b9e0ae Losing Connection 2025-05-14 06:53:05 +00:00
Roberto Viola
6ec3936bf1 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-05-14 06:39:31 +00:00
Roberto Viola
0f0991f956 peloton: handling delta metrics of only 10 seconds 2025-05-14 06:39:23 +00:00
Roberto Viola
4649744cb4 NordicTrack T6.5S Speed not matching in app and switching treadmill to metric #3417 2025-05-12 09:26:09 +02:00
Roberto Viola
755c1765b4 bowflex integration not quite right #3421 2025-05-12 09:23:19 +02:00
Roberto Viola
6df9fe6acb NordicTrack T6.5S Speed not matching in app and switching treadmill to metric (Issue #3417) 2025-05-09 09:35:01 +02:00
Roberto Viola
32180bf1bd Peloton Resistance not matching Yesoul GS1M #3430 2025-05-08 16:12:06 +02:00
Roberto Viola
a373f41ee0 NordicTrack T6.5S Speed not matching in app and switching treadmill to metric (Issue #3417) 2025-05-08 09:21:18 +02:00
Roberto Viola
bb63e0c3cf Peloton Resistance not matching Yesoul GS1M #3430 2025-05-08 08:41:21 +02:00
Roberto Viola
e84f6d0468 Sportstech sBike Lite 2025-05-08 08:31:39 +02:00
Roberto Viola
b08cf90bdb fixing tests 2025-05-07 10:48:45 +02:00
Roberto Viola
9939056115 Hammer Speedbike S 2025-05-07 08:53:44 +02:00
Roberto Viola
9ea09aca32 Hammer Speedbike S 2025-05-06 08:35:32 +02:00
Roberto Viola
65e15ab29b NordicTrack T6.5S Speed not matching in app and switching treadmill to metric (Issue #3417) 2025-05-05 11:16:10 +02:00
Roberto Viola
1d1e63c40d Nordictrack Elite 800 not accepting speed/incline changes from QZ (Issue #3393) 2025-05-02 13:59:07 +02:00
Roberto Viola
7e854f5bb1 NordicTrack T6.5S Speed not matching in app and switching treadmill to metric #3417 2025-05-02 13:26:09 +02:00
Roberto Viola
fa42eadf43 adding SPERAX_RM-01 2025-05-01 09:35:30 +02:00
Roberto Viola
7ee4f85f67 Mobvoi Treadmill Plus Elapsed Time / Moving Time #3404 2025-04-29 13:15:39 +02:00
Roberto Viola
afa02fab3f Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-04-29 09:41:03 +00:00
Roberto Viola
c10703316c merge 2025-04-29 09:40:57 +00:00
Roberto Viola
93b09d64b0 Core Sensor Support (Issue #3347) 2025-04-29 11:27:10 +02:00
Roberto Viola
12663907d2 merge 2025-04-29 06:07:15 +00:00
Roberto Viola
f72717d440 merge 2025-04-29 06:05:48 +00:00
Roberto Viola
c94174a994 No metrics showing on Apex Rides bike #2459 2025-04-28 14:59:22 +02:00
Roberto Viola
2789c2bf0f No data from my CycleOps Magnus smart trainer and the QZ application. (Issue #3410) 2025-04-28 08:30:06 +02:00
Roberto Viola
0c69226747 Update main.yml 2025-04-27 06:59:04 +02:00
Roberto Viola
9dc65861cc Neo Bike Plus added 2025-04-26 14:24:15 +02:00
Roberto Viola
b14d917f3d Mobvoi Treadmill Plus Elapsed Time / Moving Time (Issue #3404) 2025-04-25 13:56:59 +02:00
Roberto Viola
23fb91b4d2 Update horizontreadmill.cpp 2025-04-25 12:20:16 +02:00
Roberto Viola
110eea144b Mobvoi Treadmill Plus Elapsed Time / Moving Time (Issue #3404) 2025-04-25 12:00:45 +02:00
Roberto Viola
f413068074 Mobvoi Treadmill Plus Elapsed Time / Moving Time #3404 2025-04-24 16:41:11 +02:00
Roberto Viola
917d559ebf fixing CI for linux #2818 2025-04-24 14:00:40 +02:00
Roberto Viola
8d52455574 Sportstech sBike Lite fix 2025-04-24 11:40:42 +02:00
Roberto Viola
3602fa566c Please implement ability for QZ to receive data via ant+ from ant+ bike/ftms (Issue #3257) (#3266)
* i need to add the new bike class

* Update bluetooth.cpp

* Update BikeChannelController.java

* Update BikeChannelController.java

* Update bluetooth.cpp

* class added

* settings aligned

* Update android_antbike.cpp

* Update BikeChannelController.java
2025-04-21 06:46:29 +02:00
Roberto Viola
a53239df97 Powermeter pédale and ERG #2818 (#2880)
* Powermeter pédale and ERG #2818

* Update ergtable_test.h

* Update qdomyos-zwift.pro

* Update main.cpp

* Delete src/ergtable_test.h

* handling TITAN_7000 case

* Update project.pbxproj

* handling TITAN_7000 case

* Update ergtable.h

* Update project.pbxproj
2025-04-21 06:37:37 +02:00
Roberto Viola
6154158254 Elite Drivio II 2025-04-18 17:38:20 +00:00
Roberto Viola
f91b25a177 rower distance on apple health? 2025-04-18 17:38:04 +00:00
octera
650a74dd8c feat: add Proform performance 300i (#3398) 2025-04-18 13:53:11 +02:00
Roberto Viola
d44f57285f Inclination overide not working for Domyos T900C (Issue #3267) (#3270) 2025-04-18 12:37:20 +02:00
Roberto Viola
33b1dcf0f8 Android emulator actions (#3372)
* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Create android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update main.yml

* Update android_test.yml

* Update android_test.yml

* Update android_test.yml

* Update main.yml

* Update android_test.yml

* Update main.yml

* Update main.yml

* Update main.yml

* Delete .github/workflows/android_test.yml
2025-04-17 06:56:34 +02:00
Roberto Viola
e914be7ee0 3G Cardio Pro Runner X cannot change incline nor speed (Issue #3382) 2025-04-17 06:15:09 +02:00
Roberto Viola
b2ea3b0525 3G Cardio Pro Runner X cannot change incline nor speed (Issue #3382) 2025-04-17 05:08:08 +02:00
Roberto Viola
3c5c0518b4 too many KM with BikeERG #3376 2025-04-16 14:26:10 +02:00
Roberto Viola
2b1e11d2e0 [BUG] 3G Cardio Pro Runner X cannot change incline nor speed #3382 2025-04-14 10:37:13 +02:00
Roberto Viola
b3eab44f50 Life Fitness T5 treadmill not connecting to QZ #3381 2025-04-14 10:07:37 +02:00
Roberto Viola
bec8776c63 fixing Align Gear Value on Both Zwift and QZ setting 2025-04-13 07:03:41 +02:00
Roberto Viola
519e39d54b power sensor also for cadence on wahoo bike 2025-04-12 16:32:22 +02:00
Roberto Viola
8e4ab441c0 power sensor also for cadence on wahoo bike 2025-04-12 16:04:12 +02:00
Roberto Viola
6522baccd6 build issue 2025-04-12 15:56:17 +02:00
Roberto Viola
be851955b6 fixing tacx settings 2025-04-12 15:55:08 +02:00
Roberto Viola
4083059155 Life Fitness Treadmill (model 95T with Discover SE console) #3374 2025-04-12 13:18:05 +02:00
Roberto Viola
9ace6dd570 tacxneo2: handling negative inclination with gears due to the flywheel 2025-04-12 11:16:05 +02:00
Roberto Viola
b0677a768d adding setting for the gear alignment between zwift and qz 2025-04-12 06:17:56 +02:00
Roberto Viola
dad99fe6bf [BUG] 3G Cardio Pro Runner X cannot change incline nor speed #3382 2025-04-12 06:03:17 +02:00
Roberto Viola
b5c48bd9f7 Matrix bike not sending data after successful connection #3383 2025-04-12 05:57:50 +02:00
Roberto Viola
8e73dd7a7c Life Fitness T5 treadmill not connecting to QZ (Issue #3381) 2025-04-11 16:16:40 +02:00
Roberto Viola
905c06771b Life Fitness T5 treadmill not connecting to QZ #3381 2025-04-11 09:13:15 +02:00
Roberto Viola
255dbde832 Android Log in QDebug (#3370)
* let's see if it compiles

* fixing cases?

* fixing

* Update Log.java

* fixing

* Update WearableMessageListenerService.java

* fixing

* Update androidqlog.cpp

* restoring patch
2025-04-11 08:30:01 +02:00
Roberto Viola
eaaaee2b4b Life Fitness T5 treadmill not connecting to QZ #3381 2025-04-11 08:25:24 +02:00
Roberto Viola
7227d32c3b 77a361905b (r155155285) 2025-04-11 07:58:31 +02:00
Roberto Viola
988419b7e2 Sessions with negative calories (Issue #3345) 2025-04-09 12:28:11 +02:00
Roberto Viola
1d1401b1d6 Life Fitness Treadmill (model 95T with Discover SE console) #3374 2025-04-09 09:00:39 +02:00
Roberto Viola
282667c6e3 jdpurcell/install-qt-action@v5 (#3373)
* Update main.yml

* Update main.yml
2025-04-09 08:38:03 +02:00
Roberto Viola
fa13ba1d72 Update main.yml (#3371) 2025-04-08 18:12:13 +02:00
Roberto Viola
a2ad67fbac adding restore buttons for wheeldiameter in the wahoo options 2025-04-08 15:33:35 +02:00
Roberto Viola
5e42345889 tacxneo2: handling negative inclination with gears due to the flywheel 2025-04-08 13:26:29 +02:00
Roberto Viola
684ed7d7e9 QZ app with NordicTrack Elliptical FS10i 2025-04-08 10:55:11 +02:00
Roberto Viola
2ff3c1d3b4 QZ app with NordicTrack Elliptical FS10i 2025-04-08 09:41:35 +02:00
Roberto Viola
e0ea3b2fe0 New peloton Login issues #3323 2025-04-08 09:31:01 +02:00
Roberto Viola
35fe2c3fd3 Echelon Sport StairClimber (Issue #3336) 2025-04-07 15:52:42 +02:00
Roberto Viola
0ec6f99429 QZ app with NordicTrack Elliptical FS10i 2025-04-07 15:02:28 +02:00
Roberto Viola
dbe407e784 Update main.yml 2025-04-07 13:05:37 +02:00
Roberto Viola
93f0c714fc fixing build 2025-04-07 12:09:44 +02:00
Roberto Viola
a1b57654c6 Installing on raspberry pi zero w2 from source or download from nightly (Issue #3361) 2025-04-07 11:05:39 +02:00
Roberto Viola
624234dc92 Update project.pbxproj 2025-04-07 10:19:18 +02:00
Roberto Viola
cdda64575a proformwifibike: gpx changes both resistance and inclination 2025-04-07 09:23:54 +02:00
Roberto Viola
483a31d984 adding the fitness equipment in the fit file for the elliptical 2025-04-07 08:56:43 +02:00
Roberto Viola
6d88e7e831 Update settings.qml 2025-04-07 08:54:44 +02:00
Roberto Viola
bb97e49982 Elite Square compatibility (Issue #3354) (#3356)
* Elite Square compatibility (Issue #3354)

* fixing

* fixing

* fixing
2025-04-07 08:52:16 +02:00
Roberto Viola
4f8090e5bf cycplus t2 trainer and cycplus bc2 virtual shift controller. (Issue #3359) 2025-04-04 13:50:40 +02:00
Roberto Viola
8c18d6f179 cycplus t2 trainer and cycplus bc2 virtual shift controller. #3359 2025-04-04 10:02:38 +02:00
Roberto Viola
7d615b4e65 version 2.18.24 2025-04-03 16:32:21 +02:00
Roberto Viola
e264a4c887 Multiple Peloton account with the new login system (#3355)
* trying to fix it. not tested

* Update peloton.cpp

* adding version into the agent

* Update peloton.cpp

* Update peloton.cpp

* Update peloton.cpp

* Update project.pbxproj
2025-04-03 16:14:49 +02:00
Roberto Viola
0991495f51 Echelon Sport StairClimber #3336 2025-04-03 16:06:57 +02:00
Roberto Viola
94c19d70eb QZ App will connect to but not report data for Nautilus E618 Elliptical #2188 2025-04-02 11:44:02 +02:00
Roberto Viola
805639bbfa NordicTrack Elliptical FS10i 2025-04-02 08:45:00 +02:00
Roberto Viola
a900ab939d NordicTrack Elliptical FS10i 2025-04-01 13:20:02 +02:00
Roberto Viola
34881df30a Update project.pbxproj 2025-04-01 09:04:49 +00:00
Roberto Viola
b35995a925 Update project.pbxproj 2025-04-01 09:00:42 +00:00
Roberto Viola
ea9a7170ca NordicTrack Elliptical FS10i 2025-04-01 11:00:10 +02:00
Roberto Viola
4e41a8aaac Revert "NordicTrack Elliptical FS10i"
This reverts commit 8b03718422.
2025-04-01 10:58:40 +02:00
Roberto Viola
b718eb3f4c Not following class callouts (Issue #3350) 2025-04-01 10:01:23 +02:00
Roberto Viola
8b03718422 NordicTrack Elliptical FS10i 2025-03-31 15:13:04 +02:00
Roberto Viola
e0bb6f3c95 Echelon Sport StairClimber #3336 2025-03-31 14:51:11 +02:00
Roberto Viola
4b1649b26a NordicTrack Elliptical FS10i 2025-03-31 13:37:20 +02:00
Roberto Viola
20bd6dbdc4 QZ App will connect to but not report data for Nautilus E618 Elliptical (Issue #2188) 2025-03-31 08:42:31 +02:00
Roberto Viola
2c6088e96c NordicTrack Elliptical FS10i 2025-03-30 16:20:20 +02:00
Roberto Viola
db3961443c QZ App will connect to but not report data for Nautilus E618 Elliptical (Issue #2188) 2025-03-30 15:59:08 +02:00
Roberto Viola
83c3cea88f QZ App will connect to but not report data for Nautilus E618 Elliptical #2188 2025-03-30 08:22:55 +02:00
Roberto Viola
850f680e32 New peloton Login issues (Issue #3323) 2025-03-28 15:59:15 +01:00
Roberto Viola
dcc0b3cbd8 lap distance added to the lap elapsed tile 2025-03-27 09:40:41 +01:00
Roberto Viola
fb372cee89 Using Speed Gain causes treadmill to change speed on inclination change from Zwift (Issue #3334) 2025-03-27 09:30:32 +01:00
Roberto Viola
916af395b6 Proform 225 csx on Mywhoosh, no auto resistance or tile control, manual only (Issue #3230) 2025-03-25 17:19:23 +01:00
Roberto Viola
27b9ef9216 2.18.23 2025-03-25 17:01:15 +01:00
Roberto Viola
c85463b728 REEBOK ftms bike with 32 resistance levels 2025-03-25 11:51:47 +01:00
Roberto Viola
9561cc0269 Update nordictrackifitadbtreadmill.cpp 2025-03-25 11:42:36 +01:00
Roberto Viola
e69c1689ec Dircon doesn't work on android 13 and 14 #3325 2025-03-24 09:51:12 +01:00
Roberto Viola
18497e92fd Skandika Abisko / ERG support #3326 2025-03-24 09:39:50 +01:00
Roberto Viola
f692621565 Proform 225 csx on Mywhoosh, no auto resistance or tile control, manual only #3230 2025-03-24 08:36:41 +01:00
Roberto Viola
934a1089db Add Support for SmO2 NIRS Moxy Sensor (Issue #3250) 2025-03-21 15:13:29 +01:00
Roberto Viola
b0dfeb6e5f Proform 225 csx on Mywhoosh, no auto resistance or tile control, manual only #3230 2025-03-21 14:46:50 +01:00
Roberto Viola
448702b081 Skandika Abisko Support (Issue #3313) 2025-03-21 11:45:27 +01:00
Roberto Viola
8ed9117bee Automatic resistance for Peloton classes not applying correctly for BH Fitness Osaka bike (Issue #3322) 2025-03-21 09:53:14 +01:00
Roberto Viola
439bb30bbd New peloton Login issues (Issue #3323) 2025-03-21 08:34:17 +01:00
Roberto Viola
fc29cd7051 Rower Found but stops getting values (Issue #3320) 2025-03-20 15:10:11 +01:00
Roberto Viola
51aca98536 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-03-20 14:07:57 +01:00
Roberto Viola
2ab3f0b442 Thinkrider #3321 2025-03-20 14:01:36 +01:00
Roberto Viola
cb9105682e Skandika Abisko Support #3313 2025-03-20 13:54:14 +01:00
Roberto Viola
84d46e3aee fixing profile other folders button for all the archs except android
https://github.com/cagnulein/qdomyos-zwift/issues/3251#issuecomment-2737517104
2025-03-20 11:25:10 +01:00
Roberto Viola
9a97dc5221 Rower Found but stops getting values (Issue #3320) 2025-03-20 09:39:07 +01:00
Roberto Viola
ee810b9e0c fixing speed numbers without the comma for nordictrack adb treadmills 2025-03-20 09:34:56 +01:00
Roberto Viola
516a96a4a8 trying to increase strongness of the ios osx bt patch
https://github.com/cagnulein/qdomyos-zwift/issues/3313#issuecomment-2739527022
2025-03-20 09:27:29 +01:00
Roberto Viola
506a9c0896 Skandika Abisko Support #3313 2025-03-20 09:08:49 +01:00
Roberto Viola
71196983ca Update main.yml 2025-03-19 14:58:28 +01:00
Roberto Viola
fc600873b4 Update main.yml 2025-03-19 13:40:08 +01:00
Roberto Viola
49e588e60e Robx e1 bike 2025-03-19 12:55:34 +01:00
Roberto Viola
dc5beebb24 Skandika Abisko Support (Issue #3313) 2025-03-19 09:08:34 +01:00
Roberto Viola
58d4e8b456 Open Resistance does not change when using virtual shifting from Zwift to Kickr Snap #3301 2025-03-18 12:10:16 +01:00
Roberto Viola
ca89072273 Pro-Form, model PFTL99015.0 #3209 2025-03-16 18:53:31 +01:00
Roberto Viola
ff5d8baa1a Data not correctly going to strava (Issue #3298) 2025-03-14 10:59:50 +01:00
Roberto Viola
9ac09db4c3 Update main.yml 2025-03-14 10:14:48 +01:00
Roberto Viola
18e845f99f QT6 on Nigthly (#3293)
* Update main.yml

* Update main.yml
2025-03-14 09:42:16 +01:00
Roberto Viola
e6307dec97 Bcube SP303 2025-03-14 08:16:34 +01:00
Roberto Viola
d2941d94bc Mywhoosh ERG resistance drops (Issue #3300) 2025-03-14 08:05:32 +01:00
Roberto Viola
ba132a0546 removing msvc2022 for the moment 2025-03-11 06:39:57 +01:00
Roberto Viola
d3bbd836f6 Update main.yml 2025-03-09 13:14:49 +01:00
Roberto Viola
8777c2df64 Update main.yml 2025-03-09 10:31:43 +01:00
Roberto Viola
e123c94e8a Body worx JTX3.00 #3288 2025-03-09 10:22:15 +01:00
Roberto Viola
7b3af6ac90 2.18.22 2025-03-08 09:23:47 +01:00
Roberto Viola
ce522a75b5 Update main.yml 2025-03-08 09:19:14 +01:00
Roberto Viola
24720e02f0 QZ app bluetooth keeps disconnecting from bike #3199 2025-03-07 08:55:40 +01:00
Roberto Viola
f34430348a getting QT6 binary from the PR 2025-03-07 03:31:24 +01:00
Roberto Viola
667cdb1520 Proform XBike #3214 2025-03-07 03:02:31 +01:00
Roberto Viola
966bcfadab fixing bootcamp treadmill peloton classes and also fixing void login_onfinish results 2025-03-07 02:54:32 +01:00
Roberto Viola
3c13f211f2 windows 11 builds 2025-03-06 13:34:24 +01:00
Roberto Viola
0033d261d3 adding button on the debug log to show the current folder on macos 2025-03-05 07:57:39 +01:00
Roberto Viola
e1ed32af92 reverting Rouvy: Virtual shifting with Zwift gearing, noticeably harder than physical gearing #3031 (#3268) 2025-03-04 20:11:27 +01:00
Roberto Viola
0d51f4c1d4 Heart Rate Zone Minutes Tracker #3259 2025-03-04 15:35:10 +01:00
Roberto Viola
4c0ffd483e Proform XBike #3214 2025-03-04 10:22:58 +01:00
Roberto Viola
3e4751070f Tunturi F40 Bike #3265 2025-03-03 08:23:21 +01:00
Roberto Viola
4b6611ae2b Don't adjust speed by inclination when using PID (Issue #3254) 2025-02-28 09:13:37 +01:00
Roberto Viola
1a58636cd4 DIRETO XR limited at 600W #3193 2025-02-28 09:06:05 +01:00
Roberto Viola
11dc65bdf1 Fixing raspberry 64bit? 2025-02-28 07:16:02 +01:00
Roberto Viola
387a8f0efe fixing crash build 64bit raspberry? 2025-02-27 14:17:04 +01:00
Roberto Viola
0847f9237f improving crash handling on ios 2025-02-27 12:27:25 +01:00
Roberto Viola
2f5818ba5a Proform 225 csx on Mywhoosh, no auto resistance or tile control, manual only (Issue #3230) 2025-02-26 15:01:44 +01:00
Roberto Viola
d502f983c0 QZ app bluetooth keeps disconnecting from bike (Issue #3199) 2025-02-26 14:15:48 +01:00
Roberto Viola
af79605268 2.18.21 2025-02-26 11:27:54 +01:00
Roberto Viola
0192da5a92 Peloton API Login Issues (Issue #3217) (#3240)
* Peloton API Login Issues (Issue #3217)

* Update peloton.cpp

* fixing variant

* Update peloton.cpp

* improving api stability with the retry mechanism

* Revert "improving api stability with the retry mechanism"

This reverts commit b319f84252.

* Update homeform.cpp

* starting peloton engine after the first login

* check for user id
2025-02-26 11:24:37 +01:00
Roberto Viola
e563a9dc21 Proform 225 csx on Mywhoosh, no auto resistance or tile control, manual only (Issue #3230) 2025-02-26 09:31:15 +01:00
Roberto Viola
8f5d969f61 Yosuda spin bike #3246 2025-02-26 08:06:16 +01:00
Roberto Viola
3a8fb33dd6 Rogue Echo Rower #3241 2025-02-25 11:13:46 +01:00
Roberto Viola
0bc2d74dcc Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-02-24 14:56:57 +01:00
Roberto Viola
13da5056be improving peloton auth dialog 2025-02-24 14:56:50 +01:00
Roberto Viola
ecb2d98ad7 fixing strava secrets on github actions 2025-02-24 14:23:43 +01:00
Roberto Viola
e88811c1fd peloton connect on the wizard 2025-02-24 08:08:34 +01:00
Roberto Viola
56c3ab74cb Peloton API Login Issues (Issue #3217)
https://github.com/cagnulein/qdomyos-zwift/issues/3217#issuecomment-2676048595
2025-02-23 18:05:57 +01:00
Roberto Viola
ae131c7cad adding kcal on the summary 2025-01-16 15:34:58 +01:00
Roberto Viola
67f5446fc2 details start to work! 2025-01-16 15:11:20 +01:00
Roberto Viola
4ce8e1b20c preview of the fit file is almost ready 2025-01-16 14:45:21 +01:00
Roberto Viola
57cdf246a9 removed workoutdetails because db would be too heavy. let's open the fit files 2025-01-16 14:03:19 +01:00
Roberto Viola
4cc0b2520b data fixed 2025-01-16 12:57:03 +01:00
Roberto Viola
8f4364f525 kind of works on ios 2025-01-16 12:47:18 +01:00
Roberto Viola
df0ab76187 the workout history works with the db! 2025-01-16 11:45:35 +01:00
Roberto Viola
b7530dcdff Merge branch 'master' into workout-history 2025-01-16 10:26:35 +01:00
Roberto Viola
12dc37191e Merge branch 'master' into workout-history 2025-01-16 10:07:55 +01:00
Roberto Viola
3b0b7e4bd9 adding fit file processor 2025-01-16 08:47:44 +01:00
Roberto Viola
a3e6640e3f fixing 2024-12-05 16:49:41 +01:00
Roberto Viola
27922da186 Merge branch 'master' into workout-history 2024-12-05 16:42:16 +01:00
Roberto Viola
efb3b74149 connection works 2024-05-07 12:24:25 +02:00
Roberto Viola
c7cc127ea0 emitting signal not tested 2024-05-07 12:00:08 +02:00
Roberto Viola
c1dbaf0f05 let's work on build up the list 2024-05-07 11:27:17 +02:00
Roberto Viola
cbbc054ec8 fix build 2024-05-07 10:39:56 +02:00
Roberto Viola
cb0e7a9389 Merge branch 'master' into workout-history 2024-05-07 10:19:00 +02:00
Roberto Viola
7256cb5ef5 added target cadence, watt and resistance to fit file along with user info 2022-07-19 08:11:29 +02:00
Roberto Viola
f731651c71 build fixed 2022-07-18 14:18:32 +02:00
Roberto Viola
38e9987fff sport type added to preview function 2022-07-18 12:24:33 +02:00
Roberto Viola
28c4084780 using a different template for the preview charts 2022-07-16 15:16:18 +02:00
Roberto Viola
02f4154577 workout history works with a bluetooth device connected 2022-07-15 13:47:06 +02:00
Roberto Viola
6e8ef20efd Merge branch 'master' into workout-history 2022-07-15 11:03:55 +02:00
Roberto Viola
7b01867934 preparing form... 2022-07-14 14:30:33 +02:00
508 changed files with 38899 additions and 5509 deletions

View File

@@ -128,7 +128,7 @@ jobs:
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -305,7 +305,7 @@ jobs:
# qmake
# cd src
# echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
# echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
# echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
# echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
# echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
# echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -510,7 +510,7 @@ jobs:
# This workflow contains a single job called "build"
android-build:
# The type of runner that the job will run on
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
@@ -534,11 +534,21 @@ jobs:
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# This token is provided by Actions, you do not need to create your own token
token: ${{ secrets.GITHUB_TOKEN }}
submodules: recursive # or 'true' if you want to check out only immediate submodules
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
- name: Checkout submodules with specific branches
run: |
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- name: Install packages required to run QZ inside workflow
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
@@ -573,7 +583,7 @@ jobs:
# waiting github.com/jurplel/install-qt-action/issues/63
- name: Install Qt Android
uses: jurplel/install-qt-action@v3
uses: jdpurcell/install-qt-action@v5
with:
version: '5.15.0'
host: 'linux'
@@ -608,7 +618,7 @@ jobs:
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -639,19 +649,92 @@ jobs:
name: fdroid-android-trial
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/
# - name: Exit if not on master branch
# if: github.ref == 'refs/heads/master'
# run: exit 1
android-emulator-test:
runs-on: ubuntu-latest
needs: android-build
steps:
- name: Checkout code
uses: actions/checkout@v2
# - name: upload windows artifact
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ github.token }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk
# asset_name: fdroid-android-trial.zip
# asset_content_type: application/zip
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# Download the APK from the previous job
- name: Download APK Artifact
uses: actions/download-artifact@v4
with:
name: fdroid-android-trial
path: apk-debug
- name: Setup Java for Android Emulator
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
# Use a smaller emulator configuration
- name: Run tests on emulator
uses: ReactiveCircus/android-emulator-runner@v2
with:
target: default # Use default instead of Google APIs
arch: x86
api-level: 29
profile: Nexus 6
disable-animations: true
script: |
# Display available space
df -h
# List available files
echo "Files in apk-debug directory:"
ls -la apk-debug/
# Install the APK
adb install apk-debug/android-debug.apk
# Grant necessary permissions for API 25
echo "Granting permissions..."
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.ACCESS_FINE_LOCATION || true
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH || true
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.BLUETOOTH_ADMIN || true
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.READ_EXTERNAL_STORAGE || true
adb shell pm grant org.cagnulen.qdomyoszwift android.permission.WRITE_EXTERNAL_STORAGE || true
# Start the main activity
adb shell am start -n org.cagnulen.qdomyoszwift/org.qtproject.qt5.android.bindings.QtActivity
# Wait for app to start
sleep 40
# Verify the app is running
echo "Checking if app is running..."
adb shell "ps -A" > process_list.txt
grep -q "qdomyos" process_list.txt || (echo "App process not found in process list" && echo "TEST FAILED: App process not running" && exit 1)
echo "App is running successfully"
# Take a screenshot for verification
adb shell screencap -p /sdcard/screenshot.png
adb pull /sdcard/screenshot.png
# Check if the package is installed
adb shell pm list packages | grep org.cagnulen.qdomyoszwift
# Display logcat output for debugging (just the last 100 lines)
adb logcat -d | grep -i qdomyos | tail -n 100
- name: Upload test evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: android-emulator-test-evidence
path: |
screenshot.png
process_list.txt
if-no-files-found: warn
ios-build:
# The type of runner that the job will run on
@@ -712,7 +795,7 @@ jobs:
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -823,7 +906,7 @@ jobs:
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -854,7 +937,7 @@ jobs:
- name: Install dependencies
run: |
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=D:\a\qdomyos-zwift\vcpkg\installed
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
working-directory: ${{ runner.workspace }}
- name: Build
@@ -1011,7 +1094,7 @@ jobs:
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -1042,7 +1125,7 @@ jobs:
- name: Install dependencies
run: |
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=D:\a\qdomyos-zwift\vcpkg\installed
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
working-directory: ${{ runner.workspace }}
- name: Build
@@ -1106,7 +1189,7 @@ jobs:
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -1154,7 +1237,7 @@ jobs:
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0
image: tonistiigi/binfmt:master
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -1165,7 +1248,7 @@ jobs:
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
@@ -1194,15 +1277,211 @@ jobs:
- name: Rename binary
run: mv src/qdomyos-zwift src/qdomyos-zwift-64bit
- name: Archive Raspberry Pi 64bit binary
uses: actions/upload-artifact@v4
with:
name: raspberry-pi-binary-64bit
path: src/qdomyos-zwift-64bit
window-msvc2022-build:
runs-on: windows-latest
if: github.event_name == 'schedule'
strategy:
matrix:
config:
- {python: true}
- {python: false}
steps:
- name: Checkout PR code
uses: actions/checkout@v2
with:
ref: refs/pull/1508/head
submodules: recursive
- uses: actions/setup-python@v4
with:
python-version: 3.7
- name: download python and paddleocr
run: |
python -VV
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools
python -m pip install "protobuf<=3.20.2,>=3.1.0"
python -m pip install paddlepaddle==2.5.1
python -m pip install paddleocr
python -m pip install imutils
python -m pip install "Pillow<10.0.0"
python -m pip install opencv-python
python -m pip install numpy
python -m pip install pywin32
if: matrix.config.python
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: '6.8.2'
host: 'windows'
modules: 'qtnetworkauth qtcharts qtconnectivity qtspeech qtpositioning qtwebsockets qtlocation qtmultimedia qtwebengine qtwebview qthttpserver qtwebchannel'
target: "desktop"
arch: win64_msvc2022_64
dir: "${{github.workspace}}/qt/"
install-deps: "true"
cache: 'true'
cache-key-prefix: 'install-qt-action-windows'
- name: Install MSVC compiler
uses: ilammy/msvc-dev-cmd@v1
with:
# 14.1 is for vs2017, 14.2 is vs2019, following the upstream vcpkg build from Qv2ray-deps repo
#toolset: 14.2
arch: x64
# - name: download 3rd party files for qthttpserver
# run: |
# cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
# - name: Build qthttpserver
# run: |
# cd src\qthttpserver
# qmake
# nmake
# nmake install
# cd ../..
- name: Secrets
if: github.ref == 'refs/heads/master'
run: |
cd src
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
- name: Clone vcpkg
run: git clone https://github.com/microsoft/vcpkg.git
working-directory: ${{ runner.workspace }}
- name: Bootstrap vcpkg
run: .\vcpkg\bootstrap-vcpkg.bat
working-directory: ${{ runner.workspace }}
- name: Create vcpkg.json
working-directory: ${{ runner.workspace }}
run: |
echo '{
"name": "qdomyos-zwift",
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"dependencies": [
"protobuf",
"protobuf-c",
"abseil"
],
"builtin-baseline": "8c2fcacefba009d63672f9d137f192765e632c9f"
}' > vcpkg.json
- name: Install dependencies
run: |
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=${{ runner.workspace }}\vcpkg\installed
working-directory: ${{ runner.workspace }}
- name: Build
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
run: |
qmake
nmake
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp ../../../icons/iOS/iTunesArtwork@2x.png .
cp ../../AppxManifest.xml .
cp ../../windows/*.py .
cp ../../windows/*.bat .
cp ../../../windows_openssl/*.* .
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\bin\*.* -Destination . -Verbose
mkdir adb
mkdir python
Copy-Item -Path C:\hostedtoolcache\windows\Python\3.7.9\x64 -Destination python -Recurse
cp ../../adb/* adb/
cd ..
cd appx
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
if: matrix.config.python
- name: Build without python
# Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination . -Verbose
# Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\lib\*.* -Destination src/ -Verbose
# Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\include\* -Destination src/ -Recurse -Verbose
run: |
qmake
nmake
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp "C:/mingw64/bin/libwinpthread-1.dll" .
cp "C:/mingw64/bin/libgcc_s_seh-1.dll" .
cp "C:/mingw64/bin/libstdc++-6.dll" .
cp ../../../icons/iOS/iTunesArtwork@2x.png .
cp ../../AppxManifest.xml .
cp ../../../windows_openssl/*.* .
Copy-Item -Path ${{ runner.workspace }}\vcpkg\installed\x64-windows\bin\*.* -Destination . -Verbose
mkdir adb
cp ../../adb/* adb/
cd ..
cd appx
#../../MSIX-Toolkit/WindowsSDK/10/10.0.20348.0/x64/makeappx.exe pack /d ../output/ /p qz
if: matrix.config.python == false
- name: patching qt for bluetooth
run: cp qt-patches/windows/5.15.2/binary/msvc2019/*.* ${{ github.workspace }}/src/debug/output/
- name: Zip artifact for deployment
run: Compress-Archive src/debug/output windows-msvc2022-binary.zip
if: matrix.config.python
- name: Zip artifact for deployment
run: Compress-Archive src/debug/output windows-msvc2022-binary-no-python.zip
if: ${{ ! matrix.config.python }}
- name: Archive windows binary
uses: actions/upload-artifact@v4
with:
name: windows-msvc2022-binary
path: windows-msvc2022-binary.zip
if: matrix.config.python
- name: Archive windows binary
uses: actions/upload-artifact@v4
with:
name: windows-msvc2022-binary-no-python
path: windows-msvc2022-binary-no-python.zip
if: ${{ ! matrix.config.python }}
upload_to_release:
permissions: write-all
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
if: github.event_name == 'schedule'
needs: [linux-x86-build, window-msvc2019-build, ios-build, window-build, android-build, raspberry-pi-build] # Specify the job dependencies
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Update nightly release
uses: andelf/nightly-release@main
env:
@@ -1220,14 +1499,24 @@ jobs:
of new complex code, the stability of the program may suffer compared to
official releases, so **use it with caution**!
## Windows Builds:
- **windows-msvc2019**: Recommended for Windows 10
- **windows-msvc2022**: Recommended for Windows 11 (experimental, QT6)
- **windows**: MinGW build (alternative version)
## Other Platforms:
- **fdroid-android-trial**: Android build
- **raspberry-pi-binary**: Raspberry Pi build
__Please help us improve QZ by reporting any issues you encounter!__ :wink:
files: |
windows-msvc2019-binary-no-python/*
windows-msvc2019-binary/*
windows-msvc2022-binary-no-python/*
windows-msvc2022-binary/*
windows-msvc2019-ai-server-binary/*
windows-binary-no-python/*
windows-binary/*
fdroid-android-trial/*
raspberry-pi-binary/qdomyos-zwift-32bit
#raspberry-pi-64bit-binary/qdomyos-zwift-64bit
#2024-10-22-raspios-bookworm-arm64-lite.img.xz
raspberry-pi-binary/qdomyos-zwift-64bit

1
.gitignore vendored
View File

@@ -50,3 +50,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

2
.gitmodules vendored
View File

@@ -12,7 +12,7 @@
[submodule "tst/googletest"]
path = tst/googletest
url = https://github.com/google/googletest.git
branch = tags/release-1.12.1
tag = release-1.12.1
[submodule "src/qthttpserver"]
path = src/qthttpserver
url = https://github.com/qt-labs/qthttpserver

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

@@ -299,6 +299,10 @@
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8762D5112601F89500F6F049 /* scanrecordresult.cpp */; };
87646C2027B5064600F82131 /* bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C1E27B5064500F82131 /* bhfitnesselliptical.cpp */; };
87646C2227B5065100F82131 /* moc_bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */; };
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */; };
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */; };
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */; };
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */; };
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */; };
8768C8BA2BBC11C80099DBE1 /* file_sync_client.c in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8768C89C2BBC11C70099DBE1 /* file_sync_client.c */; };
8768C8BB2BBC11C80099DBE1 /* protocol.txt in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 8768C89D2BBC11C70099DBE1 /* protocol.txt */; };
@@ -392,8 +396,11 @@
8785D5442B3DD105005A2EB7 /* moc_zwift_client_auth.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */; };
87873AEE2D09A8AA005F86B4 /* moc_sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */; };
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */; };
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 */; };
@@ -472,6 +479,7 @@
87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BE6FDD272D2A3E00C35795 /* moc_horizongr7bike.cpp */; };
87BF116D298E28CA00B5B6E7 /* pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */; };
87BF116F298E28EC00B5B6E7 /* moc_pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */; };
87BFEA2F2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */; };
87C424262BC1294000503687 /* moc_treadmillErgTable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */; };
87C481FA26DFA7C3006211AD /* eliterizer.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C481F926DFA7C3006211AD /* eliterizer.cpp */; };
87C481FC26DFA7D1006211AD /* moc_eliterizer.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C481FB26DFA7D1006211AD /* moc_eliterizer.cpp */; };
@@ -523,6 +531,12 @@
87D5DC4228230496008CCDE7 /* moc_truetreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D5DC4128230496008CCDE7 /* moc_truetreadmill.cpp */; };
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F992800B9970026D43C /* proformwifibike.cpp */; };
87D91F9C2800B9B90026D43C /* moc_proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F9B2800B9B90026D43C /* moc_proformwifibike.cpp */; };
87DA62A42D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */; };
87DA62A52D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */; };
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */; };
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */; };
87DA62AF2D2426F2008ADA0F /* characteristicnotifier0004.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */; };
87DA62B02D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */; };
87DA76502848F98200A71B64 /* libQt5TextToSpeech.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87DA764F2848F8F200A71B64 /* libQt5TextToSpeech.a */; };
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA8464284933D200B550E9 /* fakeelliptical.cpp */; };
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA8466284933DE00B550E9 /* moc_fakeelliptical.cpp */; };
@@ -532,6 +546,12 @@
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */; };
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */; };
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */; };
87DED80627D1273900BE4FBB /* filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80427D1273800BE4FBB /* filedownloader.cpp */; };
87DED80827D1274600BE4FBB /* moc_filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */; };
87DF68B825E2673B00FCDA46 /* eslinkertreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DF68B625E2673600FCDA46 /* eslinkertreadmill.cpp */; };
@@ -560,6 +580,12 @@
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
@@ -567,6 +593,10 @@
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */; };
87F1179E26A5FBDE00541B3A /* libqtwebview_darwin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87420DF7269D7CE1000C5EC6 /* libqtwebview_darwin.a */; };
87F1BD682DBFBCE700416506 /* moc_android_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */; };
87F1BD692DBFBCE700416506 /* android_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD662DBFBCE700416506 /* android_antbike.cpp */; };
87F1BD712DC0D59600416506 /* moc_coresensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD702DC0D59600416506 /* moc_coresensor.cpp */; };
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD6F2DC0D59600416506 /* coresensor.cpp */; };
87F4FB5A29D550C00061BB4A /* schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */; };
87F4FB5C29D550E00061BB4A /* moc_schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */; };
87F527BE28EEB5AA00A9F8D5 /* qzsettings.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F527BC28EEB5AA00A9F8D5 /* qzsettings.cpp */; };
@@ -1199,6 +1229,13 @@
87646C1E27B5064500F82131 /* bhfitnesselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bhfitnesselliptical.cpp; path = ../src/devices/bhfitnesselliptical/bhfitnesselliptical.cpp; sourceTree = "<group>"; };
87646C1F27B5064500F82131 /* bhfitnesselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bhfitnesselliptical.h; path = ../src/devices/bhfitnesselliptical/bhfitnesselliptical.h; sourceTree = "<group>"; };
87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bhfitnesselliptical.cpp; sourceTree = "<group>"; };
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = elitesquarecontroller.h; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.h; sourceTree = SOURCE_ROOT; };
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = elitesquarecontroller.cpp; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.cpp; sourceTree = SOURCE_ROOT; };
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elitesquarecontroller.cpp; sourceTree = "<group>"; };
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_wahookickrsnapbike.h; path = ../src/ios/ios_wahookickrsnapbike.h; sourceTree = SOURCE_ROOT; };
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_wahookickrsnapbike.mm; path = ../src/ios/ios_wahookickrsnapbike.mm; sourceTree = SOURCE_ROOT; };
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_zwiftclickremote.h; path = ../src/ios/ios_zwiftclickremote.h; sourceTree = SOURCE_ROOT; };
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_zwiftclickremote.mm; path = ../src/ios/ios_zwiftclickremote.mm; sourceTree = SOURCE_ROOT; };
8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor.cpp; path = ../src/characteristics/characteristicwriteprocessor.cpp; sourceTree = "<group>"; };
8768C89C2BBC11C70099DBE1 /* file_sync_client.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = file_sync_client.c; path = ../src/ios/adb/adb/file_sync_client.c; sourceTree = "<group>"; };
8768C89D2BBC11C70099DBE1 /* protocol.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = protocol.txt; path = ../src/ios/adb/adb/protocol.txt; sourceTree = "<group>"; };
@@ -1347,10 +1384,15 @@
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusrower.cpp; sourceTree = "<group>"; };
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sportsplusrower.cpp; sourceTree = "<group>"; };
87873AF22D09AADF005F86B4 /* sportsplusrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportsplusrower.h; path = ../src/devices/sportsplusrower/sportsplusrower.h; sourceTree = SOURCE_ROOT; };
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = inclinationresistancetable.h; path = ../src/inclinationresistancetable.h; sourceTree = SOURCE_ROOT; };
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inclinationresistancetable.cpp; sourceTree = "<group>"; };
8789DCDB6A4F681A76DF3F92 /* Qt5Widgets */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = Qt5Widgets; path = "/Users/cagnulein/Qt/5.15.2/ios/lib/libQt5Widgets$(QT_LIBRARY_SUFFIX).a"; sourceTree = "<absolute>"; };
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>"; };
@@ -1469,6 +1511,8 @@
87BF116B298E28CA00B5B6E7 /* pelotonbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = pelotonbike.h; path = ../src/devices/pelotonbike/pelotonbike.h; sourceTree = "<group>"; };
87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = pelotonbike.cpp; path = ../src/devices/pelotonbike/pelotonbike.cpp; sourceTree = "<group>"; };
87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_pelotonbike.cpp; sourceTree = "<group>"; };
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_echelonconnectsport.h; path = ../src/ios/ios_echelonconnectsport.h; sourceTree = SOURCE_ROOT; };
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_echelonconnectsport.mm; path = ../src/ios/ios_echelonconnectsport.mm; sourceTree = SOURCE_ROOT; };
87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_treadmillErgTable.cpp; sourceTree = "<group>"; };
87C481F826DFA7C3006211AD /* eliterizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = eliterizer.h; path = ../src/devices/eliterizer/eliterizer.h; sourceTree = "<group>"; };
87C481F926DFA7C3006211AD /* eliterizer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = eliterizer.cpp; path = ../src/devices/eliterizer/eliterizer.cpp; sourceTree = "<group>"; };
@@ -1546,6 +1590,12 @@
87D91F982800B9970026D43C /* proformwifibike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformwifibike.h; path = ../src/devices/proformwifibike/proformwifibike.h; sourceTree = "<group>"; };
87D91F992800B9970026D43C /* proformwifibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformwifibike.cpp; path = ../src/devices/proformwifibike/proformwifibike.cpp; sourceTree = "<group>"; };
87D91F9B2800B9B90026D43C /* moc_proformwifibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformwifibike.cpp; sourceTree = "<group>"; };
87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicnotifier0002.cpp; sourceTree = "<group>"; };
87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicwriteprocessor0003.cpp; sourceTree = "<group>"; };
87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier0002.cpp; path = ../src/characteristics/characteristicnotifier0002.cpp; sourceTree = SOURCE_ROOT; };
87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor0003.cpp; path = ../src/characteristics/characteristicwriteprocessor0003.cpp; sourceTree = SOURCE_ROOT; };
87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier0004.cpp; path = ../src/characteristics/characteristicnotifier0004.cpp; sourceTree = SOURCE_ROOT; };
87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicnotifier0004.cpp; sourceTree = "<group>"; };
87DA764F2848F8F200A71B64 /* libQt5TextToSpeech.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5TextToSpeech.a; path = ../../Qt/5.15.2/ios/lib/libQt5TextToSpeech.a; sourceTree = "<group>"; };
87DA8463284933D200B550E9 /* fakeelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fakeelliptical.h; path = ../src/devices/fakeelliptical/fakeelliptical.h; sourceTree = "<group>"; };
87DA8464284933D200B550E9 /* fakeelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakeelliptical.cpp; path = ../src/devices/fakeelliptical/fakeelliptical.cpp; sourceTree = "<group>"; };
@@ -1559,6 +1609,15 @@
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = stairclimber.cpp; path = ../src/devices/stairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_echelonstairclimber.cpp; sourceTree = "<group>"; };
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_stairclimber.cpp; sourceTree = "<group>"; };
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_moxy5sensor.cpp; sourceTree = "<group>"; };
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = moxy5sensor.h; path = ../src/devices/moxy5sensor/moxy5sensor.h; sourceTree = SOURCE_ROOT; };
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = moxy5sensor.cpp; path = ../src/devices/moxy5sensor/moxy5sensor.cpp; sourceTree = SOURCE_ROOT; };
87DED80427D1273800BE4FBB /* filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = filedownloader.cpp; path = ../src/filedownloader.cpp; sourceTree = "<group>"; };
87DED80527D1273900BE4FBB /* filedownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filedownloader.h; path = ../src/filedownloader.h; sourceTree = "<group>"; };
87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_filedownloader.cpp; sourceTree = "<group>"; };
@@ -1600,6 +1659,15 @@
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
@@ -1609,6 +1677,12 @@
87F02E3E29178523000DB52C /* octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = octaneelliptical.cpp; path = ../src/devices/octaneelliptical/octaneelliptical.cpp; sourceTree = "<group>"; };
87F02E3F29178524000DB52C /* octaneelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = octaneelliptical.h; path = ../src/devices/octaneelliptical/octaneelliptical.h; sourceTree = "<group>"; };
87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_octaneelliptical.cpp; sourceTree = "<group>"; };
87F1BD652DBFBCE700416506 /* android_antbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = android_antbike.h; path = ../src/devices/android_antbike/android_antbike.h; sourceTree = SOURCE_ROOT; };
87F1BD662DBFBCE700416506 /* android_antbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = android_antbike.cpp; path = ../src/devices/android_antbike/android_antbike.cpp; sourceTree = SOURCE_ROOT; };
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_android_antbike.cpp; sourceTree = "<group>"; };
87F1BD6E2DC0D59600416506 /* coresensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = coresensor.h; path = ../src/devices/coresensor/coresensor.h; sourceTree = SOURCE_ROOT; };
87F1BD6F2DC0D59600416506 /* coresensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = coresensor.cpp; path = ../src/devices/coresensor/coresensor.cpp; sourceTree = SOURCE_ROOT; };
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_coresensor.cpp; sourceTree = "<group>"; };
87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = schwinn170bike.cpp; path = ../src/devices/schwinn170bike/schwinn170bike.cpp; sourceTree = "<group>"; };
87F4FB5929D550BF0061BB4A /* schwinn170bike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = schwinn170bike.h; path = ../src/devices/schwinn170bike/schwinn170bike.h; sourceTree = "<group>"; };
87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_schwinn170bike.cpp; sourceTree = "<group>"; };
@@ -2007,6 +2081,8 @@
25B08E2869634E9BCBA333A2 /* Generated Sources */ = {
isa = PBXGroup;
children = (
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */,
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */,
87C4E5BC2C1C1D2600D0750E /* moc_crossrope.cpp */,
87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */,
874823FD2B935ADA006F3CA1 /* moc_ergtable.cpp */,
@@ -2179,6 +2255,35 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
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 */,
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */,
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */,
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */,
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */,
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */,
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */,
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */,
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */,
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */,
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */,
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */,
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */,
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */,
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */,
8798FDC02D66075B00CF8EE8 /* osxbluetooth_p.h */,
8798FDC12D66075B00CF8EE8 /* osxbtcentralmanager_p.h */,
8798FDC22D66075B00CF8EE8 /* osxbtgcdtimer_p.h */,
@@ -2674,6 +2779,13 @@
87FE06822D170D5600CDAAF6 /* trxappgateusbrower.h */,
87FE06832D170D5600CDAAF6 /* trxappgateusbrower.cpp */,
87FE06802D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp */,
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */,
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */,
87DC27E82D9BDB53007A1B9D /* stairclimber.h */,
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */,
87F1BD652DBFBCE700416506 /* android_antbike.h */,
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
);
name = Sources;
sourceTree = "<group>";
@@ -2827,6 +2939,19 @@
name = AppleWatchToIpad;
sourceTree = "<group>";
};
8798FDCA2D6F338200CF8EE8 /* Recovered References */ = {
isa = PBXGroup;
children = (
87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */,
87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */,
87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */,
87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */,
87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */,
87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */,
);
name = "Recovered References";
sourceTree = "<group>";
};
87DF60DE337FB58864343E39 /* Resources */ = {
isa = PBXGroup;
children = (
@@ -3245,6 +3370,7 @@
AF39DD055C3EF8226FBE929D /* Frameworks */,
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
FE0A091FDBFB3E9C31B7A1BD /* Products */,
8798FDCA2D6F338200CF8EE8 /* Recovered References */,
);
name = qdomyoszwift;
sourceTree = "<group>";
@@ -3441,6 +3567,8 @@
873824C027E64707004F1B46 /* moc_dirconmanager.cpp in Compile Sources */,
2F0E70F826316F3600E11F3A /* virtualbike_zwift.swift in Compile Sources */,
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */,
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */,
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */,
87062646259480B200D06586 /* ViewController.swift in Compile Sources */,
87D269A425F535340076AA48 /* moc_m3ibike.cpp in Compile Sources */,
87BAFE482B8CA7AA00065FCD /* moc_focustreadmill.cpp in Compile Sources */,
@@ -3461,6 +3589,8 @@
873824BC27E64707004F1B46 /* moc_resolver.cpp in Compile Sources */,
87FA11AD27C5ECE4008AC5D1 /* moc_ultrasportbike.cpp in Compile Sources */,
871235BF26B297670012D0F2 /* kingsmithr1protreadmill.cpp in Compile Sources */,
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */,
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */,
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */,
20A50533946A39CBD2C89104 /* bluetoothdevice.cpp in Compile Sources */,
87C5F0D126285E7E0067A1B5 /* moc_stagesbike.cpp in Compile Sources */,
@@ -3497,9 +3627,12 @@
87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */,
87E2F85D291ED308002BDC65 /* lifefitnesstreadmill.cpp in Compile Sources */,
8752C0E82B15D85600C3D1A5 /* eliteariafan.cpp in Compile Sources */,
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */,
87917A7328E768D200F8D9AC /* Browser.swift in Compile Sources */,
873CD20B27EF8D8A000131BC /* inapptransaction.cpp in Compile Sources */,
8727C7D52B3BF1E4005429EB /* moc_proformtelnetbike.cpp in Compile Sources */,
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */,
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */,
873824EF27E647A9004F1B46 /* query.cpp in Compile Sources */,
876F45FF279350D9003CDA5A /* moc_concept2skierg.cpp in Compile Sources */,
BE93C6EF2C2A6BFEEC9EA565 /* fit_buffered_mesg_broadcaster.cpp in Compile Sources */,
@@ -3507,6 +3640,7 @@
87DAE16426E9FF3A00B0527E /* kingsmithr2treadmill.cpp in Compile Sources */,
87F93427278E0EC00088B596 /* domyosrower.cpp in Compile Sources */,
87B617EE25F25FED0094A1CB /* snodebike.cpp in Compile Sources */,
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */,
87C5F0B526285E5F0067A1B5 /* mimemessage.cpp in Compile Sources */,
8768C8C92BBC11C80099DBE1 /* adb_client.c in Compile Sources */,
873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */,
@@ -3576,6 +3710,8 @@
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */,
87CC3BA425A0885F001EC5A8 /* elliptical.cpp in Compile Sources */,
4AD2C93A2B8FD5855E521630 /* fit_encode.cpp in Compile Sources */,
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
@@ -3604,6 +3740,7 @@
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */,
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */,
876E50F52B701C050080FAAF /* moc_zwiftclickremote.cpp in Compile Sources */,
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */,
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
@@ -3631,6 +3768,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 */,
@@ -3690,6 +3829,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 */,
@@ -3707,6 +3852,7 @@
873824BB27E64707004F1B46 /* moc_prober_p.cpp in Compile Sources */,
877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */,
8742C2B227C92C30007D3FA0 /* wahookickrsnapbike.cpp in Compile Sources */,
87BFEA2F2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm in Compile Sources */,
87EB918327EE5FE7002535E1 /* moc_inappstore.cpp in Compile Sources */,
87CF516B293C87B000A7CABC /* moc_characteristicwriteprocessore005.cpp in Compile Sources */,
8780D949264FB8B800192D41 /* moc_smartspin2k.cpp in Compile Sources */,
@@ -3804,7 +3950,11 @@
1FBBC7C86C436CAAAFD37E56 /* moc_domyostreadmill.cpp in Compile Sources */,
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */,
873824BD27E64707004F1B46 /* moc_resolver_p.cpp in Compile Sources */,
87DA62A42D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp in Compile Sources */,
87DA62A52D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp in Compile Sources */,
876ED21A25C3E9010065F3DC /* moc_material.cpp in Compile Sources */,
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */,
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */,
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */,
87D10552290996EA00B3935B /* mepanelbike.cpp in Compile Sources */,
9D9484EED654597C394345DE /* moc_echelonconnectsport.cpp in Compile Sources */,
@@ -3824,6 +3974,8 @@
87A33F1A2D611D8400BFFF29 /* moc_logwriter.cpp in Compile Sources */,
87D105542909971100B3935B /* moc_mepanelbike.cpp in Compile Sources */,
87B617F325F260150094A1CB /* moc_snodebike.cpp in Compile Sources */,
87F1BD712DC0D59600416506 /* moc_coresensor.cpp in Compile Sources */,
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
@@ -3845,6 +3997,8 @@
873824B227E64706004F1B46 /* moc_hostname.cpp in Compile Sources */,
873824EB27E647A8004F1B46 /* prober.cpp in Compile Sources */,
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */,
87DA62AF2D2426F2008ADA0F /* characteristicnotifier0004.cpp in Compile Sources */,
87DA62B02D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp in Compile Sources */,
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */,
87B617F425F260150094A1CB /* moc_screencapture.cpp in Compile Sources */,
87B617ED25F25FED0094A1CB /* fitshowtreadmill.cpp in Compile Sources */,
@@ -3865,6 +4019,8 @@
87A0771029B641D600A368BF /* wahookickrheadwind.cpp in Compile Sources */,
87EAC3D32D1D8D1D004FE975 /* moc_pitpatbike.cpp in Compile Sources */,
8791A8AB25C861BD003B50B2 /* inspirebike.cpp in Compile Sources */,
87F1BD682DBFBCE700416506 /* moc_android_antbike.cpp in Compile Sources */,
87F1BD692DBFBCE700416506 /* android_antbike.cpp in Compile Sources */,
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */,
871235C126B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp in Compile Sources */,
87061397286D8CFE00D2446E /* PathController.cpp in Compile Sources */,
@@ -4255,7 +4411,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1035;
CURRENT_PROJECT_VERSION = 1115;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4291,6 +4447,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,
@@ -4337,7 +4494,7 @@
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.19;
OTHER_CFLAGS = (
"-pipe",
"-g",
@@ -4449,7 +4606,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1035;
CURRENT_PROJECT_VERSION = 1115;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4487,6 +4644,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,
@@ -4533,7 +4691,7 @@
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.19;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"-pipe",
@@ -4679,7 +4837,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1035;
CURRENT_PROJECT_VERSION = 1115;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4704,7 +4862,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.19;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4775,7 +4933,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1035;
CURRENT_PROJECT_VERSION = 1115;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -4796,7 +4954,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.19;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4867,7 +5025,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1035;
CURRENT_PROJECT_VERSION = 1115;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -4912,7 +5070,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.19;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4983,7 +5141,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1035;
CURRENT_PROJECT_VERSION = 1115;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5024,7 +5182,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.18;
MARKETING_VERSION = 2.19;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";

View File

@@ -54,20 +54,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):
@@ -227,9 +233,11 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: workoutSession.startDate!,
start: startDate,
end: Date())
workoutBuilder.add([sample]) {(success, error) in}
@@ -249,7 +257,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: workoutSession.startDate!,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -268,6 +276,67 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
}
}
}
} else if(sport == 4) { // Rowing
// Guard to check if steps quantity type is available
guard let quantityTypeSteps = HKQuantityType.quantityType(
forIdentifier: .stepCount) else {
return
}
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
// Create a sample for total steps
let sampleSteps = HKCumulativeQuantitySeriesSample(
type: quantityTypeSteps,
quantity: stepsQuantity,
start: startDate,
end: Date())
// Add the steps sample to workout builder
workoutBuilder.add([sampleSteps]) { (success, error) in
if let error = error {
print(error)
}
}
// Per il rowing, HealthKit utilizza un tipo specifico di distanza
// Se non esiste un tipo specifico per il rowing, possiamo usare un tipo generico di distanza
var quantityTypeDistance: HKQuantityType?
// In watchOS 10 e versioni successive, possiamo usare un tipo specifico se disponibile
if #available(watchOSApplicationExtension 10.0, *) {
// Verifica se esiste un tipo specifico per il rowing, altrimenti utilizza un tipo generico
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)
} else {
// Nelle versioni precedenti, usa il tipo generico
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)
}
guard let typeDistance = quantityTypeDistance else {
return
}
let sampleDistance = HKCumulativeQuantitySeriesSample(type: typeDistance,
quantity: quantityMiles,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
if let error = error {
print(error)
}
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
if let error = error {
print(error)
}
self.workoutBuilder.finishWorkout{ (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
}
}
}
} else {
// Guard to check if steps quantity type is available
@@ -282,7 +351,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
@@ -314,7 +383,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: workoutSession.startDate!,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in

188
helpers/dircon-parser.py Normal file
View File

@@ -0,0 +1,188 @@
from dataclasses import dataclass
from typing import List, Optional, Tuple
import re
@dataclass
class HubRidingData:
power: Optional[int] = None
cadence: Optional[int] = None
speed_x100: Optional[int] = None
hr: Optional[int] = None
unknown1: Optional[int] = None
unknown2: Optional[int] = None
def __str__(self):
return (f"Power={self.power}W Cadence={self.cadence}rpm "
f"Speed={self.speed_x100/100 if self.speed_x100 else 0:.1f}km/h "
f"HR={self.hr}bpm Unknown1={self.unknown1} Unknown2={self.unknown2}")
def parse_protobuf_varint(data: bytes, offset: int = 0) -> Tuple[int, int]:
result = 0
shift = 0
while offset < len(data):
byte = data[offset]
result |= (byte & 0x7F) << shift
offset += 1
if not (byte & 0x80):
break
shift += 7
return result, offset
def parse_hub_riding_data(data: bytes) -> Optional[HubRidingData]:
try:
riding_data = HubRidingData()
offset = 0
while offset < len(data):
key, new_offset = parse_protobuf_varint(data, offset)
wire_type = key & 0x07
field_number = key >> 3
offset = new_offset
if wire_type == 0:
value, offset = parse_protobuf_varint(data, offset)
if field_number == 1:
riding_data.power = value
elif field_number == 2:
riding_data.cadence = value
elif field_number == 3:
riding_data.speed_x100 = value
elif field_number == 4:
riding_data.hr = value
elif field_number == 5:
riding_data.unknown1 = value
elif field_number == 6:
riding_data.unknown2 = value
return riding_data
except Exception as e:
print(f"Error parsing protobuf: {e}")
return None
@dataclass
class DirconPacket:
message_version: int = 1
identifier: int = 0xFF
sequence_number: int = 0
response_code: int = 0
length: int = 0
uuid: int = 0
uuids: List[int] = None
additional_data: bytes = b''
is_request: bool = False
riding_data: Optional[HubRidingData] = None
def __str__(self):
uuids_str = ','.join(f'{u:04x}' for u in (self.uuids or []))
base_str = (f"vers={self.message_version} Id={self.identifier} sn={self.sequence_number} "
f"resp={self.response_code} len={self.length} req?={self.is_request} "
f"uuid={self.uuid:04x} dat={self.additional_data.hex()} uuids=[{uuids_str}]")
if self.riding_data:
base_str += f"\nRiding Data: {self.riding_data}"
return base_str
def parse_dircon_packet(data: bytes, offset: int = 0) -> Tuple[Optional[DirconPacket], int]:
if len(data) - offset < 6:
return None, 0
packet = DirconPacket()
packet.message_version = data[offset]
packet.identifier = data[offset + 1]
packet.sequence_number = data[offset + 2]
packet.response_code = data[offset + 3]
packet.length = (data[offset + 4] << 8) | data[offset + 5]
total_length = 6 + packet.length
if len(data) - offset < total_length:
return None, 0
curr_offset = offset + 6
if packet.identifier == 0x01: # DPKT_MSGID_DISCOVER_SERVICES
if packet.length == 0:
packet.is_request = True
elif packet.length % 16 == 0:
packet.uuids = []
while curr_offset + 16 <= offset + total_length:
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
packet.uuids.append(uuid)
curr_offset += 16
elif packet.identifier == 0x02: # DPKT_MSGID_DISCOVER_CHARACTERISTICS
if packet.length >= 16:
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
if packet.length == 16:
packet.is_request = True
elif (packet.length - 16) % 17 == 0:
curr_offset += 16
packet.uuids = []
packet.additional_data = b''
while curr_offset + 17 <= offset + total_length:
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
packet.uuids.append(uuid)
packet.additional_data += bytes([data[curr_offset + 16]])
curr_offset += 17
elif packet.identifier in [0x03, 0x04, 0x05, 0x06]: # READ/WRITE/NOTIFY characteristics
if packet.length >= 16:
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
if packet.length > 16:
packet.additional_data = data[curr_offset + 16:offset + total_length]
if packet.uuid == 0x0002:
packet.riding_data = parse_hub_riding_data(packet.additional_data)
if packet.identifier != 0x06:
packet.is_request = True
return packet, total_length
def extract_bytes_from_c_array(content: str) -> List[Tuple[str, bytes]]:
packets = []
pattern = r'static const unsigned char (\w+)\[\d+\] = \{([^}]+)\};'
matches = re.finditer(pattern, content)
for match in matches:
name = match.group(1)
hex_str = match.group(2)
hex_values = []
for line in hex_str.split('\n'):
line = line.split('//')[0]
values = re.findall(r'0x[0-9a-fA-F]{2}', line)
hex_values.extend(values)
byte_data = bytes([int(x, 16) for x in hex_values])
packets.append((name, byte_data))
return packets
def get_tcp_payload(data: bytes) -> bytes:
ip_header_start = 14 # Skip Ethernet header
ip_header_len = (data[ip_header_start] & 0x0F) * 4
tcp_header_start = ip_header_start + ip_header_len
tcp_header_len = ((data[tcp_header_start + 12] >> 4) & 0x0F) * 4
payload_start = tcp_header_start + tcp_header_len
return data[payload_start:]
def parse_file(filename: str):
with open(filename, 'r') as f:
content = f.read()
packets = extract_bytes_from_c_array(content)
for name, data in packets:
print(f"\nPacket {name}:")
payload = get_tcp_payload(data)
print(f"Dircon payload: {payload.hex()}")
offset = 0
while offset < len(payload):
packet, consumed = parse_dircon_packet(payload, offset)
if packet is None or consumed == 0:
break
print(f"Frame: {packet}")
offset += consumed
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python script.py <input_file>")
sys.exit(1)
parse_file(sys.argv[1])

331
helpers/wahoo-simulator.py Normal file
View File

@@ -0,0 +1,331 @@
import sys
import logging
import asyncio
import threading
import random
import struct
import binascii
import time
from typing import Any, Union
# Verificare che siamo su macOS
if sys.platform != 'darwin':
print("Questo script è progettato specificamente per macOS!")
sys.exit(1)
# Importare bless
try:
from bless import (
BlessServer,
BlessGATTCharacteristic,
GATTCharacteristicProperties,
GATTAttributePermissions,
)
except ImportError:
print("Errore: impossibile importare bless. Installarlo con: pip install bless")
sys.exit(1)
# Configurazione logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Trigger per eventi
trigger = threading.Event()
# Informazioni sul dispositivo
DEVICE_NAME = "Wahoo KICKR 51A6"
# UUID dei servizi standard
CYCLING_POWER_SERVICE = "00001818-0000-1000-8000-00805f9b34fb"
USER_DATA_SERVICE = "0000181c-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_SERVICE = "00001826-0000-1000-8000-00805f9b34fb"
# UUID dei servizi Wahoo personalizzati
WAHOO_SERVICE_1 = "a026ee01-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_SERVICE_3 = "a026ee03-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_SERVICE_6 = "a026ee06-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_SERVICE_B = "a026ee0b-0a7d-4ab3-97fa-f1500f9feb8b"
# UUID delle caratteristiche standard
CYCLING_POWER_MEASUREMENT = "00002a63-0000-1000-8000-00805f9b34fb"
CYCLING_POWER_FEATURE = "00002a65-0000-1000-8000-00805f9b34fb"
SENSOR_LOCATION = "00002a5d-0000-1000-8000-00805f9b34fb"
CYCLING_POWER_CONTROL_POINT = "00002a66-0000-1000-8000-00805f9b34fb"
WEIGHT = "00002a98-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_FEATURE = "00002acc-0000-1000-8000-00805f9b34fb"
TRAINING_STATUS = "00002ad3-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_CONTROL_POINT = "00002ad9-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_STATUS = "00002ada-0000-1000-8000-00805f9b34fb"
INDOOR_BIKE_DATA = "00002ad2-0000-1000-8000-00805f9b34fb"
# UUID delle caratteristiche Wahoo personalizzate
WAHOO_CUSTOM_CP_CHAR = "a026e005-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_1 = "a026e002-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_2 = "a026e004-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_3 = "a026e00a-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_4 = "a026e023-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_5 = "a026e037-0a7d-4ab3-97fa-f1500f9feb8b"
# Stato dispositivo - variabili globali
current_power = 120
current_cadence = 85
current_speed = 25.0
current_resistance = 5
# Funzioni di callback
def read_request(characteristic, **kwargs):
logger.debug(f"Lettura: {characteristic.value}")
return characteristic.value
def write_request(characteristic, value, **kwargs):
uuid_str = str(characteristic.uuid).lower()
logger.info(f"Scrittura su caratteristica: {uuid_str}, valore: {binascii.hexlify(value)}")
# Gestione delle richieste di scrittura
if uuid_str == FITNESS_MACHINE_CONTROL_POINT.lower():
handle_ftms_control_point(value)
elif uuid_str == CYCLING_POWER_CONTROL_POINT.lower():
handle_cp_control_point(value)
elif uuid_str in [WAHOO_CHAR_1.lower(), WAHOO_CHAR_3.lower(), WAHOO_CHAR_4.lower(), WAHOO_CHAR_5.lower()]:
handle_wahoo_char_write(uuid_str, value)
characteristic.value = value
# Gestori di richieste di scrittura
def handle_ftms_control_point(data):
global current_power, current_resistance
if not data:
return
op_code = data[0]
logger.info(f"Comando FTMS Control Point: {op_code:#x}")
if op_code == 0x05: # Set Target Power (ERG mode)
if len(data) >= 3:
target_power = int.from_bytes(data[1:3], byteorder='little')
logger.info(f"Target power impostato: {target_power}W")
current_power = target_power
def handle_cp_control_point(data):
if not data:
return
op_code = data[0]
logger.info(f"Comando CP Control Point: {op_code:#x}")
def handle_wahoo_char_write(uuid_str, data):
logger.info(f"Scrittura su caratteristica Wahoo {uuid_str}: {binascii.hexlify(data)}")
# Funzioni per generare dati
def generate_cycling_power_data():
global current_power, current_cadence
# Varia leggermente i valori
current_power += random.randint(-3, 3)
current_power = max(0, min(2000, current_power))
current_cadence += random.randint(-1, 1)
current_cadence = max(0, min(200, current_cadence))
# Crea Cycling Power Measurement
power_data = bytearray(16)
power_data[0:2] = (0x0034).to_bytes(2, byteorder='little')
power_data[2:4] = (current_power).to_bytes(2, byteorder='little')
power_data[4:8] = (int(current_power * 10)).to_bytes(4, byteorder='little')
power_data[8:12] = (0).to_bytes(4, byteorder='little')
power_data[12:14] = (current_cadence).to_bytes(2, byteorder='little')
power_data[14:16] = (0xBAD8).to_bytes(2, byteorder='little')
return bytes(power_data)
def generate_indoor_bike_data():
global current_speed, current_cadence
# Varia leggermente i valori
current_speed += random.uniform(-0.2, 0.2)
current_speed = max(0, min(60.0, current_speed))
# Crea Indoor Bike Data
bike_data = bytearray(8)
bike_data[0:2] = (0x0044).to_bytes(2, byteorder='little')
bike_data[2:4] = (int(current_speed * 100)).to_bytes(2, byteorder='little')
bike_data[4:6] = (current_cadence).to_bytes(2, byteorder='little')
bike_data[6:8] = (0).to_bytes(2, byteorder='little')
return bytes(bike_data)
async def run():
# Crea server con minimo di parametri
server = BlessServer(name=DEVICE_NAME)
server.read_request_func = read_request
server.write_request_func = write_request
logger.info(f"Configurazione del simulatore {DEVICE_NAME}...")
# 1. Servizi standard
# Aggiungi Cycling Power Service
await server.add_new_service(CYCLING_POWER_SERVICE)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
CYCLING_POWER_MEASUREMENT,
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable
)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
CYCLING_POWER_FEATURE,
GATTCharacteristicProperties.read,
None,
GATTAttributePermissions.readable
)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
CYCLING_POWER_CONTROL_POINT,
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
WAHOO_CUSTOM_CP_CHAR,
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
# Aggiungi Fitness Machine Service
await server.add_new_service(FITNESS_MACHINE_SERVICE)
await server.add_new_characteristic(
FITNESS_MACHINE_SERVICE,
INDOOR_BIKE_DATA,
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable
)
await server.add_new_characteristic(
FITNESS_MACHINE_SERVICE,
FITNESS_MACHINE_CONTROL_POINT,
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
await server.add_new_characteristic(
FITNESS_MACHINE_SERVICE,
FITNESS_MACHINE_FEATURE,
GATTCharacteristicProperties.read,
None,
GATTAttributePermissions.readable
)
# 2. Servizi Wahoo personalizzati
# Wahoo Service 1
await server.add_new_service(WAHOO_SERVICE_1)
await server.add_new_characteristic(
WAHOO_SERVICE_1,
WAHOO_CHAR_1,
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
await server.add_new_characteristic(
WAHOO_SERVICE_1,
WAHOO_CHAR_2,
GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable
)
# Wahoo Service 3
await server.add_new_service(WAHOO_SERVICE_3)
await server.add_new_characteristic(
WAHOO_SERVICE_3,
WAHOO_CHAR_3,
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
# Wahoo Service 6
await server.add_new_service(WAHOO_SERVICE_6)
await server.add_new_characteristic(
WAHOO_SERVICE_6,
WAHOO_CHAR_4,
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
# Wahoo Service B
await server.add_new_service(WAHOO_SERVICE_B)
await server.add_new_characteristic(
WAHOO_SERVICE_B,
WAHOO_CHAR_5,
GATTCharacteristicProperties.read | GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
logger.info("Configurazione dei servizi completata")
# Avvia il server
await server.start()
logger.info(f"{DEVICE_NAME} è ora in fase di advertising")
# Imposta i valori iniziali DOPO l'avvio del server
# Valori per servizi standard
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
server.get_characteristic(CYCLING_POWER_FEATURE).value = (0x08).to_bytes(4, byteorder='little')
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
server.get_characteristic(FITNESS_MACHINE_FEATURE).value = (0x02C6).to_bytes(4, byteorder='little')
# Valori per caratteristiche Wahoo
server.get_characteristic(WAHOO_CHAR_1).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_2).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_3).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_4).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_5).value = bytearray(1)
# Loop di aggiornamento
try:
counter = 0
while True:
# Aggiorna i dati principali
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
# Invia notifiche
server.update_value(FITNESS_MACHINE_SERVICE, INDOOR_BIKE_DATA)
server.update_value(CYCLING_POWER_SERVICE, CYCLING_POWER_MEASUREMENT)
if counter % 10 == 0: # Log ogni 10 cicli
logger.info(f"Potenza: {current_power}W, Cadenza: {current_cadence}rpm, Velocità: {current_speed:.1f}km/h")
counter += 1
await asyncio.sleep(0.1)
except KeyboardInterrupt:
logger.info("Arresto richiesto dall'utente")
except Exception as e:
logger.error(f"Errore durante l'esecuzione: {e}")
finally:
await server.stop()
logger.info("Server arrestato")
if __name__ == "__main__":
print("=" * 80)
print(f"Wahoo KICKR 51A6 BLE Simulator per macOS (Versione completa)")
print("=" * 80)
print(f"Avvio della simulazione di {DEVICE_NAME}")
print("Premi Ctrl+C per terminare il server")
print("=" * 80)
try:
asyncio.run(run())
except KeyboardInterrupt:
print("\nSimulazione fermata dall'utente")
except Exception as e:
print(f"Errore: {e}")
print("Potrebbe essere necessario eseguire questo script con sudo")
sys.exit(1)

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

@@ -9,11 +9,15 @@ import QtQuick.Layouts 1.3
import QtWebView 1.1
Item {
id: pelotonAuthPage
anchors.fill: parent
height: parent.height
width: parent.width
visible: true
// Signal to notify the parent stack when we want to go back
signal goBack()
WebView {
anchors.fill: parent
height: parent.height
@@ -27,7 +31,11 @@ Item {
parent: Overlay.overlay
enabled: rootItem.pelotonPopupVisible
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnectedWeb.open() }
onClosed: { rootItem.pelotonPopupVisible = false; }
onClosed: {
rootItem.pelotonPopupVisible = false;
// Attempt to go back to the previous view after the popup is closed
goBack();
}
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
@@ -45,14 +53,28 @@ Item {
{
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.horizontalCenter: parent.horizontalCenter
width: 370
height: 120
text: qsTr("Your Peloton account is now connected!<br><br>Restart the app to apply this!")
text: qsTr("Your Peloton account is now connected!")
}
}
// Add a MouseArea to capture clicks anywhere on the popup
MouseArea {
anchors.fill: parent
onClicked: {
popupPelotonConnectedWeb.close();
}
}
}
// Component is being completed
Component.onCompleted: {
console.log("WebPelotonAuth loaded")
}
}

View File

@@ -5,6 +5,7 @@ import Qt.labs.settings 1.0
Page {
id: wizardPage
objectName: "wizardPage"
property int currentStep: 0
property var selectedOptions: ({})
@@ -335,7 +336,7 @@ Page {
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Peloton Login")
text: qsTr("Connect to Peloton")
font.pixelSize: 24
font.bold: true
color: "white"
@@ -343,56 +344,38 @@ Page {
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Username")
text: qsTr("Click the button below to connect your Peloton account")
font.pixelSize: 20
font.bold: true
wrapMode: Text.WordWrap
Layout.fillWidth: true
width: stackViewLocal.width * 0.8
horizontalAlignment: Text.AlignHCenter
color: "white"
}
TextField {
id: pelotonUsernameTextField
text: settings.peloton_username
horizontalAlignment: Text.AlignHCenter
Image {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
onAccepted: settings.peloton_username = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
source: "icons/icons/Button_Connect_Rect_DarkMode.png"
fillMode: Image.PreserveAspectFit
width: parent.width * 0.8
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Password")
font.pixelSize: 20
font.bold: true
color: "white"
}
TextField {
id: pelotonPasswordTextField
text: settings.peloton_password
horizontalAlignment: Text.AlignHCenter
Layout.fillHeight: false
Layout.alignment: Qt.AlignHCenter
inputMethodHints: Qt.ImhHiddenText
echoMode: TextInput.PasswordEchoOnEdit
onAccepted: settings.peloton_password = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
MouseArea {
anchors.fill: parent
onClicked: {
stackViewLocal.push("WebPelotonAuth.qml")
stackViewLocal.currentItem.goBack.connect(function() {
stackViewLocal.pop();
stackViewLocal.push(pelotonDifficultyComponent)
})
peloton_connect_clicked()
}
}
}
Item {
Layout.preferredHeight: 50
}
WizardButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Next")
onClicked: {
settings.peloton_username = pelotonUsernameTextField.text;
settings.peloton_password = pelotonPasswordTextField.text;
stackViewLocal.push(pelotonDifficultyComponent)
}
}
WizardButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Back")

227
src/WorkoutsHistory.qml Normal file
View File

@@ -0,0 +1,227 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtCharts 2.15
Page {
id: workoutHistoryPage
// Signal for chart preview
signal fitfile_preview_clicked(var url)
// 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"
Text {
anchors.centerIn: parent
text: "Workout History"
font.pixelSize: 24
font.bold: true
}
}
// 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 {
Layout.fillWidth: true
Layout.fillHeight: true
model: workoutModel
spacing: 8
clip: true
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: "🗑️ Delete"
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"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 16
// Sport icon
Column {
Layout.alignment: Qt.AlignVCenter
Text {
text: getSportIcon(sport)
font.pixelSize: 32
}
}
// Workout info
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: title
font.bold: true
font.pixelSize: 18
}
Text {
text: date
color: "#666666"
}
// Stats row
RowLayout {
spacing: 16
Text {
text: "⏱ " + duration
}
Text {
text: "📏 " + distance.toFixed(2) + " km"
}
}
RowLayout {
spacing: 16
Text {
text: "🔥 " + Math.round(calories) + " kcal"
}
}
}
}
}
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
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
var fileUrl = Qt.resolvedUrl("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()
}
}
}

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.20" android:versionCode="1020" 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.19.2" android:versionCode="1114" 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 ->

View File

@@ -1,5 +1,4 @@
package org.cagnulen.qdomyoszwift;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.Activity;
@@ -23,110 +22,145 @@ import android.widget.Button;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.content.Intent;
public class Ant {
private ChannelService.ChannelServiceComm mChannelService = null;
private boolean mChannelServiceBound = false;
private final String TAG = "Ant";
public static Activity activity = null;
static boolean speedRequest = false;
static boolean heartRequest = false;
static boolean bikeRequest = false; // Added bike request flag
static boolean garminKey = false;
static boolean treadmill = false;
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill) {
Log.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
// Updated antStart method with BikeRequest parameter at the end
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest) {
QLog.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
bikeRequest = BikeRequest; // Set bike request flag
activity = a;
if(a != null)
QLog.v(TAG, "antStart activity is valid");
else
{
QLog.v(TAG, "antStart activity is invalid");
return;
}
activity = a;
if(a != null)
Log.v(TAG, "antStart activity is valid");
else
{
Log.v(TAG, "antStart activity is invalid");
return;
}
if(!mChannelServiceBound) doBindChannelService();
}
private ServiceConnection mChannelServiceConnection = new ServiceConnection()
{
@Override
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
{
Log.v(TAG, "mChannelServiceConnection.onServiceConnected...");
@Override
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
{
QLog.v(TAG, "mChannelServiceConnection.onServiceConnected...");
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
QLog.v(TAG, "...mChannelServiceConnection.onServiceConnected");
}
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
Log.v(TAG, "...mChannelServiceConnection.onServiceConnected");
}
@Override
public void onServiceDisconnected(ComponentName arg0)
{
Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
// Clearing and disabling when disconnecting from ChannelService
mChannelService = null;
Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
}
};
@Override
public void onServiceDisconnected(ComponentName arg0)
{
QLog.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
// Clearing and disabling when disconnecting from ChannelService
mChannelService = null;
QLog.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
}
};
private void doBindChannelService()
{
Log.v(TAG, "doBindChannelService...");
// Binds to ChannelService. ChannelService binds and manages connection between the
// app and the ANT Radio Service
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection , Context.BIND_AUTO_CREATE);
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
doUnbindChannelService();
Log.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
Log.v(TAG, "...doBindChannelService");
}
QLog.v(TAG, "doBindChannelService...");
// Binds to ChannelService. ChannelService binds and manages connection between the
// app and the ANT Radio Service
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection, Context.BIND_AUTO_CREATE);
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
doUnbindChannelService();
QLog.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
QLog.v(TAG, "...doBindChannelService");
}
public void doUnbindChannelService()
{
Log.v(TAG, "doUnbindChannelService...");
if(mChannelServiceBound)
{
activity.unbindService(mChannelServiceConnection);
mChannelServiceBound = false;
}
Log.v(TAG, "...doUnbindChannelService");
}
QLog.v(TAG, "doUnbindChannelService...");
if(mChannelServiceBound)
{
activity.unbindService(mChannelServiceConnection);
mChannelServiceBound = false;
}
QLog.v(TAG, "...doUnbindChannelService");
}
public void setCadenceSpeedPower(float speed, int power, int cadence)
{
if(mChannelService == null)
return;
Log.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
mChannelService.setSpeed(speed);
mChannelService.setPower(power);
mChannelService.setCadence(cadence);
if(mChannelService == null)
return;
QLog.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
mChannelService.setSpeed(speed);
mChannelService.setPower(power);
mChannelService.setCadence(cadence);
}
public int getHeart()
{
if(mChannelService == null)
return 0;
if(mChannelService == null)
return 0;
QLog.v(TAG, "getHeart");
return mChannelService.getHeart();
}
Log.v(TAG, "getHeart");
return mChannelService.getHeart();
// Added bike-related getter methods
public int getBikeCadence() {
if(mChannelService == null)
return 0;
QLog.v(TAG, "getBikeCadence");
return mChannelService.getBikeCadence();
}
public int getBikePower() {
if(mChannelService == null)
return 0;
QLog.v(TAG, "getBikePower");
return mChannelService.getBikePower();
}
public double getBikeSpeed() {
if(mChannelService == null)
return 0.0;
QLog.v(TAG, "getBikeSpeed");
return mChannelService.getBikeSpeed();
}
public long getBikeDistance() {
if(mChannelService == null)
return 0;
QLog.v(TAG, "getBikeDistance");
return mChannelService.getBikeDistance();
}
public boolean isBikeConnected() {
if(mChannelService == null)
return false;
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

@@ -0,0 +1,239 @@
package org.cagnulen.qdomyoszwift;
import android.content.Context;
import org.cagnulen.qdomyoszwift.QLog;
import android.app.Activity;
// ANT+ Plugin imports
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IFitnessEquipmentStateReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IBikeDataReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnessEquipmentDataReceiver;
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.defines.DeviceState;
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IDeviceStateChangeReceiver;
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IPluginAccessResultReceiver;
import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle;
// Java imports
import java.math.BigDecimal;
import java.util.EnumSet;
public class BikeChannelController {
private static final String TAG = BikeChannelController.class.getSimpleName();
private Context context;
private AntPlusFitnessEquipmentPcc fePcc = null;
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
private boolean isConnected = false;
// Bike data fields
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
public long distance = 0; // Total distance in meters
public long calories = 0; // Total calories burned
public EquipmentType equipmentType = EquipmentType.UNKNOWN;
public EquipmentState equipmentState = EquipmentState.ASLEEP_OFF;
public int heartRate = 0; // Heart rate from equipment
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
// Fitness equipment state receiver
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
new IFitnessEquipmentStateReceiver() {
@Override
public void onNewFitnessEquipmentState(long estTimestamp,
EnumSet<EventFlag> eventFlags,
EquipmentType type,
EquipmentState state) {
equipmentType = type;
equipmentState = state;
QLog.d(TAG, "Equipment type: " + type + ", State: " + state);
// Only subscribe to bike specific data if this is actually a bike
if (type == EquipmentType.BIKE && !isSubscribedToBikeData) {
subscribeToBikeSpecificData();
isSubscribedToBikeData = true;
}
}
};
public BikeChannelController() {
this.context = Ant.activity;
openChannel();
}
public boolean openChannel() {
// Request access to first available fitness equipment device
// Using requestNewOpenAccess from the sample code
releaseHandle = AntPlusFitnessEquipmentPcc.requestNewOpenAccess(
(Activity)context,
context,
new IPluginAccessResultReceiver<AntPlusFitnessEquipmentPcc>() {
@Override
public void onResultReceived(AntPlusFitnessEquipmentPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
switch(resultCode) {
case SUCCESS:
fePcc = result;
isConnected = true;
QLog.d(TAG, "Connected to fitness equipment: " + result.getDeviceName());
subscribeToBikeEvents();
break;
case CHANNEL_NOT_AVAILABLE:
QLog.e(TAG, "Channel Not Available");
break;
case ADAPTER_NOT_DETECTED:
QLog.e(TAG, "ANT Adapter Not Available");
break;
case BAD_PARAMS:
QLog.e(TAG, "Bad request parameters");
break;
case OTHER_FAILURE:
QLog.e(TAG, "RequestAccess failed");
break;
case DEPENDENCY_NOT_INSTALLED:
QLog.e(TAG, "Dependency not installed");
break;
case USER_CANCELLED:
QLog.e(TAG, "User cancelled");
break;
default:
QLog.e(TAG, "Unrecognized result: " + resultCode);
break;
}
}
},
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
if (newDeviceState == DeviceState.DEAD) {
isConnected = false;
}
}
},
mFitnessEquipmentStateReceiver
);
return isConnected;
}
private void subscribeToBikeEvents() {
if (fePcc != null) {
// General fitness equipment data
fePcc.subscribeGeneralFitnessEquipmentDataEvent(new IGeneralFitnessEquipmentDataReceiver() {
@Override
public void onNewGeneralFitnessEquipmentData(long estTimestamp, EnumSet<EventFlag> eventFlags,
BigDecimal elapsedTime, long cumulativeDistance,
BigDecimal instantaneousSpeed, boolean virtualInstantaneousSpeed,
int instantaneousHeartRate, HeartRateDataSource source) {
if (elapsedTime != null && elapsedTime.intValue() != -1) {
BikeChannelController.this.elapsedTime = elapsedTime;
}
if (cumulativeDistance != -1) {
distance = cumulativeDistance;
}
if (instantaneousSpeed != null && instantaneousSpeed.intValue() != -1) {
speed = instantaneousSpeed;
}
if (instantaneousHeartRate != -1) {
heartRate = instantaneousHeartRate;
heartRateSource = source;
}
QLog.d(TAG, "General Data - Time: " + elapsedTime + "s, Distance: " +
distance + "m, Speed: " + speed + "m/s, HR: " + heartRate + "bpm");
}
});
}
}
private boolean isSubscribedToBikeData = false;
private void subscribeToBikeSpecificData() {
if (fePcc != null) {
// Subscribe to bike specific data
fePcc.getBikeMethods().subscribeBikeDataEvent(new IBikeDataReceiver() {
@Override
public void onNewBikeData(long estTimestamp, EnumSet<EventFlag> eventFlags,
int instantaneousCadence, int instantaneousPower) {
if (instantaneousCadence != -1) {
cadence = instantaneousCadence;
}
if (instantaneousPower != -1) {
power = instantaneousPower;
}
QLog.d(TAG, "Bike Data - Cadence: " + cadence + "rpm, Power: " + power + "W");
}
});
}
}
public void close() {
if (releaseHandle != null) {
releaseHandle.close();
releaseHandle = null;
}
fePcc = null;
isConnected = false;
QLog.d(TAG, "Channel Closed");
}
// Getter methods for bike data
public int getCadence() {
return cadence;
}
public int getPower() {
return power;
}
public double getSpeedKph() {
// Convert from m/s to km/h
return speed.doubleValue() * 3.6;
}
public double getSpeedMps() {
return speed.doubleValue();
}
public long getDistance() {
return distance;
}
public long getCalories() {
return calories;
}
public int getHeartRate() {
return heartRate;
}
public BigDecimal getElapsedTime() {
return elapsedTime;
}
public EquipmentState getEquipmentState() {
return equipmentState;
}
public EquipmentType getEquipmentType() {
return equipmentType;
}
public boolean isConnected() {
return isConnected;
}
}

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

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.content.BroadcastReceiver;
import android.content.ContextWrapper;
import android.content.IntentFilter;
@@ -89,12 +89,12 @@ public class BleAdvertiser {
private static AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.d("BleAdvertiser", "Advertising started successfully");
QLog.d("BleAdvertiser", "Advertising started successfully");
}
@Override
public void onStartFailure(int errorCode) {
Log.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
QLog.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
}
};
}

View File

@@ -25,7 +25,7 @@ import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class CSafeRowerUSBHID {
@@ -34,21 +34,21 @@ public class CSafeRowerUSBHID {
static int lastReadLen = 0;
public static void open(Context context) {
Log.d("QZ","CSafeRowerUSBHID open");
QLog.d("QZ","CSafeRowerUSBHID open");
hidBridge = new HidBridge(context, 0x0002, 0x17A4);
boolean ret = hidBridge.OpenDevice();
Log.d("QZ","hidBridge.OpenDevice " + ret);
QLog.d("QZ","hidBridge.OpenDevice " + ret);
if(ret == false) {
hidBridge = new HidBridge(context, 0x0001, 0x17A4);
ret = hidBridge.OpenDevice();
Log.d("QZ","hidBridge.OpenDevice " + ret);
QLog.d("QZ","hidBridge.OpenDevice " + ret);
}
hidBridge.StartReadingThread();
Log.d("QZ","hidBridge.StartReadingThread");
QLog.d("QZ","hidBridge.StartReadingThread");
}
public static void write (byte[] bytes) {
Log.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
QLog.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
hidBridge.WriteData(bytes);
}
@@ -60,10 +60,10 @@ public class CSafeRowerUSBHID {
if(hidBridge.IsThereAnyReceivedData()) {
receiveData = hidBridge.GetReceivedDataFromQueue();
lastReadLen = receiveData.length;
Log.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
QLog.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
return receiveData;
} else {
Log.d("QZ","CSafeRowerUSBHID empty data");
QLog.d("QZ","CSafeRowerUSBHID empty data");
lastReadLen = 0;
return null;
}

View File

@@ -34,7 +34,7 @@ import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.SparseArray;
import android.os.Build;
import androidx.core.content.ContextCompat;
@@ -49,15 +49,21 @@ 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
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v(TAG, "onServiceConnected");
QLog.v(TAG, "onServiceConnected");
// Must pass in the received IBinder object to correctly construct an AntService object
mAntRadioService = new AntService(service);
@@ -72,7 +78,7 @@ public class ChannelService extends Service {
// radio by attempting to acquire a channel.
boolean legacyInterfaceInUse = mAntChannelProvider.isLegacyInterfaceInUse();
Log.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
QLog.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
// If there are channels OR legacy interface in use, allow adding channels
if (mChannelAvailable || legacyInterfaceInUse) {
@@ -85,7 +91,7 @@ public class ChannelService extends Service {
try {
openAllChannels();
} catch (ChannelNotAvailableException exception) {
Log.e(TAG, "Channel not available!!");
QLog.e(TAG, "Channel not available!!");
}
} catch (RemoteException e) {
// TODO Auto-generated catch block
@@ -117,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) {
@@ -135,16 +149,164 @@ 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() {
if (null != heartChannelController) {
Log.v(TAG, "getHeart");
QLog.v(TAG, "getHeart");
return heartChannelController.heart;
}
if (null != bikeChannelController) {
return bikeChannelController.getHeartRate();
}
return 0;
}
// Added getters for bike channel data
int getBikeCadence() {
if (null != bikeChannelController) {
return bikeChannelController.getCadence();
}
return 0;
}
int getBikePower() {
if (null != bikeChannelController) {
return bikeChannelController.getPower();
}
return 0;
}
double getBikeSpeed() {
if (null != bikeChannelController) {
return bikeChannelController.getSpeedKph();
}
return 0.0;
}
long getBikeDistance() {
if (null != bikeChannelController) {
return bikeChannelController.getDistance();
}
return 0;
}
boolean isBikeConnected() {
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.
*/
@@ -165,6 +327,72 @@ public class ChannelService extends Service {
speedChannelController = new SpeedChannelController(acquireChannel());
}
}
// Add initialization for BikeChannelController (receiver)
if (Ant.bikeRequest && bikeChannelController == null) {
bikeChannelController = new BikeChannelController();
}
// 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;
}
}
}
private void closeAllChannels() {
@@ -176,10 +404,18 @@ public class ChannelService extends Service {
speedChannelController.close();
if (sdmChannelController != null)
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 {
@@ -200,13 +436,13 @@ public class ChannelService extends Service {
else {
NetworkKey mNK = new NetworkKey(new byte[]{(byte) 0xb9, (byte) 0xa5, (byte) 0x21, (byte) 0xfb,
(byte) 0xbd, (byte) 0x72, (byte) 0xc3, (byte) 0x45});
Log.v(TAG, mNK.toString());
QLog.v(TAG, mNK.toString());
mAntChannel = mAntChannelProvider.acquireChannelOnPrivateNetwork(this, mNK);
}
} catch (RemoteException e) {
Log.v(TAG, "ACP Remote Ex");
QLog.v(TAG, "ACP Remote Ex");
} catch (UnsupportedFeatureException e) {
Log.v(TAG, "ACP UnsupportedFeature Ex");
QLog.v(TAG, "ACP UnsupportedFeature Ex");
}
}
return mAntChannel;
@@ -223,14 +459,14 @@ public class ChannelService extends Service {
private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
QLog.d(TAG, "onReceive");
if (AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) {
boolean update = false;
// Retrieving the data contained in the intent
int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0);
boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false);
Log.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
QLog.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
if (mAllowAddChannel) {
// Was a acquire channel allowed
@@ -249,7 +485,7 @@ public class ChannelService extends Service {
try {
openAllChannels();
} catch (ChannelNotAvailableException exception) {
Log.e(TAG, "Channel not available!!");
QLog.e(TAG, "Channel not available!!");
}
}
}
@@ -258,7 +494,7 @@ public class ChannelService extends Service {
};
private void doBindAntRadioService() {
if (BuildConfig.DEBUG) Log.v(TAG, "doBindAntRadioService");
if (BuildConfig.DEBUG) QLog.v(TAG, "doBindAntRadioService");
ContextCompat.registerReceiver(
this,
@@ -273,14 +509,14 @@ public class ChannelService extends Service {
}
private void doUnbindAntRadioService() {
if (BuildConfig.DEBUG) Log.v(TAG, "doUnbindAntRadioService");
if (BuildConfig.DEBUG) QLog.v(TAG, "doUnbindAntRadioService");
// Stop listing for channel available intents
try {
unregisterReceiver(mChannelProviderStateChangedReceiver);
} catch (IllegalArgumentException exception) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
QLog.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
}
if (mAntRadioServiceBound) {
@@ -315,7 +551,7 @@ public class ChannelService extends Service {
}
static void die(String error) {
Log.e(TAG, "DIE: " + error);
QLog.e(TAG, "DIE: " + error);
}
}
}

View File

@@ -4,20 +4,20 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class ContentHelper {
public static String getFileName(Context context, Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
Log.d("ContentHelper", "content");
QLog.d("ContentHelper", "content");
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
Log.d("ContentHelper", "cursor " + cursor);
QLog.d("ContentHelper", "cursor " + cursor);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
Log.d("ContentHelper", "result " + result);
QLog.d("ContentHelper", "result " + result);
}
} finally {
cursor.close();

View File

@@ -0,0 +1,34 @@
package org.cagnulen.qdomyoszwift;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.qtproject.qt5.android.bindings.QtActivity;
public class CustomQtActivity extends QtActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Handle Android 16 API 36 WindowInsetsController for fullscreen support
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 (API 30) and above - use WindowInsetsController
getWindow().setDecorFitsSystemWindows(false);
WindowInsetsController controller = getWindow().getDecorView().getWindowInsetsController();
if (controller != null) {
controller.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
// Fallback for older Android versions (API < 30)
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
);
}
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -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

@@ -24,7 +24,7 @@ import android.widget.Toast;
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.WebViewClient;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.content.SharedPreferences;
public class FloatingWindowGFG extends Service {
@@ -82,14 +82,14 @@ public class FloatingWindowGFG extends Service {
});
WebSettings settings = wv.getSettings();
settings.setJavaScriptEnabled(true);
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/floating.htm");
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
wv.clearView();
wv.measure(100, 100);
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
settings.setBuiltInZoomControls(true);
settings.setUseWideViewPort(true);
settings.setDomStorageEnabled(true);
Log.d("QZ","loadurl");
QLog.d("QZ","loadurl");
// WindowManager.LayoutParams takes a lot of parameters to set the
@@ -153,7 +153,7 @@ public class FloatingWindowGFG extends Service {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("QZ","onTouch");
QLog.d("QZ","onTouch");
switch (event.getAction()) {
// When the window will be touched,

View File

@@ -10,7 +10,7 @@ import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import android.content.pm.ServiceInfo;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class ForegroundService extends Service {
public static final String CHANNEL_ID = "ForegroundServiceChannel";
@@ -43,7 +43,7 @@ public class ForegroundService extends Service {
startForeground(1, notification);
}
} catch (Exception e) {
Log.e("ForegroundService", "Failed to start foreground service", e);
QLog.e("ForegroundService", "Failed to start foreground service", e);
return START_NOT_STICKY;
}
//do heavy work on a background thread

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;
@@ -53,22 +53,22 @@ public class Garmin {
private static Integer Power = 0;
public static int getHR() {
Log.d(TAG, "getHR " + HR);
QLog.d(TAG, "getHR " + HR);
return HR;
}
public static int getPower() {
Log.d(TAG, "getPower " + Power);
QLog.d(TAG, "getPower " + Power);
return Power;
}
public static double getSpeed() {
Log.d(TAG, "getSpeed " + Speed);
QLog.d(TAG, "getSpeed " + Speed);
return Speed;
}
public static int getFootCad() {
Log.d(TAG, "getFootCad " + FootCad);
QLog.d(TAG, "getFootCad " + FootCad);
return FootCad;
}
@@ -83,7 +83,7 @@ public class Garmin {
@Override
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
Log.e(TAG, errStatus.toString());
QLog.e(TAG, errStatus.toString());
connectIqReady = false;
}
@@ -91,7 +91,7 @@ public class Garmin {
public void onSdkReady() {
connectIqInitializing = false;
connectIqReady = true;
Log.i(TAG, " onSdkReady");
QLog.i(TAG, " onSdkReady");
registerWatchMessagesReceiver();
registerDeviceStatusReceiver();
@@ -118,16 +118,16 @@ public class Garmin {
try {
List<IQDevice> devices = connectIQ.getConnectedDevices();
if (devices != null && devices.size() > 0) {
Log.v(TAG, "getDevice connected: " + devices.get(0).toString() );
QLog.v(TAG, "getDevice connected: " + devices.get(0).toString() );
deviceCache = devices.get(0);
return deviceCache;
} else {
return deviceCache;
}
} catch (InvalidStateException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
} catch (ServiceUnavailableException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
}
return null;
}
@@ -193,33 +193,33 @@ public class Garmin {
@Override
public void onApplicationInfoReceived(IQApp app) {
Log.d(TAG, "App installed.");
QLog.d(TAG, "App installed.");
}
@Override
public void onApplicationNotInstalled(String applicationId) {
if (getDevice() != null) {
Toast.makeText(context, "App not installed on your Garmin watch", Toast.LENGTH_LONG).show();
Log.d(TAG, "watch app not installed.");
QLog.d(TAG, "watch app not installed.");
}
}
});
} catch (InvalidStateException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
} catch (ServiceUnavailableException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
}
}
private static void registerDeviceStatusReceiver() {
Log.d(TAG, "registerDeviceStatusReceiver");
QLog.d(TAG, "registerDeviceStatusReceiver");
IQDevice device = getDevice();
try {
if (device != null) {
connectIQ.registerForDeviceEvents(device, new ConnectIQ.IQDeviceEventListener() {
@Override
public void onDeviceStatusChanged(IQDevice device, IQDevice.IQDeviceStatus newStatus) {
Log.d(TAG, "Device status changed, now " + newStatus);
QLog.d(TAG, "Device status changed, now " + newStatus);
}
});
}
@@ -229,7 +229,7 @@ public class Garmin {
}
private static void registerWatchMessagesReceiver(){
Log.d(TAG, "registerWatchMessageReceiver");
QLog.d(TAG, "registerWatchMessageReceiver");
IQDevice device = getDevice();
try {
if (device != null) {
@@ -238,7 +238,7 @@ public class Garmin {
public void onMessageReceived(IQDevice device, IQApp app, List<Object> message, ConnectIQ.IQMessageStatus status) {
if (status == ConnectIQ.IQMessageStatus.SUCCESS) {
//MessageHandler.getInstance().handleMessageFromWatchUsingCIQ(message, status, context);
Log.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
QLog.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
try {
String var[] = message.toArray()[0].toString().split(",");
HR = Integer.parseInt(var[0].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
@@ -249,21 +249,21 @@ public class Garmin {
Speed = Double.parseDouble(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
}
}
Log.d(TAG, "HR " + HR);
Log.d(TAG, "FootCad " + FootCad);
QLog.d(TAG, "HR " + HR);
QLog.d(TAG, "FootCad " + FootCad);
} catch (Exception e) {
Log.e(TAG, "Processing error", e);
QLog.e(TAG, "Processing error", e);
}
} else {
Log.d(TAG, "onMessageReceived error, status: " + status.toString());
QLog.d(TAG, "onMessageReceived error, status: " + status.toString());
}
}
});
} else {
Log.d(TAG, "registerWatchMessagesReceiver: No device found.");
QLog.d(TAG, "registerWatchMessagesReceiver: No device found.");
}
} catch (InvalidStateException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
}
}
@@ -273,19 +273,19 @@ public class Garmin {
try {
if (context != null) {
Log.d(TAG, "Shutting down with wrapped context");
QLog.d(TAG, "Shutting down with wrapped context");
connectIQ.shutdown(context);
} else {
Log.d(TAG, "Shutting down without wrapped context");
QLog.d(TAG, "Shutting down without wrapped context");
connectIQ.shutdown(applicationContext);
}
} catch (InvalidStateException e) {
// This is usually because the SDK was already shut down so no worries.
Log.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
QLog.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
} catch (RuntimeException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
}
}
@@ -299,11 +299,11 @@ public class Garmin {
}
}
} catch (InvalidStateException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
} catch (IllegalArgumentException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
} catch (RuntimeException e) {
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
}
}
}

View File

@@ -1,7 +1,7 @@
package org.cagnulen.qdomyoszwift;
import android.content.Context;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.app.Activity;
// ANT+ Plugin imports
@@ -57,26 +57,26 @@ public class HeartChannelController {
case SUCCESS:
hrPcc = result;
isConnected = true;
Log.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
subscribeToHrEvents();
break;
case CHANNEL_NOT_AVAILABLE:
Log.e(TAG, "Channel Not Available");
QLog.e(TAG, "Channel Not Available");
break;
case ADAPTER_NOT_DETECTED:
Log.e(TAG, "ANT Adapter Not Available");
QLog.e(TAG, "ANT Adapter Not Available");
break;
case BAD_PARAMS:
Log.e(TAG, "Bad request parameters");
QLog.e(TAG, "Bad request parameters");
break;
case OTHER_FAILURE:
Log.e(TAG, "RequestAccess failed");
QLog.e(TAG, "RequestAccess failed");
break;
case DEPENDENCY_NOT_INSTALLED:
Log.e(TAG, "Dependency not installed");
QLog.e(TAG, "Dependency not installed");
break;
default:
Log.e(TAG, "Unrecognized result: " + resultCode);
QLog.e(TAG, "Unrecognized result: " + resultCode);
break;
}
}
@@ -84,7 +84,7 @@ public class HeartChannelController {
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
Log.d(TAG, "Device State Changed to: " + newDeviceState);
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
if (newDeviceState == DeviceState.DEAD) {
isConnected = false;
}
@@ -104,7 +104,7 @@ public class HeartChannelController {
BigDecimal heartBeatEventTime, DataState dataState) {
heart = computedHeartRate;
Log.d(TAG, "Heart Rate: " + heart);
QLog.d(TAG, "Heart Rate: " + heart);
}
});
}
@@ -117,7 +117,7 @@ public class HeartChannelController {
}
hrPcc = null;
isConnected = false;
Log.d(TAG, "Channel Closed");
QLog.d(TAG, "Channel Closed");
}
public int getHeartRate() {

View File

@@ -16,7 +16,7 @@ import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.os.Build;
import androidx.core.content.ContextCompat;
@@ -169,7 +169,7 @@ public class HidBridge {
} catch(NullPointerException e)
{
Log("Error happened while writing. Could not connect to the device or interface is busy?");
Log.e("HidBridge", Log.getStackTraceString(e));
QLog.e("HidBridge", QLog.getStackTraceString(e));
return false;
}
return true;
@@ -289,7 +289,7 @@ public class HidBridge {
catch (NullPointerException e) {
Log("Error happened while reading. No device or the connection is busy");
Log.e("HidBridge", Log.getStackTraceString(e));
QLog.e("HidBridge", QLog.getStackTraceString(e));
}
catch (ThreadDeath e) {
if (readConnection != null) {
@@ -332,7 +332,7 @@ public class HidBridge {
}
}
else {
Log.d("TAG", "permission denied for the device " + device);
QLog.d("TAG", "permission denied for the device " + device);
}
}
}
@@ -344,7 +344,7 @@ public class HidBridge {
* @param message to log.
*/
private void Log(String message) {
Log.d("HidBridge", message);
QLog.d("HidBridge", message);
}
/**

View File

@@ -8,7 +8,7 @@ import com.garmin.android.connectiq.IQDevice;
import java.nio.BufferUnderflowException;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class IQMessageReceiverWrapper extends BroadcastReceiver {
private final BroadcastReceiver receiver;
@@ -20,7 +20,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive intent " + intent.getAction());
QLog.d(TAG, "onReceive intent " + intent.getAction());
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
@@ -32,7 +32,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
try {
receiver.onReceive(context, intent);
} catch (IllegalArgumentException | BufferUnderflowException e) {
Log.d(TAG, e.toString());
QLog.d(TAG, e.toString());
}
}
@@ -44,7 +44,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
intent.putExtra(extraName, device.getDeviceIdentifier());
}
} catch (ClassCastException e) {
Log.d(TAG, e.toString());
QLog.d(TAG, e.toString());
// It's already a long, i.e. on the simulator.
}
}

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.location.LocationManager;
import android.provider.Settings;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class LocationHelper {
private static final String TAG = "LocationHelper";
@@ -13,7 +13,7 @@ public class LocationHelper {
private static boolean isBluetoothEnabled = false;
public static boolean start(Context context) {
Log.d(TAG, "Starting LocationHelper check...");
QLog.d(TAG, "Starting LocationHelper check...");
isLocationEnabled = isLocationEnabled(context);
isBluetoothEnabled = isBluetoothEnabled();
@@ -23,7 +23,7 @@ public class LocationHelper {
public static void requestPermissions(Context context) {
if (!isLocationEnabled || !isBluetoothEnabled) {
Log.d(TAG, "Some services are disabled. Prompting user...");
QLog.d(TAG, "Some services are disabled. Prompting user...");
if (!isLocationEnabled) {
promptEnableLocation(context);
}
@@ -31,7 +31,7 @@ public class LocationHelper {
promptEnableBluetooth(context);
}
} else {
Log.d(TAG, "All services are already enabled.");
QLog.d(TAG, "All services are already enabled.");
}
}
@@ -50,14 +50,14 @@ public class LocationHelper {
}
private static void promptEnableLocation(Context context) {
Log.d(TAG, "Prompting to enable Location...");
QLog.d(TAG, "Prompting to enable Location...");
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
private static void promptEnableBluetooth(Context context) {
Log.d(TAG, "Prompting to enable Bluetooth...");
QLog.d(TAG, "Prompting to enable Bluetooth...");
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.os.Build;
public class MediaButtonReceiver extends BroadcastReceiver {
@@ -13,7 +13,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MediaButtonReceiver", "Received intent: " + intent.toString());
QLog.d("MediaButtonReceiver", "Received intent: " + intent.toString());
String intentAction = intent.getAction();
if ("android.media.VOLUME_CHANGED_ACTION".equals(intentAction)) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -21,7 +21,7 @@ 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);
Log.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
}
}
@@ -36,7 +36,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
if (context == null) {
Log.e("MediaButtonReceiver", "Context is null, cannot register receiver");
QLog.e("MediaButtonReceiver", "Context is null, cannot register receiver");
return;
}
@@ -44,21 +44,21 @@ public class MediaButtonReceiver extends BroadcastReceiver {
try {
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
} catch (SecurityException se) {
Log.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
}
} else {
try {
context.registerReceiver(instance, filter);
} catch (SecurityException se) {
Log.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
}
}
Log.d("MediaButtonReceiver", "Receiver registered successfully");
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
} catch (IllegalArgumentException e) {
Log.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
} catch (Exception e) {
Log.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
QLog.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
}
}

View File

@@ -12,7 +12,7 @@ import android.util.DisplayMetrics;
import android.os.Build;
import android.provider.Settings;
import android.app.AppOpsManager;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.annotation.TargetApi;
import com.rvalerio.fgchecker.AppChecker;
@@ -83,11 +83,11 @@ public class MediaProjection {
@Override
public void onForeground(String packageName) {
_packageName = packageName;
/*Log.e("MediaProjection", packageName);
/*QLog.e("MediaProjection", packageName);
if(isLandscape())
Log.e("MediaProjection", "Landscape");
QLog.e("MediaProjection", "Landscape");
else
Log.e("MediaProjection", "Portrait");*/
QLog.e("MediaProjection", "Portrait");*/
}
})
.timeout(1000)

View File

@@ -1,5 +1,5 @@
package org.cagnulen.qdomyoszwift;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
@@ -12,6 +12,6 @@ public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
super.onCreate(savedInstanceState);
this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
activity_ = this;
Log.v(TAG, "onCreate");
QLog.v(TAG, "onCreate");
}
}

View File

@@ -2,7 +2,7 @@ package org.cagnulen.qdomyoszwift;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import java.util.List;
@@ -12,7 +12,7 @@ public class NativeScanCallback extends ScanCallback {
public native void scanError(int code);
@Override
public void onScanResult(int callbackType, ScanResult result) {
Log.i(TAG, "Res " + result);
QLog.i(TAG, "Res " + result);
newScanResult(new ScanRecordResult(result));
}
@@ -24,7 +24,7 @@ public class NativeScanCallback extends ScanCallback {
@Override
public void onScanFailed(int errorCode) {
Log.i(TAG, "onScanFailed "+errorCode);
QLog.i(TAG, "onScanFailed "+errorCode);
scanError(errorCode);
}
}

View File

@@ -17,7 +17,7 @@
package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
@@ -61,7 +61,7 @@ public class PowerChannelController {
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
Log.w(TAG, "Channel was already open");
QLog.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
@@ -92,7 +92,7 @@ public class PowerChannelController {
mAntChannel.open();
mIsOpen = true;
Log.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
QLog.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
@@ -102,7 +102,7 @@ public class PowerChannelController {
}
}
} else {
Log.w(TAG, "No channel available");
QLog.w(TAG, "No channel available");
}
return mIsOpen;
@@ -112,7 +112,7 @@ public class PowerChannelController {
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
Log.e(TAG, logString);
QLog.e(TAG, logString);
}
@@ -142,7 +142,7 @@ public class PowerChannelController {
.append(failureReason);
}
Log.e(TAG, logString.toString());
QLog.e(TAG, logString.toString());
mAntChannel.release();
}
@@ -158,7 +158,7 @@ public class PowerChannelController {
mAntChannel = null;
}
Log.e(TAG, "Channel Closed");
QLog.e(TAG, "Channel Closed");
}
/**
@@ -175,13 +175,13 @@ public class PowerChannelController {
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
Log.e(TAG, "Channel Death");
QLog.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
byte[] payload = new byte[8];
if(carousalTimer == null) {
@@ -189,7 +189,7 @@ public class PowerChannelController {
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Log.d(TAG, "Tx Unsollicited");
QLog.d(TAG, "Tx Unsollicited");
byte[] payload = new byte[8];
eventCount = (eventCount + 1) & 0xFF;
cumulativePower = (cumulativePower + power) & 0xFFFF;
@@ -225,7 +225,7 @@ public class PowerChannelController {
// Rx Data
//updateData(new AcknowledgedDataMessage(antParcel).getPayload());
payload = new AcknowledgedDataMessage(antParcel).getPayload();
Log.d(TAG, "AcknowledgedDataMessage: " + payload);
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) {
payload[0] = (byte) 0x01;
@@ -268,7 +268,7 @@ public class PowerChannelController {
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
Log.d(TAG, "Event Code: " + code);
QLog.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
@@ -320,7 +320,7 @@ public class PowerChannelController {
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
Log.e(TAG, "No Device Found");
QLog.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:

121
src/android/src/QLog.java Normal file
View File

@@ -0,0 +1,121 @@
package org.cagnulen.qdomyoszwift;
import android.util.Log;
/**
* QLog - Wrapper for Android's Log class that redirects logs to Qt's logging system
* Usage: import org.cagnulen.qdomyoszwift.Log;
*/
public class QLog {
public static native void sendToQt(int level, String tag, String message);
static {
try {
// Try to load the native library if needed
System.loadLibrary("qtlogging_native");
} catch (UnsatisfiedLinkError e) {
// Library might be loaded elsewhere, or will be loaded later
Log.w("QLog", "Native library not loaded yet: " + e.getMessage());
}
}
// Debug level methods
public static int d(String tag, String msg) {
sendToQt(3, tag, msg);
return Log.d(tag, msg);
}
public static int d(String tag, String msg, Throwable tr) {
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
return Log.d(tag, msg, tr);
}
// Error level methods
public static int e(String tag, String msg) {
sendToQt(6, tag, msg);
return Log.e(tag, msg);
}
public static int e(String tag, String msg, Throwable tr) {
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
return Log.e(tag, msg, tr);
}
// Info level methods
public static int i(String tag, String msg) {
sendToQt(4, tag, msg);
return Log.i(tag, msg);
}
public static int i(String tag, String msg, Throwable tr) {
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
return Log.i(tag, msg, tr);
}
// Verbose level methods
public static int v(String tag, String msg) {
sendToQt(2, tag, msg);
return Log.v(tag, msg);
}
public static int v(String tag, String msg, Throwable tr) {
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
return Log.v(tag, msg, tr);
}
// Warning level methods
public static int w(String tag, String msg) {
sendToQt(5, tag, msg);
return Log.w(tag, msg);
}
public static int w(String tag, String msg, Throwable tr) {
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
return Log.w(tag, msg, tr);
}
public static int w(String tag, Throwable tr) {
sendToQt(5, tag, Log.getStackTraceString(tr));
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);
return Log.wtf(tag, msg);
}
public static int wtf(String tag, Throwable tr) {
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
return Log.wtf(tag, tr);
}
public static int wtf(String tag, String msg, Throwable tr) {
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
return Log.wtf(tag, msg, tr);
}
// Utility methods
public static String getStackTraceString(Throwable tr) {
return Log.getStackTraceString(tr);
}
public static boolean isLoggable(String tag, int level) {
return Log.isLoggable(tag, level);
}
// Additional utility methods
public static int println(int priority, String tag, String msg) {
sendToQt(priority, tag, msg);
return Log.println(priority, tag, msg);
}
// API Level 28+ (Android 9+) methods
public static RuntimeException getStackTraceElement() {
try {
return (RuntimeException) Log.class.getMethod("getStackTraceElement").invoke(null);
} catch (Exception e) {
return new RuntimeException("QLog: Failed to get stack trace element");
}
}
}

View File

@@ -17,7 +17,7 @@ import android.os.Environment;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.view.View;
import android.widget.Button;
import android.widget.RadioButton;
@@ -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;
Log.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;
Log.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;
Log.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;
Log.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
@@ -96,7 +113,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
@Override
public void receivedData(DeviceConnection devConn, byte[] data, int offset, int length) {
Log.i(LOG_TAG, data.toString());
QLog.i(LOG_TAG, data.toString());
}
@Override
@@ -111,96 +128,132 @@ 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 */
Log.i(LOG_TAG,
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)
{
Log.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) {
Log.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
QLog.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
if(ADBConnected) {
StringBuilder commandBuffer = new StringBuilder();
@@ -212,7 +265,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
/* Send it to the device */
connection.queueCommand(commandBuffer.toString());
} else {
Log.e(LOG_TAG, "sendCommand ADB is not connected!");
QLog.e(LOG_TAG, "sendCommand ADB is not connected!");
}
}

View File

@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
@@ -68,7 +68,7 @@ public class SDMChannelController {
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
Log.w(TAG, "Channel was already open");
QLog.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
@@ -99,7 +99,7 @@ public class SDMChannelController {
mAntChannel.open();
mIsOpen = true;
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
@@ -108,7 +108,7 @@ public class SDMChannelController {
}
}
} else {
Log.w(TAG, "No channel available");
QLog.w(TAG, "No channel available");
}
return mIsOpen;
@@ -117,7 +117,7 @@ public class SDMChannelController {
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
Log.e(TAG, logString);
QLog.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
@@ -146,11 +146,11 @@ public class SDMChannelController {
.append(failureReason);
}
Log.e(TAG, logString.toString());
QLog.e(TAG, logString.toString());
mAntChannel.release();
Log.e(TAG, "ANT Command Failed");
QLog.e(TAG, "ANT Command Failed");
}
public void close() {
@@ -164,7 +164,7 @@ public class SDMChannelController {
mAntChannel = null;
}
Log.e(TAG, "Channel Closed");
QLog.e(TAG, "Channel Closed");
}
/**
@@ -186,20 +186,20 @@ public class SDMChannelController {
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
Log.e(TAG, "Channel Death");
QLog.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Log.d(TAG, "Tx Unsollicited");
QLog.d(TAG, "Tx Unsollicited");
long realtimeMillis = SystemClock.elapsedRealtime();
double speedM_s = speed / 3.6;
long deltaTime = (realtimeMillis - lastTime);
@@ -243,7 +243,7 @@ public class SDMChannelController {
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
Log.d(TAG, "Event Code: " + code);
QLog.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
@@ -278,7 +278,7 @@ public class SDMChannelController {
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
Log.e(TAG, "No Device Found");
QLog.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:

View File

@@ -18,7 +18,7 @@ import android.media.projection.MediaProjectionManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.WindowManager;
@@ -43,7 +43,7 @@ import android.graphics.Rect;
import android.graphics.Point;
import androidx.core.util.Pair;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.os.Build;
public class ScreenCaptureService extends Service {
@@ -137,7 +137,7 @@ public class ScreenCaptureService extends Service {
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * mWidth;
//Log.e(TAG, "Image reviewing");
//QLog.e(TAG, "Image reviewing");
isRunning = true;
@@ -152,7 +152,7 @@ public class ScreenCaptureService extends Service {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
IMAGES_PRODUCED++;
Log.e(TAG, "captured image: " + IMAGES_PRODUCED);
QLog.e(TAG, "captured image: " + IMAGES_PRODUCED);
*/
InputImage inputImage = InputImage.fromBitmap(bitmap, 0);
@@ -169,7 +169,7 @@ public class ScreenCaptureService extends Service {
public void onSuccess(Text result) {
// Task completed successfully
//Log.e(TAG, "Image done!");
//QLog.e(TAG, "Image done!");
String resultText = result.getText();
lastText = resultText;
@@ -204,12 +204,12 @@ public class ScreenCaptureService extends Service {
@Override
public void onFailure(Exception e) {
// Task failed with an exception
//Log.e(TAG, "Image fail");
//QLog.e(TAG, "Image fail");
isRunning = false;
}
});
} else {
//Log.e(TAG, "Image ignored");
//QLog.e(TAG, "Image ignored");
}
}
} catch (Exception e) {
@@ -246,7 +246,7 @@ public class ScreenCaptureService extends Service {
private class MediaProjectionStopCallback extends MediaProjection.Callback {
@Override
public void onStop() {
Log.e(TAG, "stopping projection.");
QLog.e(TAG, "stopping projection.");
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -276,12 +276,12 @@ public class ScreenCaptureService extends Service {
if (!storeDirectory.exists()) {
boolean success = storeDirectory.mkdirs();
if (!success) {
Log.e(TAG, "failed to create file storage directory.");
QLog.e(TAG, "failed to create file storage directory.");
stopSelf();
}
}
} else {
Log.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
QLog.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
stopSelf();
}
@@ -310,7 +310,7 @@ public class ScreenCaptureService extends Service {
startForeground(notification.first, notification.second);
}
} catch (Exception e) {
Log.e("ForegroundService", "Failed to start foreground service", e);
QLog.e("ForegroundService", "Failed to start foreground service", e);
return START_NOT_STICKY;
}
// start projection

View File

@@ -6,7 +6,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.content.IntentFilter;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.app.Service;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -35,7 +35,7 @@ public class Shortcuts {
List<ShortcutInfo> shortcuts = new ArrayList<>();
Log.d("Shortcuts", folder);
QLog.d("Shortcuts", folder);
File[] files = new File(folder, "profiles").listFiles();
if (files != null) {
for (int i = 0; i < files.length && i < 5; i++) { // Limit to 5 shortcuts
@@ -45,7 +45,7 @@ public class Shortcuts {
if (dotIndex > 0) { // Check if there is a dot, indicating an extension exists
fileNameWithoutExtension = fileNameWithoutExtension.substring(0, dotIndex);
}
Log.d("Shortcuts", file.getAbsolutePath());
QLog.d("Shortcuts", file.getAbsolutePath());
Intent intent = new Intent(context, context.getClass());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra("profile_path", file.getAbsolutePath());
@@ -74,7 +74,7 @@ public class Shortcuts {
for (String key : extras.keySet()) {
Object value = extras.get(key);
if("profile_path".equals(key)) {
Log.d("Shortcuts", "profile_path: " + value.toString());
QLog.d("Shortcuts", "profile_path: " + value.toString());
return value.toString();
}
}
@@ -88,7 +88,7 @@ public class Shortcuts {
if (extras != null) {
for (String key : extras.keySet()) {
Object value = extras.get(key);
Log.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
QLog.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
}
}
}

View File

@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
@@ -67,7 +67,7 @@ public class SpeedChannelController {
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
Log.w(TAG, "Channel was already open");
QLog.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
@@ -98,7 +98,7 @@ public class SpeedChannelController {
mAntChannel.open();
mIsOpen = true;
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
@@ -107,7 +107,7 @@ public class SpeedChannelController {
}
}
} else {
Log.w(TAG, "No channel available");
QLog.w(TAG, "No channel available");
}
return mIsOpen;
@@ -116,7 +116,7 @@ public class SpeedChannelController {
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
Log.e(TAG, logString);
QLog.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
@@ -145,11 +145,11 @@ public class SpeedChannelController {
.append(failureReason);
}
Log.e(TAG, logString.toString());
QLog.e(TAG, logString.toString());
mAntChannel.release();
Log.e(TAG, "ANT Command Failed");
QLog.e(TAG, "ANT Command Failed");
}
public void close() {
@@ -163,7 +163,7 @@ public class SpeedChannelController {
mAntChannel = null;
}
Log.e(TAG, "Channel Closed");
QLog.e(TAG, "Channel Closed");
}
/**
@@ -185,20 +185,20 @@ public class SpeedChannelController {
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
Log.e(TAG, "Channel Death");
QLog.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Log.d(TAG, "Tx Unsollicited");
QLog.d(TAG, "Tx Unsollicited");
long realtimeMillis = SystemClock.elapsedRealtime();
if (lastTime != 0) {
@@ -252,7 +252,7 @@ public class SpeedChannelController {
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
Log.d(TAG, "Event Code: " + code);
QLog.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
@@ -296,7 +296,7 @@ public class SpeedChannelController {
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
Log.e(TAG, "No Device Found");
QLog.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:

View File

@@ -8,7 +8,7 @@ import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.app.Service;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -43,12 +43,12 @@ public class Usbserial {
static int lastReadLen = 0;
public static void open(Context context) {
Log.d("QZ","UsbSerial open");
QLog.d("QZ","UsbSerial open");
// Find all available drivers from attached devices.
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
if (availableDrivers.isEmpty()) {
Log.d("QZ","UsbSerial no available drivers");
QLog.d("QZ","UsbSerial no available drivers");
return;
}
@@ -58,7 +58,7 @@ public class Usbserial {
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
RingtoneManager.getRingtone(context, notification).play();
Log.d("QZ","USB permission ...");
QLog.d("QZ","USB permission ...");
final Boolean[] granted = {null};
BroadcastReceiver usbReceiver = new BroadcastReceiver() {
@Override
@@ -85,12 +85,12 @@ public class Usbserial {
// Do something here
}
}
Log.d("QZ","USB permission "+granted[0]);
QLog.d("QZ","USB permission "+granted[0]);
}
UsbDeviceConnection connection = manager.openDevice(driver.getDevice());
if (connection == null) {
Log.d("QZ","UsbSerial no permissions");
QLog.d("QZ","UsbSerial no permissions");
// add UsbManager.requestPermission(driver.getDevice(), ..) handling here
return;
}
@@ -104,14 +104,14 @@ public class Usbserial {
// Do something here
}
Log.d("QZ","UsbSerial port opened");
QLog.d("QZ","UsbSerial port opened");
}
public static void write (byte[] bytes) {
if(port == null)
return;
Log.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
QLog.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
try {
port.write(bytes, 2000);
}
@@ -132,7 +132,7 @@ public class Usbserial {
try {
lastReadLen = port.read(receiveData, 2000);
Log.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
QLog.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
}
catch (IOException e) {
// Do something here

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -33,7 +33,7 @@ public class WearableController {
_intent = new Intent(context, WearableMessageListenerService.class);
// FloatingWindowGFG service is started
context.startService(_intent);
Log.v("WearableController", "started");
QLog.v("WearableController", "started");
}
public static int getHeart() {

View File

@@ -15,7 +15,7 @@ import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.DataMap;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.os.Bundle;
import com.google.android.gms.common.api.Status;
import java.io.InputStream;
@@ -31,7 +31,7 @@ public class WearableMessageListenerService extends Service implements
@Override
public void onCreate() {
super.onCreate();
Log.v("WearableMessageListenerService","onCreate");
QLog.v("WearableMessageListenerService","onCreate");
}
public static int getHeart() {
@@ -55,7 +55,7 @@ public class WearableMessageListenerService extends Service implements
mWearableClient.addListener(this);
Wearable.getDataClient(this).addListener(this);
Log.v("WearableMessageListenerService","onStartCommand");
QLog.v("WearableMessageListenerService","onStartCommand");
// Return START_STICKY to restart the service if it's killed by the system
return START_STICKY;
@@ -65,9 +65,9 @@ public class WearableMessageListenerService extends Service implements
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_DELETED) {
Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
QLog.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
QLog.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
if(event.getDataItem().getUri().getPath().equals("/qz")) {
new Thread(new Runnable() {
@Override
@@ -78,14 +78,14 @@ public class WearableMessageListenerService extends Service implements
heart_rate = DataMap.fromByteArray(result.get(0).getData())
.getInt("heart_rate", 0);
} else {
Log.e(TAG, "Unexpected number of DataItems found.\n"
QLog.e(TAG, "Unexpected number of DataItems found.\n"
+ "\tExpected: 1\n"
+ "\tActual: " + result.getCount());
}
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onHandleIntent: failed to get current alarm state");
} else {
QLog.d(TAG, "onHandleIntent: failed to get current alarm state");
}
Log.d(TAG, "Heart: " + heart_rate);
QLog.d(TAG, "Heart: " + heart_rate);
}
}).start();
}
@@ -96,17 +96,17 @@ public class WearableMessageListenerService extends Service implements
@Override
public void onConnected(Bundle bundle) {
Log.v("WearableMessageListenerService","onConnected");
QLog.v("WearableMessageListenerService","onConnected");
}
@Override
public void onConnectionSuspended(int i) {
Log.v("WearableMessageListenerService","onConnectionSuspended");
QLog.v("WearableMessageListenerService","onConnectionSuspended");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.v("WearableMessageListenerService","onConnectionFailed");
QLog.v("WearableMessageListenerService","onConnectionFailed");
}
@Override
@@ -117,8 +117,8 @@ public class WearableMessageListenerService extends Service implements
// Handle the received message data here
String messageData = new String(data); // Assuming it's a simple string message
Log.v("Wearable", path);
Log.v("Wearable", messageData);
QLog.v("Wearable", path);
QLog.v("Wearable", messageData);
// You can then perform actions or update data in your service based on the received message
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import android.content.BroadcastReceiver;
import android.content.ContextWrapper;
import android.content.IntentFilter;
@@ -44,12 +44,12 @@ public class ZapClickLayer {
}
public static int processCharacteristic(byte[] value) {
Log.d(TAG, "processCharacteristic");
QLog.d(TAG, "processCharacteristic");
return device.processCharacteristic("QZ", value);
}
public static byte[] buildHandshakeStart() {
Log.d(TAG, "buildHandshakeStart");
QLog.d(TAG, "buildHandshakeStart");
return device.buildHandshakeStart();
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;
@@ -49,17 +49,17 @@ public class ZwiftAPI {
// Ora puoi usare 'message' come un oggetto normale
} catch (InvalidProtocolBufferException e) {
// Gestisci l'eccezione se il messaggio non può essere parsato
Log.e(TAG, e.toString());
QLog.e(TAG, e.toString());
}
}
public static float getAltitude() {
Log.d(TAG, "getAltitude " + playerState.getAltitude());
QLog.d(TAG, "getAltitude " + playerState.getAltitude());
return playerState.getAltitude();
}
public static float getDistance() {
Log.d(TAG, "getDistance " + playerState.getDistance());
QLog.d(TAG, "getDistance " + playerState.getDistance());
return playerState.getDistance();
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;

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

@@ -23,7 +23,7 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
public class ShellService extends Service implements DeviceConnectionListener {
@@ -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) {
Log.e("ForegroundService", "Failed to start foreground service", 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

@@ -55,7 +55,7 @@ import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
@@ -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;
/***********************************************************************
@@ -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
@@ -119,25 +122,28 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
public void initializeConnection(){
Log.w(TAG, "initializeConnection start");
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() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
Log.w(TAG, "onBillingSetupFinished");
QLog.w(TAG, "onBillingSetupFinished");
if (billingResult.getResponseCode() == RESULT_OK) {
purchasedProductsQueried(m_nativePointer);
} else {
Log.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
QLog.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
}
}
@Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "Billing service disconnected");
QLog.w(TAG, "Billing service disconnected");
}
});
}
@@ -191,7 +197,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult)
{
Log.d(TAG, "Purchase acknowledged ");
QLog.d(TAG, "Purchase acknowledged ");
}
}
);
@@ -199,9 +205,9 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
public void queryDetails(final String[] productIds) {
Log.d(TAG, "queryDetails: start");
QLog.d(TAG, "queryDetails: start");
int index = 0;
Log.d(TAG, "queryDetails: productIds.length " + productIds.length);
QLog.d(TAG, "queryDetails: productIds.length " + productIds.length);
while (index < productIds.length) {
List<String> productIdList = new ArrayList<>();
for (int i = index; i < Math.min(index + 20, productIds.length); ++i) {
@@ -209,31 +215,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();
Log.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
QLog.d(TAG, "onProductDetailsResponse: responseCode " + responseCode);
if (responseCode != RESULT_OK) {
Log.e(TAG, "queryDetails: Couldn't retrieve sku details.");
QLog.e(TAG, "queryDetails: Couldn't retrieve product details.");
return;
}
if (skuDetailsList == null) {
Log.e(TAG, "queryDetails: No details list in response.");
if (productDetailsList == null || productDetailsList.isEmpty()) {
QLog.e(TAG, "queryDetails: No details list in response.");
return;
}
Log.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 +262,6 @@ public class InAppPurchase implements PurchasesUpdatedListener
e.printStackTrace();
}
}
}
});
@@ -255,33 +273,41 @@ 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) {
Log.e(TAG, "Unable to launch Google Play purchase screen");
QLog.e(TAG, "Unable to launch Google Play purchase screen");
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 productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.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);
}
});
}
@@ -291,7 +317,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() != RESULT_OK) {
Log.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
QLog.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
}
}
};
@@ -312,7 +338,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if (billingResult.getResponseCode() != RESULT_OK){
Log.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
QLog.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
}
}
};
@@ -321,18 +347,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());
}
}

View File

@@ -17,7 +17,7 @@
package org.qtproject.qt.android.purchasing;
import android.text.TextUtils;
import android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
import org.json.JSONException;
import org.json.JSONObject;
@@ -58,7 +58,7 @@ public class Security {
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (signedData == null) {
Log.e(TAG, "data is null");
QLog.e(TAG, "data is null");
return false;
}
@@ -67,7 +67,7 @@ public class Security {
PublicKey key = Security.generatePublicKey(base64PublicKey);
verified = Security.verify(key, signedData, signature);
if (!verified) {
Log.w(TAG, "signature does not match data.");
QLog.w(TAG, "signature does not match data.");
return false;
}
}
@@ -89,10 +89,10 @@ public class Security {
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
QLog.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
QLog.e(TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
@@ -113,18 +113,18 @@ public class Security {
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
Log.e(TAG, "Signature verification failed.");
QLog.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
QLog.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
QLog.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
QLog.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
QLog.e(TAG, "Base64 decoding failed.");
}
return false;
}

42
src/androidqlog.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include <QDebug>
#ifdef Q_OS_ANDROID
#include <QAndroidJniObject>
#include <jni.h>
extern "C" JNIEXPORT void JNICALL
Java_org_cagnulen_qdomyoszwift_QLog_sendToQt(JNIEnv *env, jclass clazz,
jint level, jstring tag, jstring message) {
const char *tagChars = env->GetStringUTFChars(tag, nullptr);
const char *msgChars = env->GetStringUTFChars(message, nullptr);
QString tagStr = QString::fromUtf8(tagChars);
QString msgStr = QString::fromUtf8(msgChars);
// Converti i livelli di log Android in livelli Qt
switch (level) {
case 2: // VERBOSE
qDebug() << "[VERBOSE:" << tagStr << "]" << msgStr;
break;
case 3: // DEBUG
qDebug() << "[DEBUG:" << tagStr << "]" << msgStr;
break;
case 4: // INFO
qInfo() << "[INFO:" << tagStr << "]" << msgStr;
break;
case 5: // WARN
qWarning() << "[WARN:" << tagStr << "]" << msgStr;
break;
case 6: // ERROR
qCritical() << "[ERROR:" << tagStr << "]" << msgStr;
break;
case 7: // ASSERT/WTF
qCritical() << "[ASSERT:" << tagStr << "]" << msgStr;
break;
default:
qDebug() << "[LOG:" << tagStr << "(" << level << ")]" << msgStr;
}
env->ReleaseStringUTFChars(tag, tagChars);
env->ReleaseStringUTFChars(message, msgChars);
}
#endif

View File

@@ -0,0 +1,23 @@
#include "characteristicnotifier0002.h"
#include "bike.h"
#include <QDebug>
#include <QList>
CharacteristicNotifier0002::CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent)
: CharacteristicNotifier(0x0002, parent) {
Bike = bike;
answerList = QList<QByteArray>(); // Initialize empty list
}
void CharacteristicNotifier0002::addAnswer(const QByteArray &newAnswer) {
answerList.append(newAnswer);
}
int CharacteristicNotifier0002::notify(QByteArray &value) {
if(!answerList.isEmpty()) {
value.append(answerList.first()); // Get first item
answerList.removeFirst(); // Remove it from list
return CN_OK;
}
return CN_INVALID;
}

View File

@@ -0,0 +1,19 @@
#ifndef CHARACTERISTICNOTIFIER0002_H
#define CHARACTERISTICNOTIFIER0002_H
#include "bluetoothdevice.h"
#include "characteristicnotifier.h"
#include <QList>
class CharacteristicNotifier0002 : public CharacteristicNotifier {
Q_OBJECT
bluetoothdevice* Bike = nullptr;
QList<QByteArray> answerList;
public:
explicit CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent = nullptr);
int notify(QByteArray &value) override;
void addAnswer(const QByteArray &newAnswer);
};
#endif // CHARACTERISTICNOTIFIER0002_H

View File

@@ -0,0 +1,23 @@
#include "characteristicnotifier0004.h"
#include "bike.h"
#include <QDebug>
#include <QList>
CharacteristicNotifier0004::CharacteristicNotifier0004(bluetoothdevice *bike, QObject *parent)
: CharacteristicNotifier(0x0004, parent) {
Bike = bike;
answerList = QList<QByteArray>();
}
void CharacteristicNotifier0004::addAnswer(const QByteArray &newAnswer) {
answerList.append(newAnswer);
}
int CharacteristicNotifier0004::notify(QByteArray &value) {
if(!answerList.isEmpty()) {
value.append(answerList.first());
answerList.removeFirst();
return CN_OK;
}
return CN_INVALID;
}

View File

@@ -0,0 +1,19 @@
#ifndef CHARACTERISTICNOTIFIER0004_H
#define CHARACTERISTICNOTIFIER0004_H
#include "bluetoothdevice.h"
#include "characteristicnotifier.h"
#include <QList>
class CharacteristicNotifier0004 : public CharacteristicNotifier {
Q_OBJECT
bluetoothdevice* Bike = nullptr;
QList<QByteArray> answerList;
public:
explicit CharacteristicNotifier0004(bluetoothdevice *bike, QObject *parent = nullptr);
int notify(QByteArray &value) override;
void addAnswer(const QByteArray &newAnswer);
};
#endif // CHARACTERISTICNOTIFIER0004_H

View File

@@ -1,6 +1,7 @@
#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) {}
@@ -9,7 +10,8 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::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;
@@ -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

@@ -0,0 +1,316 @@
#include "characteristicwriteprocessor0003.h"
#include <QDebug>
#include "bike.h"
CharacteristicWriteProcessor0003::CharacteristicWriteProcessor0003(double bikeResistanceGain,
int8_t bikeResistanceOffset,
bluetoothdevice *bike,
CharacteristicNotifier0002 *notifier0002,
CharacteristicNotifier0004 *notifier0004,
QObject *parent)
: CharacteristicWriteProcessor(bikeResistanceGain, bikeResistanceOffset, bike, parent), notifier0002(notifier0002), notifier0004(notifier0004) {
}
CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003::decodeVarint(const QByteArray& bytes, int startIndex) {
qint64 result = 0;
int shift = 0;
int bytesRead = 0;
for (int i = startIndex; i < bytes.size(); i++) {
quint8 byte = static_cast<quint8>(bytes.at(i));
result |= static_cast<qint64>(byte & 0x7F) << shift;
bytesRead++;
if ((byte & 0x80) == 0) {
break;
}
shift += 7;
}
return {result, bytesRead};
}
double CharacteristicWriteProcessor0003::currentGear() {
if(zwiftGearReceived)
return currentZwiftGear;
else
return ((bike*)Bike)->gears();
}
qint32 CharacteristicWriteProcessor0003::decodeSInt(const QByteArray& bytes) {
if (static_cast<quint8>(bytes.at(0)) != 0x22) {
qFatal("Invalid field header");
}
int length = static_cast<quint8>(bytes.at(1));
if (static_cast<quint8>(bytes.at(2)) != 0x10) {
qFatal("Invalid inner header");
}
VarintResult varint = decodeVarint(bytes, 3);
qint32 decoded = (varint.value >> 1) ^ -(varint.value & 1);
return decoded;
}
void CharacteristicWriteProcessor0003::handleZwiftGear(const QByteArray &array) {
uint8_t g = 0;
if (array.size() >= 2) {
if ((uint8_t)array[0] == (uint8_t)0xCC && (uint8_t)array[1] == (uint8_t)0x3A) g = 1;
else if ((uint8_t)array[0] == (uint8_t)0xFC && (uint8_t)array[1] == (uint8_t)0x43) g = 2;
else if ((uint8_t)array[0] == (uint8_t)0xAC && (uint8_t)array[1] == (uint8_t)0x4D) g = 3;
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0x56) g = 4;
else if ((uint8_t)array[0] == (uint8_t)0x8C && (uint8_t)array[1] == (uint8_t)0x60) g = 5;
else if ((uint8_t)array[0] == (uint8_t)0xE8 && (uint8_t)array[1] == (uint8_t)0x6B) g = 6;
else if ((uint8_t)array[0] == (uint8_t)0xC4 && (uint8_t)array[1] == (uint8_t)0x77) g = 7;
else if (array.size() >= 3) {
if ((uint8_t)array[0] == (uint8_t)0xA0 && (uint8_t)array[1] == (uint8_t)0x83 && (uint8_t)array[2] == (uint8_t)0x01) g = 8;
else if ((uint8_t)array[0] == (uint8_t)0xA8 && (uint8_t)array[1] == (uint8_t)0x91 && (uint8_t)array[2] == (uint8_t)0x01) g = 9;
else if ((uint8_t)array[0] == (uint8_t)0xB0 && (uint8_t)array[1] == (uint8_t)0x9F && (uint8_t)array[2] == (uint8_t)0x01) g = 10;
else if ((uint8_t)array[0] == (uint8_t)0xB8 && (uint8_t)array[1] == (uint8_t)0xAD && (uint8_t)array[2] == (uint8_t)0x01) g = 11;
else if ((uint8_t)array[0] == (uint8_t)0xC0 && (uint8_t)array[1] == (uint8_t)0xBB && (uint8_t)array[2] == (uint8_t)0x01) g = 12;
else if ((uint8_t)array[0] == (uint8_t)0xF3 && (uint8_t)array[1] == (uint8_t)0xCB && (uint8_t)array[2] == (uint8_t)0x01) g = 13;
else if ((uint8_t)array[0] == (uint8_t)0xA8 && (uint8_t)array[1] == (uint8_t)0xDC && (uint8_t)array[2] == (uint8_t)0x01) g = 14;
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0xEC && (uint8_t)array[2] == (uint8_t)0x01) g = 15;
else if ((uint8_t)array[0] == (uint8_t)0x90 && (uint8_t)array[1] == (uint8_t)0xFD && (uint8_t)array[2] == (uint8_t)0x01) g = 16;
else if ((uint8_t)array[0] == (uint8_t)0xD4 && (uint8_t)array[1] == (uint8_t)0x90 && (uint8_t)array[2] == (uint8_t)0x02) g = 17;
else if ((uint8_t)array[0] == (uint8_t)0x98 && (uint8_t)array[1] == (uint8_t)0xA4 && (uint8_t)array[2] == (uint8_t)0x02) g = 18;
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0xB7 && (uint8_t)array[2] == (uint8_t)0x02) g = 19;
else if ((uint8_t)array[0] == (uint8_t)0x9F && (uint8_t)array[1] == (uint8_t)0xCB && (uint8_t)array[2] == (uint8_t)0x02) g = 20;
else if ((uint8_t)array[0] == (uint8_t)0xD8 && (uint8_t)array[1] == (uint8_t)0xE2 && (uint8_t)array[2] == (uint8_t)0x02) g = 21;
else if ((uint8_t)array[0] == (uint8_t)0x90 && (uint8_t)array[1] == (uint8_t)0xFA && (uint8_t)array[2] == (uint8_t)0x02) g = 22;
else if ((uint8_t)array[0] == (uint8_t)0xC8 && (uint8_t)array[1] == (uint8_t)0x91 && (uint8_t)array[2] == (uint8_t)0x03) g = 23;
else if ((uint8_t)array[0] == (uint8_t)0xF3 && (uint8_t)array[1] == (uint8_t)0xAC && (uint8_t)array[2] == (uint8_t)0x03) g = 24;
else { return; }
}
else { return; }
}
QSettings settings;
if(settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool()) {
int actGear = ((bike*)Bike)->gears();
if (g < actGear) {
for (int i = 0; i < actGear - g; i++) {
((bike*)Bike)->gearDown();
}
} else if (g > actGear) {
for (int i = 0; i < g - actGear; i++) {
((bike*)Bike)->gearUp();
}
}
} else {
if (g < currentZwiftGear) {
for (int i = 0; i < currentZwiftGear - g; ++i) {
((bike*)Bike)->gearDown();
}
} else if (g > currentZwiftGear) {
for (int i = 0; i < g - currentZwiftGear; ++i) {
((bike*)Bike)->gearUp();
}
}
}
currentZwiftGear = g;
zwiftGearReceived = true;
}
QByteArray CharacteristicWriteProcessor0003::encodeHubRidingData(
uint32_t power,
uint32_t cadence,
uint32_t speedX100,
uint32_t hr,
uint32_t unknown1,
uint32_t unknown2
) {
QByteArray buffer;
buffer.append(char(0x03));
auto encodeVarInt32 = [](QByteArray& buf, uint32_t value) {
do {
uint8_t byte = value & 0x7F;
value >>= 7;
if (value) byte |= 0x80;
buf.append(char(byte));
} while (value);
};
encodeVarInt32(buffer, (1 << 3) | 0);
encodeVarInt32(buffer, power);
encodeVarInt32(buffer, (2 << 3) | 0);
encodeVarInt32(buffer, cadence);
encodeVarInt32(buffer, (3 << 3) | 0);
encodeVarInt32(buffer, speedX100);
encodeVarInt32(buffer, (4 << 3) | 0);
encodeVarInt32(buffer, hr);
encodeVarInt32(buffer, (5 << 3) | 0);
encodeVarInt32(buffer, unknown1);
encodeVarInt32(buffer, (6 << 3) | 0);
encodeVarInt32(buffer, unknown2);
return buffer;
}
static uint32_t lastUnknown1 = 836; // Starting value from logs
static uint32_t baseValue = 19000; // Base value from original code
uint32_t CharacteristicWriteProcessor0003::calculateUnknown1(uint16_t power) {
// Increment by a value between 400-800 based on current power
uint32_t increment = 400 + (power * 2);
if (increment > 800) increment = 800;
// Adjust based on power changes
if (power > 0) {
lastUnknown1 += increment;
} else {
// For zero power, larger increments
lastUnknown1 += 600;
}
// Keep within observed range (800-24000)
if (lastUnknown1 > 24000) lastUnknown1 = baseValue;
return lastUnknown1;
}
int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
static const QByteArray expectedHexArray = QByteArray::fromHex("52696465 4F6E02");
static const QByteArray expectedHexArray2 = QByteArray::fromHex("410805");
static const QByteArray expectedHexArray3 = QByteArray::fromHex("00088804");
static const QByteArray expectedHexArray4 = QByteArray::fromHex("042A0A10 C0BB0120");
static const QByteArray expectedHexArray4b = QByteArray::fromHex("042A0A10 A0830120");
static const QByteArray expectedHexArray5 = QByteArray::fromHex("0422");
static const QByteArray expectedHexArray6 = QByteArray::fromHex("042A0410");
static const QByteArray expectedHexArray7 = QByteArray::fromHex("042A0310");
static const QByteArray expectedHexArray8 = QByteArray::fromHex("0418");
static const QByteArray expectedHexArray9 = QByteArray::fromHex("042a0810");
static const QByteArray expectedHexArray10 = QByteArray::fromHex("000800");
QByteArray receivedData = data;
if (receivedData.startsWith(expectedHexArray)) {
qDebug() << "Zwift Play Processor: Initial connection request";
reply = QByteArray::fromHex("2a08031211220f4154582030342c2053545820303400");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("2a0803120d220b524944455f4f4e28322900");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("526964654f6e0200");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray2)) {
qDebug() << "Zwift Play Processor: Device info request";
reply = QByteArray::fromHex("3c080012320a3008800412040500050"
"11a0b4b49434b5220434f524500320f"
"3430323431383030393834000000003a01314204080110140");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray3)) {
qDebug() << "Zwift Play Processor: Status request";
reply = QByteArray::fromHex("3c0888041206 0a0440c0bb01");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray4) || receivedData.startsWith(expectedHexArray4b)) {
qDebug() << "Zwift Play Ask 4";
reply = QByteArray::fromHex("0308001000185920002800309bed01");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("2a08031227222567"
"61705f706172616d735f6368616e6765"
"2832293a2037322c2037322c20302c20"
"36303000");
notifier0002->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray5)) {
qDebug() << "Zwift Play Processor: Slope change request";
double slopefloat = decodeSInt(receivedData.mid(1));
QByteArray slope(2, 0);
slope[0] = quint8(qint16(slopefloat) & 0xFF);
slope[1] = quint8((qint16(slopefloat) >> 8) & 0x00FF);
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(),
QByteArray::fromHex("116901") + slope + QByteArray::fromHex("3228"));
changeSlope(slopefloat, 0 /* TODO */, 0 /* TODO */);
reply = encodeHubRidingData(
Bike->wattsMetric().value(),
Bike->currentCadence().value(),
0,
Bike->wattsMetric().value(),
calculateUnknown1(Bike->wattsMetric().value()),
0
);
notifier0002->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray6)) {
qDebug() << "Zwift Play Ask 6";
reply = QByteArray::fromHex("3c0888041206 0a0440c0bb01");
reply[9] = receivedData[4];
reply[10] = receivedData[5];
reply[11] = receivedData[6];
handleZwiftGear(receivedData.mid(4));
notifier0004->addAnswer(reply);
reply = QByteArray::fromHex("03080010001827e7 20002896143093ed01");
notifier0002->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray7)) {
qDebug() << "Zwift Play Ask 7";
reply = QByteArray::fromHex("03080010001827e7 2000 28 00 3093ed01");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("3c088804120503408c60");
reply[8] = receivedData[4];
reply[9] = receivedData[5];
handleZwiftGear(receivedData.mid(4));
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray8)) {
qDebug() << "Zwift Play Processor: Power request";
VarintResult Power = decodeVarint(receivedData, 2);
QByteArray power(2, 0);
power[0] = quint8(qint16(Power.value) & 0xFF);
power[1] = quint8((qint16(Power.value) >> 8) & 0x00FF);
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(),
QByteArray::fromHex("05") + power);
reply = encodeHubRidingData(
Bike->wattsMetric().value(),
Bike->currentCadence().value(),
0,
Bike->wattsMetric().value(),
calculateUnknown1(Bike->wattsMetric().value()),
0
);
notifier0002->addAnswer(reply);
changePower(Power.value);
}
else if (receivedData.startsWith(expectedHexArray9)) {
qDebug() << "Zwift Play Ask 9";
reply = QByteArray::fromHex("050a08400058b60560fc26");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray10)) {
qDebug() << "Zwift Play Ask 10";
reply = QByteArray::fromHex("3c0800122408800412040004000c1a00320f42412d4534333732443932374244453a00420408011053");
notifier0004->addAnswer(reply);
}
else {
qDebug() << "Zwift Play Processor: Unhandled request:" << receivedData.toHex();
return -1;
}
return 0;
}

View File

@@ -0,0 +1,45 @@
#ifndef CHARACTERISTICWRITEPROCESSOR0003_H
#define CHARACTERISTICWRITEPROCESSOR0003_H
#include "characteristicnotifier0002.h"
#include "characteristicnotifier0004.h"
#include "characteristicwriteprocessor.h"
class CharacteristicWriteProcessor0003 : public CharacteristicWriteProcessor {
Q_OBJECT
CharacteristicNotifier0002 *notifier0002 = nullptr;
CharacteristicNotifier0004 *notifier0004 = nullptr;
public:
explicit CharacteristicWriteProcessor0003(double bikeResistanceGain, int8_t bikeResistanceOffset,
bluetoothdevice *bike, CharacteristicNotifier0002 *notifier0002,
CharacteristicNotifier0004 *notifier0004,
QObject *parent = nullptr);
int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out) override;
static QByteArray encodeHubRidingData(uint32_t power,
uint32_t cadence,
uint32_t speedX100,
uint32_t hr,
uint32_t unknown1,
uint32_t unknown2);
static uint32_t calculateUnknown1(uint16_t power);
void handleZwiftGear(const QByteArray &array);
double currentGear();
private:
struct VarintResult {
qint64 value;
int bytesRead;
};
VarintResult decodeVarint(const QByteArray& bytes, int startIndex);
qint32 decodeSInt(const QByteArray& bytes);
int currentZwiftGear = 8;
bool zwiftGearReceived = false;
signals:
void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
};
#endif // CHARACTERISTICWRITEPROCESSOR0003_H

View File

@@ -0,0 +1,187 @@
#include "android_antbike.h"
#include "virtualdevices/virtualbike.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <QThread>
#include <math.h>
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#include <QLowEnergyConnectionParameters>
#endif
#include <chrono>
using namespace std::chrono_literals;
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
this->noVirtualDevice = noVirtualDevice;
initDone = false;
connect(refresh, &QTimer::timeout, this, &android_antbike::update);
refresh->start(200ms);
}
void android_antbike::update() {
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
#ifdef Q_OS_ANDROID
Heart = KeepAwakeHelper::antObject(true)->callMethod<int>("getHeart", "()I");
Cadence = KeepAwakeHelper::antObject(true)->callMethod<int>("getBikeCadence", "()I");
m_watt = KeepAwakeHelper::antObject(true)->callMethod<int>("getBikePower", "()I");
if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = metric::calculateSpeedFromPower(
m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0),
speedLimit());
} else {
Speed = KeepAwakeHelper::antObject(true)->callMethod<double>("getBikeSpeed", "()D");
}
bool bikeConnected = KeepAwakeHelper::antObject(true)->callMethod<jboolean>("isBikeConnected", "()Z");
qDebug() << QStringLiteral("Current ANT Cadence: ") << QString::number(Cadence.value());
qDebug() << QStringLiteral("Current ANT Speed: ") << QString::number(Speed.value());
qDebug() << QStringLiteral("Current ANT Power: ") << QString::number(m_watt.value());
qDebug() << QStringLiteral("Current ANT Heart: ") << QString::number(Heart.value());
qDebug() << QStringLiteral("ANT Bike Connected: ") << bikeConnected;
#endif
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
if (requestInclination != -100) {
Inclination = requestInclination;
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
requestInclination = -100;
}
update_metrics(false, watts());
Distance += ((Speed.value() / (double)3600.0) /
((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
// ******************************************* virtual bike init *************************************
if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
) {
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
#endif
#endif
if (virtual_device_enabled) {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
connect(virtualBike, &virtualbike::changeInclination, this, &android_antbike::changeInclinationRequested);
connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &android_antbike::ftmsCharacteristicChanged);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
if (!firstStateChanged)
emit connectedAndDiscovered();
firstStateChanged = 1;
// ********************************************************************************************************
if (!noVirtualDevice) {
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
Heart = (uint8_t)KeepAwakeHelper::heart();
debug("Current Heart: " + QString::number(Heart.value()));
}
#endif
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
}
if (Heart.value()) {
static double lastKcal = 0;
if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator
lastKcal = abs(KCal.value());
KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal;
}
if (requestResistance != -1 && requestResistance != currentResistance().value()) {
Resistance = requestResistance;
m_pelotonResistance = requestResistance;
}
}
void android_antbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QByteArray b = newValue;
qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' ');
}
void android_antbike::changeInclinationRequested(double grade, double percentage) {
if (percentage < 0)
percentage = 0;
changeInclination(grade, percentage);
}
uint16_t android_antbike::wattsFromResistance(double resistance) {
return _ergTable.estimateWattage(Cadence.value(), 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;
}*/
}
uint16_t android_antbike::watts() { return m_watt.value(); }
bool android_antbike::connected() { return true; }

View File

@@ -0,0 +1,81 @@
#ifndef ANDROID_ANTBIKE_H
#define ANDROID_ANTBIKE_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QDateTime>
#include <QObject>
#include <QString>
#include "devices/bike.h"
#include "ergtable.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class android_antbike : public bike {
Q_OBJECT
public:
android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice);
bool connected() override;
uint16_t watts() override;
resistance_t maxResistance() override { return 100; }
resistance_t resistanceFromPowerRequest(uint16_t power) override;
private:
QTimer *refresh;
uint8_t sec1Update = 0;
QByteArray lastPacket;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
QDateTime lastGoodCadence = QDateTime::currentDateTime();
uint8_t firstStateChanged = 0;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
bool noHeartService = false;
bool noVirtualDevice = false;
uint16_t oldLastCrankEventTime = 0;
uint16_t oldCrankRevs = 0;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif
uint16_t wattsFromResistance(double resistance);
signals:
void disconnected();
void debug(QString string);
private slots:
void changeInclinationRequested(double grade, double percentage);
void update();
void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
};
#endif // ANDROID_ANTBIKE_H

View File

@@ -57,7 +57,13 @@ void apexbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
}
if (!disable_log) {
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +

View File

@@ -62,17 +62,28 @@ void bike::changePower(int32_t power) {
return;
}
requestPower = power; // used by some bikes that have ERG mode builtin
QSettings settings;
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"));
double erg_filter_upper =
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
double erg_filter_lower =
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
requestPower = power; // used by some bikes that have ERG mode builtin
if(power_sensor && ergModeSupported && m_rawWatt.value() > 0 && m_watt.value() > 0 && fabs(requestPower - m_watt.average5s()) < qMax(erg_filter_upper, erg_filter_lower)) {
qDebug() << "applying delta watt to power request m_rawWatt" << m_rawWatt.average5s() << "watt" << m_watt.average5s() << "req" << requestPower;
// the concept here is to trying to add or decrease the delta from the power sensor
requestPower += (requestPower - m_watt.average5s());
}
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
.toBool();
// bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); //Not used
// anywhere in code
double erg_filter_upper =
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
double erg_filter_lower =
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
double deltaDown = wattsMetric().value() - ((double)power);
double deltaUp = ((double)power) - wattsMetric().value();
qDebug() << QStringLiteral("filter ") + QString::number(deltaUp) + " " + QString::number(deltaDown) + " " +
@@ -176,6 +187,7 @@ void bike::clearStats() {
m_jouls.clear(true);
elevationAcc = 0;
m_watt.clear(false);
m_rawWatt.clear(false);
WeightLoss.clear(false);
RequestedPelotonResistance.clear(false);
@@ -203,6 +215,7 @@ void bike::setPaused(bool p) {
Heart.setPaused(p);
m_jouls.setPaused(p);
m_watt.setPaused(p);
m_rawWatt.setPaused(p);
WeightLoss.setPaused(p);
m_pelotonResistance.setPaused(p);
Cadence.setPaused(p);
@@ -228,6 +241,7 @@ void bike::setLap() {
Heart.setLap(false);
m_jouls.setLap(true);
m_watt.setLap(false);
m_rawWatt.setLap(false);
WeightLoss.setLap(false);
WattKg.setLap(false);

View File

@@ -109,6 +109,8 @@ void bluetooth::finished() {
QSettings settings;
bool antbike =
settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool();
bool android_antbike =
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
QString nordictrack_2950_ip =
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
@@ -121,7 +123,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) {
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || antbike || android_antbike) {
// faking a bluetooth device
qDebug() << "faking a bluetooth device for nordictrack_2950_ip";
deviceDiscovered(QBluetoothDeviceInfo());
@@ -462,6 +464,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
settings.value(QZSettings::proformtreadmillip, QZSettings::default_proformtreadmillip).toString();
bool antbike_setting =
settings.value(QZSettings::antbike, QZSettings::default_antbike).toBool();
bool android_antbike_setting =
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
QString nordictrack_2950_ip =
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
@@ -817,6 +821,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(antBike);
} else if (android_antbike_setting && !android_antBike) {
this->stopDiscovery();
android_antBike = new android_antbike(noWriteResistance, noHeartService, false);
emit deviceConnected(b);
connect(android_antBike, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
// connect(cscBike, SIGNAL(disconnected()), this, SLOT(restart()));
connect(android_antBike, &android_antbike::debug, this, &bluetooth::debug);
// connect(this, SIGNAL(searchingStop()), cscBike, SLOT(searchingStop())); //NOTE: Commented due to #358
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(android_antBike);
} else if (!proformtreadmillip.isEmpty() && !proformWifiTreadmill) {
this->stopDiscovery();
proformWifiTreadmill = new proformwifitreadmill(noWriteResistance, noHeartService, bikeResistanceOffset,
@@ -1334,7 +1351,27 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(soleF80);
} else if ((b.name().toUpper().startsWith(QStringLiteral("LF")) && b.name().length() == 18) &&
} 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);
this->stopDiscovery();
@@ -1396,21 +1433,27 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
b.name().toUpper().startsWith(QStringLiteral("BFX_T9_")) ||
(b.name().toUpper().startsWith("3G PRO ")) ||
(b.name().toUpper().startsWith("3G ELITE ")) ||
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("LJJ-")) || // LJJ-02351A
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
(b.name().toUpper().startsWith("SCHWINN 810")) ||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
(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 T80-")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
@@ -1516,6 +1559,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
#endif
} 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"))) &&
@@ -1534,7 +1580,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// connect(tacxneo2Bike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
tacxneo2Bike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(tacxneo2Bike);
} else if ((b.name().toUpper().startsWith("INDOORCYCLE")) &&
} else if (((b.name().toUpper().startsWith("INDOORCYCLE")) ||
((b.name().toUpper().startsWith("MAGNUS ")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) &&
!cycleopsphantomBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -1580,7 +1627,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
(b.name().toUpper().startsWith("3G CARDIO ")) ||
(b.name().toUpper().startsWith("ZWIFT HUB")) || (b.name().toUpper().startsWith("MAGNUS ")) ||
(b.name().toUpper().startsWith("ZWIFT HUB")) ||
((b.name().toUpper().startsWith("MAGNUS ")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().toUpper().startsWith("HAMMER ") && !power_as_bike && !saris_trainer) || // HAMMER 64123
(b.name().toUpper().startsWith("FLXCY-")) || // Pro FlexBike
(b.name().toUpper().startsWith("QB-WC01")) || // Nexgim QB-C01 smart bike
@@ -1626,13 +1674,13 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("FELVON V2")) ||
(b.name().toUpper().startsWith("JUSTO")) ||
(b.name().toUpper().startsWith("MYCYCLE ")) ||
(b.name().toUpper().startsWith("T2 ")) ||
(b.name().toUpper().startsWith("DR") && b.name().length() == 2) ||
(b.name().toUpper().startsWith("T2 ")) ||
(b.name().toUpper().startsWith("RC-MAX-")) ||
(b.name().toUpper().startsWith("TPS-SPBIKE-2.0")) ||
(b.name().toUpper().startsWith("NEO BIKE SMART")) ||
(b.name().toUpper().startsWith("ZDRIVE")) ||
(b.name().toUpper().startsWith("TUNTURI E60-")) ||
(b.name().toUpper().startsWith("TUNTURI F40-")) ||
(b.name().toUpper().startsWith("JFBK5.0")) ||
(b.name().toUpper().startsWith("NEO 3M ")) ||
(b.name().toUpper().startsWith("JFBK7.0")) ||
@@ -1640,14 +1688,24 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("POOBOO")) ||
(b.name().toUpper().startsWith("ZYCLE ZPRO")) ||
(b.name().toUpper().startsWith("SM720I")) ||
(b.name().toUpper().startsWith("H9115 LYON")) ||
(b.name().toUpper().startsWith("AVANTI")) ||
(b.name().toUpper().startsWith("T300P_")) ||
(b.name().toUpper().startsWith("T200_")) ||
(b.name().toUpper().startsWith("BZ9110 ")) ||
(b.name().toUpper().startsWith("CFC") && b.name().length() == 14) || // CFC31231004349
(b.name().toUpper().startsWith("TITAN 7000")) ||
(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("MRK-S26S-")) ||
(b.name().toUpper().startsWith("ROBX")) ||
(b.name().toUpper().startsWith("XCX-")) ||
(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(QStringLiteral("FIT-BK-"))) ||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
@@ -1658,7 +1716,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(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) {
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);
@@ -1733,7 +1792,10 @@ 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("ASSIOMA")) &&
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
@@ -1766,7 +1828,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
smartrowRower->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(smartrowRower);
} else if ((b.name().toUpper().startsWith(QStringLiteral("PM5")) &&
!b.name().toUpper().endsWith(QStringLiteral("ROW"))) &&
b.name().toUpper().endsWith(QStringLiteral("SKI"))) &&
!concept2Skierg && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -1790,6 +1852,8 @@ 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("ROWER ")) ||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) ||
!b.name().compare(ftms_rower, Qt::CaseInsensitive) ||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) &&
@@ -1807,13 +1871,28 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// connect(ftmsRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
ftmsRower->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(ftmsRower);
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-EC-SPT"))) &&
!echelonStairclimber && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
echelonStairclimber = new echelonstairclimber(this->pollDeviceTime, noConsole, noHeartService);
// stateFileRead();
emit deviceConnected(b);
connect(echelonStairclimber, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
// connect(echelonRower, SIGNAL(disconnected()), this, SLOT(restart())); connect(echelonStride,
connect(echelonStairclimber, &echelonstairclimber::debug, this, &bluetooth::debug);
connect(echelonStairclimber, &echelonstairclimber::speedChanged, this, &bluetooth::speedChanged);
connect(echelonStairclimber, &echelonstairclimber::inclinationChanged, this, &bluetooth::inclinationChanged);
echelonStairclimber->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(echelonStairclimber);
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) ||
b.name().toUpper().startsWith(QLatin1String("STRIDE")) ||
b.name().toUpper().startsWith(QLatin1String("STRIDE6S-")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-SD-SPT"))) &&
!echelonStride && filter) {
!echelonStride && !echelonStairclimber && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
echelonStride = new echelonstride(this->pollDeviceTime, noConsole, noHeartService);
@@ -1903,7 +1982,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// connect(echelonRower, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
echelonRower->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(echelonRower);
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride && !ftmsBike &&
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride && !echelonStairclimber && !ftmsBike &&
!echelonConnectSport && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -2149,6 +2228,21 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// SLOT(inclinationChanged(double)));
pafersTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(pafersTreadmill);
} else if (b.name().toUpper().startsWith(QStringLiteral("MOXY6")) && !moxy5Sensor) {
// *** SPECIAL DEVICE ****
moxy5Sensor = new moxy5sensor();
connect(moxy5Sensor, &moxy5sensor::debug, this, &bluetooth::debug);
moxy5Sensor->deviceDiscovered(b);
} else if (b.name().toUpper().startsWith(QStringLiteral("CORE ")) && !coreSensor) {
// *** SPECIAL 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();
@@ -2321,10 +2415,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
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("IBIKING+")) ||
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"))) ||
@@ -2759,6 +2853,8 @@ void bluetooth::connectedAndDiscovered() {
connect(powerSensor, &stagesbike::debug, this, &bluetooth::debug);
connect(powerSensor, &bluetoothdevice::powerChanged, this->device(), &bluetoothdevice::powerSensor);
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
&bluetoothdevice::cadenceSensor);
powerSensor->deviceDiscovered(b);
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
powerSensorRun = new strydrunpowersensor(false, false, true);
@@ -2880,6 +2976,25 @@ 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) {
eliteSquareController = new elitesquarecontroller(this->device());
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
connect(eliteSquareController, &elitesquarecontroller::debug, this, &bluetooth::debug);
connect(eliteSquareController, &elitesquarecontroller::plus, (bike*)this->device(), &bike::gearUp);
connect(eliteSquareController, &elitesquarecontroller::minus, (bike*)this->device(), &bike::gearDown);
eliteSquareController->deviceDiscovered(b);
if(homeform::singleton())
homeform::singleton()->setToastRequested("Elite Square Connected!");
break;
}
}
}
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
@@ -2921,12 +3036,13 @@ void bluetooth::connectedAndDiscovered() {
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
"activity", "()Landroid/app/Activity;");
KeepAwakeHelper::antObject(true)->callMethod<void>(
"antStart", "(Landroid/app/Activity;ZZZZ)V", activity.object<jobject>(),
"antStart", "(Landroid/app/Activity;ZZZZZ)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);
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool());
}
if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) {
@@ -3074,6 +3190,11 @@ void bluetooth::restart() {
delete horizonTreadmill;
horizonTreadmill = nullptr;
}
if (speraXTreadmill) {
delete speraXTreadmill;
speraXTreadmill = nullptr;
}
if (lifefitnessTreadmill) {
delete lifefitnessTreadmill;
@@ -3179,6 +3300,11 @@ void bluetooth::restart() {
delete antBike;
antBike = nullptr;
}
if (android_antBike) {
delete android_antBike;
android_antBike = nullptr;
}
if (proformTelnetBike) {
delete proformTelnetBike;
@@ -3335,6 +3461,11 @@ void bluetooth::restart() {
delete echelonStride;
echelonStride = nullptr;
}
if (echelonStairclimber) {
delete echelonStairclimber;
echelonStairclimber = nullptr;
}
if (octaneTreadmill) {
delete octaneTreadmill;
@@ -3669,6 +3800,8 @@ bluetoothdevice *bluetooth::device() {
return proformWifiTreadmill;
} else if (antBike) {
return antBike;
} else if (android_antBike) {
return android_antBike;
} else if (nordictrackifitadbTreadmill) {
return nordictrackifitadbTreadmill;
} else if (nordictrackifitadbBike) {
@@ -3727,6 +3860,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
@@ -3751,6 +3886,8 @@ bluetoothdevice *bluetooth::device() {
return echelonRower;
} else if (echelonStride) {
return echelonStride;
} else if (echelonStairclimber) {
return echelonStairclimber;
} else if (octaneTreadmill) {
return octaneTreadmill;
} else if (ziproTreadmill) {

View File

@@ -21,7 +21,9 @@
#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"
#include "devices/bhfitnesselliptical/bhfitnesselliptical.h"
#include "devices/bkoolbike/bkoolbike.h"
@@ -29,6 +31,7 @@
#include "devices/bowflext216treadmill/bowflext216treadmill.h"
#include "devices/bowflextreadmill/bowflextreadmill.h"
#include "devices/chronobike/chronobike.h"
#include "devices/coresensor/coresensor.h"
#ifndef Q_OS_IOS
#include "devices/computrainerbike/computrainerbike.h"
#include "devices/csaferower/csaferower.h"
@@ -46,8 +49,10 @@
#include "devices/echelonconnectsport/echelonconnectsport.h"
#include "devices/echelonrower/echelonrower.h"
#include "devices/echelonstairclimber/echelonstairclimber.h"
#include "devices/eliteariafan/eliteariafan.h"
#include "devices/eliterizer/eliterizer.h"
#include "devices/elitesquarecontroller/elitesquarecontroller.h"
#include "devices/elitesterzosmart/elitesterzosmart.h"
#include "devices/eslinkertreadmill/eslinkertreadmill.h"
#include "devices/fakebike/fakebike.h"
@@ -77,6 +82,7 @@
#include "devices/m3ibike/m3ibike.h"
#include "devices/mcfbike/mcfbike.h"
#include "devices/mepanelbike/mepanelbike.h"
#include "devices/moxy5sensor/moxy5sensor.h"
#include "devices/nautilusbike/nautilusbike.h"
#include "devices/nautiluselliptical/nautiluselliptical.h"
#include "devices/nautilustreadmill/nautilustreadmill.h"
@@ -173,11 +179,13 @@ class bluetooth : public QObject, public SignalHandler {
QFile *debugCommsLog = nullptr;
QBluetoothDeviceDiscoveryAgent *discoveryAgent = nullptr;
antbike *antBike = nullptr;
android_antbike *android_antBike = nullptr;
apexbike *apexBike = nullptr;
bkoolbike *bkoolBike = nullptr;
bhfitnesselliptical *bhFitnessElliptical = nullptr;
bowflextreadmill *bowflexTreadmill = nullptr;
bowflext216treadmill *bowflexT216Treadmill = nullptr;
coresensor* coreSensor = nullptr;
crossrope *crossRope = nullptr;
fitshowtreadmill *fitshowTreadmill = nullptr;
focustreadmill *focusTreadmill = nullptr;
@@ -199,6 +207,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;
@@ -208,6 +217,7 @@ class bluetooth : public QObject, public SignalHandler {
echelonconnectsport *echelonConnectSport = nullptr;
yesoulbike *yesoulBike = nullptr;
flywheelbike *flywheelBike = nullptr;
moxy5sensor *moxy5Sensor = nullptr;
nordictrackelliptical *nordictrackElliptical = nullptr;
nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr;
nordictrackifitadbbike *nordictrackifitadbBike = nullptr;
@@ -256,6 +266,7 @@ class bluetooth : public QObject, public SignalHandler {
ftmsrower *ftmsRower = nullptr;
smartrowrower *smartrowRower = nullptr;
echelonstride *echelonStride = nullptr;
echelonstairclimber *echelonStairclimber = nullptr;
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
lifespantreadmill *lifespanTreadmill = nullptr;
keepbike *keepBike = nullptr;
@@ -292,6 +303,7 @@ class bluetooth : public QObject, public SignalHandler {
QList<zwiftclickremote* > zwiftPlayDevice;
zwiftclickremote* zwiftClickRemote = nullptr;
sramaxscontroller* sramAXSController = nullptr;
elitesquarecontroller* eliteSquareController = nullptr;
QString filterDevice = QLatin1String("");
bool testResistance = false;

View File

@@ -108,6 +108,7 @@ 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::jouls() { return m_jouls; }
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
@@ -132,6 +133,9 @@ 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::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();
@@ -281,11 +285,15 @@ void bluetoothdevice::clearStats() {
m_jouls.clear(true);
elevationAcc = 0;
m_watt.clear(false);
m_rawWatt.clear(false);
WeightLoss.clear(false);
WattKg.clear(false);
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);
}
}
@@ -301,11 +309,15 @@ void bluetoothdevice::setPaused(bool p) {
Heart.setPaused(p);
m_jouls.setPaused(p);
m_watt.setPaused(p);
m_rawWatt.setPaused(p);
WeightLoss.setPaused(p);
WattKg.setPaused(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);
}
}
@@ -320,11 +332,15 @@ void bluetoothdevice::setLap() {
Heart.setLap(false);
m_jouls.setLap(true);
m_watt.setLap(false);
m_rawWatt.setLap(false);
WeightLoss.setLap(false);
WattKg.setLap(false);
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);
}
}
@@ -475,9 +491,9 @@ void bluetoothdevice::setGPXFile(QString filename) {
}
}
void bluetoothdevice::setHeartZone(double hz) {
void bluetoothdevice::setHeartZone(double hz) {
HeartZone = hz;
if(isPaused() == false) {
if(isPaused() == false && currentHeart().value() > 0) {
hz = hz - 1;
if(hz >= maxHeartZone() ) {
hrZonesSeconds[maxHeartZone() - 1].setValue(hrZonesSeconds[maxHeartZone() - 1].value() + 1);
@@ -489,9 +505,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

@@ -146,6 +146,11 @@ class bluetoothdevice : public QObject {
*/
virtual QTime lapElapsedTime();
/**
* @brief lapOdometer Gets the distance elapsed on the current lap.
*/
virtual double lapOdometer();
/**
* @brief connected Gets a value to indicate if the device is connected.
*/
@@ -365,6 +370,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.
@@ -399,6 +422,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)
@@ -413,7 +443,7 @@ class bluetoothdevice : public QObject {
*/
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE };
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
/**
@@ -438,6 +468,11 @@ class bluetoothdevice : public QObject {
*/
virtual resistance_t maxResistance();
// Metrics for core temperature data
metric CoreBodyTemperature; // Core body temperature in °C or °F
metric SkinTemperature; // Skin temperature in °C or °F
metric HeatStrainIndex; // Heat Strain Index (0-25.4, scaled by 10)
public Q_SLOTS:
virtual void start();
virtual void stop(bool pause);
@@ -445,6 +480,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);
@@ -581,9 +619,14 @@ class bluetoothdevice : public QObject {
metric elevationAcc;
/**
* @brief m_watt Metric to get and set the power expended in the session. Unit: watts
* @brief m_watt Metric to get and set the power read from the trainer or from the power sensor Unit: watts
*/
metric m_watt;
/**
* @brief m_rawWatt Metric to get and set the power from the trainer only. Unit: watts
*/
metric m_rawWatt;
/**
* @brief WattKg Metric to get and set the watt kg for the session (what's this?). Unit: watt kg
@@ -663,6 +706,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
*/
@@ -689,6 +737,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

@@ -199,10 +199,20 @@ void bowflext216treadmill::characteristicChanged(const QLowEnergyCharacteristic
else if ((newValue.length() != 12) && bowflex_t8j == true)
return;
if (bowflex_t6 == true && characteristic.uuid() != QBluetoothUuid(QStringLiteral("a46a4a80-9803-11e3-8f3c-0002a5d5c51b")))
if(bowflex_T128 && characteristic.uuid() != QBluetoothUuid(QStringLiteral("a46a4a80-9803-11e3-8f3c-0002a5d5c51b"))) {
double incline = GetInclinationFromPacket(value);
qDebug() << QStringLiteral("Current incline: ") << incline;
if (Inclination.value() != incline) {
emit inclinationChanged(0.0, incline);
}
Inclination = incline;
return;
}
if ((bowflex_t6 == true || bowflex_T128 == true) && characteristic.uuid() != QBluetoothUuid(QStringLiteral("a46a4a80-9803-11e3-8f3c-0002a5d5c51b")))
return;
double speed = GetSpeedFromPacket(value);
double speed = GetSpeedFromPacket(value);
double incline = GetInclinationFromPacket(value);
// double kcal = GetKcalFromPacket(value);
// double distance = GetDistanceFromPacket(value);
@@ -226,10 +236,13 @@ void bowflext216treadmill::characteristicChanged(const QLowEnergyCharacteristic
emit speedChanged(speed);
}
Speed = speed;
if (Inclination.value() != incline) {
emit inclinationChanged(0.0, incline);
if(bowflex_T128 == false) {
if (Inclination.value() != incline) {
emit inclinationChanged(0.0, incline);
}
Inclination = incline;
}
Inclination = incline;
// KCal = kcal;
// Distance = distance;
@@ -271,6 +284,10 @@ double bowflext216treadmill::GetSpeedFromPacket(const QByteArray &packet) {
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(3)) + ((uint16_t)((uint8_t)packet.at(4)) << 8);
double data = (double)convertedData / 100.0f;
return data * 1.60934;
} else if(bowflex_T128) {
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 * 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;
@@ -294,6 +311,12 @@ double bowflext216treadmill::GetDistanceFromPacket(const QByteArray &packet) {
}
double bowflext216treadmill::GetInclinationFromPacket(const QByteArray &packet) {
if (bowflex_T128) {
uint16_t convertedData = packet.at(7);
double data = convertedData;
return data;
}
if (bowflex_t8j) {
uint16_t convertedData = packet.at(6);
double data = convertedData;
@@ -447,6 +470,12 @@ void bowflext216treadmill::deviceDiscovered(const QBluetoothDeviceInfo &device)
device.address().toString() + ')');
{
bluetoothDevice = device;
if(bluetoothDevice.name().toUpper().startsWith("BOWFLEX T128")) {
qDebug() << "BOWFLEX T128 workaround found!";
bowflex_T128 = true;
}
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &bowflext216treadmill::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &bowflext216treadmill::serviceScanDone);

View File

@@ -75,6 +75,7 @@ class bowflext216treadmill : public treadmill {
bool bowflex_t6 = false;
bool bowflex_btx116 = false;
bool bowflex_t8j = false;
bool bowflex_T128 = false;
Q_SIGNALS:
void disconnected();

View File

@@ -0,0 +1,456 @@
#include "coresensor.h"
#include "homeform.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QEventLoop>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <QThread>
coresensor::coresensor() :
m_sensorHeartRate(0),
m_dataQuality(0),
m_heartRateState(0),
m_isCelsius(true) {
}
coresensor::~coresensor() {
disconnectBluetooth();
}
bool coresensor::connected() {
if (!m_control) {
return false;
}
return m_control->state() == QLowEnergyController::DiscoveredState;
}
metric coresensor::currentHeart() {
// If we have a valid value from the sensor, create a metric with that value
if (m_sensorHeartRate > 0) {
metric heartMetric;
heartMetric.setValue(m_sensorHeartRate);
return heartMetric;
}
// Otherwise return the value from the base class
return bluetoothdevice::currentHeart();
}
void coresensor::update() {
// This method can be used for periodic tasks or calculations
QSettings settings;
// Future implementation if needed
}
void coresensor::disconnectBluetooth() {
qDebug() << QStringLiteral("coresensor::disconnect") << m_control;
if (m_control) {
m_control->disconnectFromDevice();
}
}
void coresensor::deviceDiscovered(const QBluetoothDeviceInfo &device) {
QSettings settings;
qDebug() << QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
device.address().toString() + ')';
if(homeform::singleton())
homeform::singleton()->setToastRequested(device.name() + QStringLiteral(" connected!"));
// We might filter the device name if needed
// For example: if (device.name().contains("CORE") || device.name().contains("CoreTemp"))
{
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &coresensor::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &coresensor::serviceScanDone);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, &coresensor::handleError);
connect(m_control, &QLowEnergyController::stateChanged, this, &coresensor::controllerStateChanged);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
qDebug() << QStringLiteral("Cannot connect to remote device.");
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
qDebug() << QStringLiteral("Controller connected. Search services...");
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
qDebug() << QStringLiteral("LowEnergy controller disconnected");
emit disconnected();
});
// Connect
m_control->connectToDevice();
return;
}
}
void coresensor::serviceDiscovered(const QBluetoothUuid &gatt) {
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
}
void coresensor::serviceScanDone() {
qDebug() << QStringLiteral("serviceScanDone");
auto services_list = m_control->services();
for (const QBluetoothUuid &s : qAsConst(services_list)) {
qDebug() << QStringLiteral("coresensor services ") << s.toString();
// Look for the specific CORE sensor service
if (s == QBluetoothUuid(QString(CORE_SERVICE_UUID))) {
QBluetoothUuid coreServiceId(QString(CORE_SERVICE_UUID));
m_coreService = m_control->createServiceObject(coreServiceId);
connect(m_coreService, &QLowEnergyService::stateChanged, this,
&coresensor::serviceStateChanged);
connect(m_coreService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &coresensor::handleServiceError);
m_coreService->discoverDetails();
return;
}
}
// If we reach here, we didn't find the CORE service
qDebug() << QStringLiteral("CORE service not found!");
}
void coresensor::serviceStateChanged(QLowEnergyService::ServiceState state) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
if (state == QLowEnergyService::ServiceDiscovered) {
auto characteristics_list = m_coreService->characteristics();
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
qDebug() << QStringLiteral("characteristic ") + c.uuid().toString();
}
// Find the Core Body Temperature characteristic
m_coreTemperatureCharacteristic =
m_coreService->characteristic(QBluetoothUuid(QString(CORE_TEMPERATURE_CHAR_UUID)));
if (!m_coreTemperatureCharacteristic.isValid()) {
qDebug() << "Core Body Temperature characteristic not valid";
return;
}
// Find the Control Point characteristic
m_coreControlPointCharacteristic =
m_coreService->characteristic(QBluetoothUuid(QString(CORE_CONTROL_POINT_UUID)));
if (!m_coreControlPointCharacteristic.isValid()) {
qDebug() << "Core Control Point characteristic not valid";
// We can continue without control point if only reading temperature
}
// Set up notifications and handle characteristic changes
connect(m_coreService, &QLowEnergyService::characteristicChanged, this,
&coresensor::characteristicChanged);
connect(m_coreService, &QLowEnergyService::characteristicWritten, this,
&coresensor::characteristicWritten);
connect(m_coreService, &QLowEnergyService::descriptorWritten, this,
&coresensor::descriptorWritten);
// Enable notifications for Core Body Temperature characteristic
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
m_coreService->writeDescriptor(
m_coreTemperatureCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration),
descriptor);
// For Control Point, we need to enable indications (not notifications)
if (m_coreControlPointCharacteristic.isValid()) {
QByteArray cpDescriptor;
cpDescriptor.append((char)0x02); // 0x02 for indications (different from notifications)
cpDescriptor.append((char)0x00);
m_coreService->writeDescriptor(
m_coreControlPointCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration),
cpDescriptor);
}
}
}
void coresensor::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
emit packetReceived();
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
// Handle Core Body Temperature characteristic updates
if (characteristic.uuid() == QBluetoothUuid(QString(CORE_TEMPERATURE_CHAR_UUID))) {
if (newValue.length() < 1) {
qDebug() << "Received invalid data packet (too short)";
return;
}
// Parse flags field (first byte)
uint8_t flags = newValue.at(0);
bool hasSkinTemp = (flags & 0x01);
bool hasCoreReserved = (flags & 0x02);
bool hasQualityState = (flags & 0x04);
bool isFahrenheit = (flags & 0x08);
bool hasHeartRate = (flags & 0x10);
bool hasHeatStrainIndex = (flags & 0x20);
// Update temperature unit flag
bool newIsCelsius = !isFahrenheit;
if (m_isCelsius != newIsCelsius) {
m_isCelsius = newIsCelsius;
emit isCelsiusChanged(m_isCelsius);
}
int offset = 1; // Start after flags byte
// Core body temperature (mandatory)
if (newValue.length() >= (offset + 2)) {
int16_t tempRaw = (int16_t)((uint8_t)newValue[offset] | ((uint8_t)newValue[offset+1] << 8));
// Special value for 'Data not available'
if (tempRaw == 0x7FFF) {
qDebug() << "Core body temperature: Data not available";
} else {
double temp = tempRaw / 100.0; // Convert from 0.01°C/F to °C/F
CoreBodyTemperature.setValue(temp);
emit coreBodyTemperatureChanged(temp);
}
offset += 2;
}
// Skin temperature (optional)
if (hasSkinTemp && newValue.length() >= (offset + 2)) {
int16_t tempRaw = (int16_t)((uint8_t)newValue[offset] | ((uint8_t)newValue[offset+1] << 8));
double temp = tempRaw / 100.0; // Convert from 0.01°C/F to °C/F
SkinTemperature.setValue(temp);
emit skinTemperatureChanged(temp);
offset += 2;
}
// Core reserved (optional)
if (hasCoreReserved && newValue.length() >= (offset + 2)) {
int16_t tempRaw = (int16_t)((uint8_t)newValue[offset] | ((uint8_t)newValue[offset+1] << 8));
double temp = tempRaw / 100.0; // Convert from 0.01°C/F to °C/F
emit coreReservedChanged(temp);
offset += 2;
}
// Quality and state (optional)
if (hasQualityState && newValue.length() >= (offset + 1)) {
uint8_t qualityState = newValue.at(offset);
int newDataQuality = qualityState & 0x0F; // Lower 4 bits, actually using only 3 bits
int newHeartRateState = (qualityState >> 4) & 0x03; // Bits 4-5
if (m_dataQuality != newDataQuality) {
m_dataQuality = newDataQuality;
emit dataQualityChanged(m_dataQuality);
}
if (m_heartRateState != newHeartRateState) {
m_heartRateState = newHeartRateState;
emit heartRateStateChanged(m_heartRateState);
}
offset += 1;
}
// Heart rate (optional)
if (hasHeartRate && newValue.length() >= (offset + 1)) {
uint8_t hr = newValue.at(offset);
if (m_sensorHeartRate != hr) {
m_sensorHeartRate = hr;
emit heartRateChanged(m_sensorHeartRate);
// Forward heart rate to base class if it's a valid value
if (hr > 0) {
bluetoothdevice::heartRate(hr);
}
}
offset += 1;
}
// Heat Strain Index (optional)
if (hasHeatStrainIndex && newValue.length() >= (offset + 1)) {
uint8_t hsiRaw = newValue.at(offset);
double hsi = hsiRaw / 10.0; // Convert from 0.1 units to actual value
if (HeatStrainIndex.value() != hsi) {
HeatStrainIndex = hsi;
emit heatStrainIndexChanged(HeatStrainIndex.value());
}
offset += 1;
}
// Log data for debugging
QString logMsg = QStringLiteral("CORE Sensor: ");
logMsg += QStringLiteral("Temperature: ") + QString::number(CoreBodyTemperature.value(), 'f', 2) +
(m_isCelsius ? "°C" : "°F");
if (hasSkinTemp) {
logMsg += QStringLiteral(" Skin: ") + QString::number(SkinTemperature.value(), 'f', 2) +
(m_isCelsius ? "°C" : "°F");
}
if (hasHeartRate) {
logMsg += QStringLiteral(" HR: ") + QString::number(m_sensorHeartRate) + QStringLiteral(" BPM");
}
if (hasHeatStrainIndex) {
logMsg += QStringLiteral(" HSI: ") + QString::number(HeatStrainIndex.value(), 'f', 1);
}
qDebug() << logMsg;
} else if (characteristic.uuid() == QBluetoothUuid(QString(CORE_CONTROL_POINT_UUID))) {
// Handle Control Point responses
handleControlPointResponse(newValue);
}
}
void coresensor::handleControlPointResponse(const QByteArray &value) {
if (value.length() < 3) {
qDebug() << "Invalid Control Point response (too short)";
return;
}
// First byte should be 0x80 (response code)
if ((uint8_t)value.at(0) != 0x80) {
qDebug() << "Invalid Control Point response (expected 0x80 response code)";
return;
}
uint8_t requestOpCode = value.at(1);
uint8_t resultCode = value.at(2);
QString result;
switch (resultCode) {
case 0x01: result = "Success"; break;
case 0x02: result = "Op Code not supported"; break;
case 0x03: result = "Invalid Parameter"; break;
case 0x04: result = "Operation Failed"; break;
default: result = "Unknown result code"; break;
}
qDebug() << "Control Point response for OpCode" << QString("0x%1").arg(requestOpCode, 2, 16, QChar('0'))
<< "Result:" << result;
m_operationInProgress = false;
}
void coresensor::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
Q_UNUSED(characteristic);
qDebug() << QStringLiteral("characteristicWritten ") + newValue.toHex(' ');
}
void coresensor::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
qDebug() << QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ');
}
void coresensor::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;
if (state == QLowEnergyController::UnconnectedState && m_control) {
qDebug() << QStringLiteral("trying to connect back again...");
m_control->connectToDevice();
}
}
void coresensor::handleError(QLowEnergyController::Error err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
qDebug() << QStringLiteral("coresensor::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString();
}
void coresensor::handleServiceError(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
qDebug() << QStringLiteral("coresensor::serviceError ") + QString::fromLocal8Bit(metaEnum.valueToKey(err));
}
bool coresensor::setExternalHeartRate(uint8_t bpm) {
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
return false;
}
QByteArray request;
request.append((char)0x13); // OpCode for external heart rate
request.append((char)bpm); // Heart rate value
m_operationInProgress = true;
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
return true;
}
bool coresensor::disableExternalHeartRate() {
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
return false;
}
QByteArray request;
request.append((char)0x13); // OpCode for external heart rate
// No additional data means disable external heart rate
m_operationInProgress = true;
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
return true;
}
bool coresensor::scanForBLEHeartRateMonitors() {
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
return false;
}
QByteArray request;
request.append((char)0x0D); // OpCode for scan BLE HRMs
request.append((char)0xFF); // Parameter for starting scan
m_operationInProgress = true;
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
return true;
}
bool coresensor::scanForANTHeartRateMonitors() {
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
return false;
}
QByteArray request;
request.append((char)0x0A); // OpCode for scan ANT+ HRMs
request.append((char)0xFF); // Parameter for starting scan
m_operationInProgress = true;
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
return true;
}
int coresensor::getNumberOfPairedHeartRateMonitors() {
// This is a simple implementation that requests the number of paired BLE HRMs
// In a complete implementation, we would make an async request and store the result
if (!m_coreControlPointCharacteristic.isValid() || m_operationInProgress) {
return -1; // Error condition
}
// Request total number of paired BLE heart rate monitors
QByteArray request;
request.append((char)0x08); // OpCode for get total number of paired BLE HRMs
m_operationInProgress = true;
m_coreService->writeCharacteristic(m_coreControlPointCharacteristic, request, QLowEnergyService::WriteWithResponse);
// Note: in a real implementation, we would need to handle the response asynchronously
// and store the result for a getter to access. This would require additional state tracking.
return 0; // Placeholder value - in reality, would return cached value from last request
}

View File

@@ -0,0 +1,196 @@
#ifndef CORESENSOR_H
#define CORESENSOR_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QObject>
#include <QTime>
#include <QDateTime>
#include "bluetoothdevice.h"
// UUID costanti per CORE Sensor secondo le specifiche
#define CORE_SERVICE_UUID "00002100-5B1E-4347-B07C-97B514DAE121"
#define CORE_TEMPERATURE_CHAR_UUID "00002101-5B1E-4347-B07C-97B514DAE121"
#define CORE_CONTROL_POINT_UUID "00002102-5B1E-4347-B07C-97B514DAE121"
// Flag bit definitions for Core Body Temperature characteristic
namespace CoreSensorFlags {
const uint8_t SKIN_TEMPERATURE_PRESENT = 0x01;
const uint8_t CORE_RESERVED_PRESENT = 0x02;
const uint8_t QUALITY_STATE_PRESENT = 0x04;
const uint8_t TEMPERATURE_UNIT_FAHRENHEIT = 0x08;
const uint8_t HEART_RATE_PRESENT = 0x10;
const uint8_t HEAT_STRAIN_INDEX_PRESENT = 0x20;
}
// Data quality values
namespace CoreSensorQuality {
const uint8_t INVALID = 0;
const uint8_t POOR = 1;
const uint8_t FAIR = 2;
const uint8_t GOOD = 3;
const uint8_t EXCELLENT = 4;
const uint8_t NOT_AVAILABLE = 7;
}
// Heart rate state values
namespace CoreSensorHRState {
const uint8_t NOT_SUPPORTED = 0;
const uint8_t SUPPORTED_NOT_RECEIVING = 1;
const uint8_t SUPPORTED_RECEIVING = 2;
const uint8_t NOT_AVAILABLE = 3;
}
// Control Point OpCodes
namespace CoreControlPoint {
// ANT+ Related OpCodes
const uint8_t CLEAR_ANT_PAIRED_HRM_LIST = 0x01;
const uint8_t ADD_ANT_HRM = 0x02;
const uint8_t REMOVE_ANT_HRM = 0x03;
const uint8_t GET_TOTAL_ANT_PAIRED_HRMS = 0x04;
const uint8_t GET_ANT_HRM_ID_AT_INDEX = 0x05;
const uint8_t SCAN_ANT_HRMS = 0x0A;
const uint8_t GET_TOTAL_SCANNED_ANT_HRMS = 0x0B;
const uint8_t GET_SCANNED_ANT_HRM_ID = 0x0C;
// BLE Related OpCodes
const uint8_t ADD_BLE_HRM = 0x06;
const uint8_t REMOVE_BLE_HRM = 0x07;
const uint8_t GET_TOTAL_BLE_PAIRED_HRMS = 0x08;
const uint8_t GET_BLE_HRM_NAME_STATE = 0x09;
const uint8_t SCAN_BLE_HRMS = 0x0D;
const uint8_t GET_TOTAL_SCANNED_BLE_HRMS = 0x0E;
const uint8_t GET_SCANNED_BLE_HRM_NAME = 0x0F;
const uint8_t GET_SCANNED_BLE_HRM_MAC = 0x10;
const uint8_t CLEAR_BLE_PAIRED_HRM_LIST = 0x11;
const uint8_t GET_BLE_HRM_MAC_STATE = 0x12;
// External heart rate input
const uint8_t EXTERNAL_HEART_RATE = 0x13;
// Response code (sent by sensor)
const uint8_t RESPONSE_CODE = 0x80;
// Scan parameters
const uint8_t START_SCAN = 0xFF;
const uint8_t START_PROXIMITY_PAIRING = 0xFE;
// Result codes
const uint8_t SUCCESS = 0x01;
const uint8_t OP_CODE_NOT_SUPPORTED = 0x02;
const uint8_t INVALID_PARAMETER = 0x03;
const uint8_t OPERATION_FAILED = 0x04;
}
// Struttura per la qualità e lo stato
struct QualityAndState {
enum DataQuality {
INVALID = 0,
POOR = 1,
FAIR = 2,
GOOD = 3,
EXCELLENT = 4,
NOT_AVAILABLE = 7
};
enum HeartRateState {
NOT_SUPPORTED = 0,
SUPPORTED_NOT_RECEIVING = 1,
SUPPORTED_RECEIVING = 2,
NOT_AVAILABLE_STATE = 3
};
DataQuality quality;
HeartRateState hrState;
};
class coresensor : public bluetoothdevice {
Q_OBJECT
public:
coresensor();
~coresensor();
bool connected() override;
// Override from bluetoothdevice
metric currentHeart() override;
private:
QLowEnergyService *m_coreService = nullptr;
QLowEnergyCharacteristic m_coreTemperatureCharacteristic;
QLowEnergyCharacteristic m_coreControlPointCharacteristic;
// Other data from Core sensor
uint8_t m_sensorHeartRate; // Heart rate in BPM
int m_dataQuality; // Quality level (0=Invalid, 1=Poor, 2=Fair, 3=Good, 4=Excellent)
int m_heartRateState; // Heart rate state
bool m_isCelsius; // Temperature unit (true=°C, false=°F)
// Control point state tracking
bool m_operationInProgress = false;
signals:
void disconnected();
void debug(QString string);
void packetReceived();
// Signals for property change notifications
void coreBodyTemperatureChanged(double value);
void skinTemperatureChanged(double value);
void coreReservedChanged(double value);
void heartRateChanged(double value);
void heatStrainIndexChanged(double value);
void dataQualityChanged(int value);
void heartRateStateChanged(int value);
void isCelsiusChanged(bool value);
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void disconnectBluetooth();
// Control point operations
bool setExternalHeartRate(uint8_t bpm);
bool disableExternalHeartRate();
// Heart rate monitor operations - these depend on implementation
bool scanForBLEHeartRateMonitors();
bool scanForANTHeartRateMonitors();
int getNumberOfPairedHeartRateMonitors();
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void serviceStateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void handleError(QLowEnergyController::Error err);
void handleServiceError(QLowEnergyService::ServiceError err);
// Handle control point responses
void handleControlPointResponse(const QByteArray &value);
};
#endif // CORESENSOR_H

View File

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

@@ -1,4 +1,5 @@
#include "devices/dircon/dirconmanager.h"
#include "devices/bike.h"
#include <QNetworkInterface>
#include <QSettings>
#include <chrono>
@@ -8,13 +9,17 @@ using namespace std::chrono_literals;
#define DM_MACHINE_TYPE_BIKE 1
#define DM_MACHINE_TYPE_TREADMILL 2
#define DM_SERV_OP(OP, P1, P2, P3) \
#define DM_SERV_OP(OP, P1, P2, P3, ZWIFT_ENABLED) \
OP(FITNESS_MACHINE_CYCLE, 0x1826, WAHOO_KICKR, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x1826, WAHOO_TREADMILL, P1, P2, P3) \
OP(CYCLING_POWER, 0x1818, WAHOO_KICKR, P1, P2, P3) \
OP(CYCLING_SPEED_AND_CADENCE, 0x1816, WAHOO_KICKR, P1, P2, P3) \
OP(RUNNING_SPEED_AND_CADENCE, 0x1814, WAHOO_TREADMILL, P1, P2, P3) \
OP(HEART_RATE, 0x180D, WAHOO_BLUEHR, P1, P2, P3)
OP(HEART_RATE, 0x180D, WAHOO_BLUEHR, P1, P2, P3) \
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, ZWIFT_PLAY_ENUM_VALUE, WAHOO_KICKR, P1, P2, P3)
#define ZWIFT_ENABLED_OP(OP, DESC, UUID, MACHINE, P1, P2, P3) OP(DESC, UUID, MACHINE, P1, P2, P3)
#define ZWIFT_DISABLED_OP(OP, DESC, UUID, MACHINE, P1, P2, P3)
#define DM_MACHINE_OP(OP, P1, P2, P3) \
OP(WAHOO_KICKR, "Wahoo KICKR $uuid_hex$", DM_MACHINE_TYPE_TREADMILL | DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
@@ -22,6 +27,7 @@ using namespace std::chrono_literals;
OP(WAHOO_RPM_SPEED, "Wahoo SPEED $uuid_hex$", DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
OP(WAHOO_TREADMILL, "Wahoo TREAD $uuid_hex$", DM_MACHINE_TYPE_TREADMILL, P1, P2, P3)
#define DP_PROCESS_WRITE_0003() (zwift_play_emulator ? writeP0003 : 0)
#define DP_PROCESS_WRITE_2AD9() writeP2AD9
#define DP_PROCESS_WRITE_2AD9T() writeP2AD9
#define DP_PROCESS_WRITE_E005() writePE005
@@ -32,7 +38,7 @@ using namespace std::chrono_literals;
#define DM_BT(A) QByteArrayLiteral(A)
#define DM_CHAR_OP(OP, P1, P2, P3) \
#define DM_CHAR_OP(OP, P1, P2, P3, ZWIFT_ENABLED) \
OP(FITNESS_MACHINE_CYCLE, 0x2ACC, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x83\x14\x00\x00\x0C\xE0\x00\x00"), \
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"), \
@@ -71,7 +77,15 @@ using namespace std::chrono_literals;
P3) \
OP(RUNNING_SPEED_AND_CADENCE, 0x2A53, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, \
P3) \
OP(HEART_RATE, 0x2A37, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3)
OP(HEART_RATE, 0x2A37, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, 0x0003, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_0003, P1, P2, P3) \
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, 0x0002, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
ZWIFT_ENABLED(OP, ZWIFT_PLAY_EMULATOR, 0x0004, DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x02\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3)
#define ZWIFT_CHAR_ENABLED_OP(OP, DESC, UUID, TYPE, READV, WRITEP, P1, P2, P3) \
OP(DESC, UUID, TYPE, READV, WRITEP, P1, P2, P3)
#define ZWIFT_CHAR_DISABLED_OP(OP, DESC, UUID, TYPE, READV, WRITEP, P1, P2, P3)
#define DM_MACHINE_ENUM_OP(DESC, NAME, TYPE, P1, P2, P3) DM_MACHINE_##DESC,
@@ -79,15 +93,22 @@ enum { DM_MACHINE_OP(DM_MACHINE_ENUM_OP, 0, 0, 0) };
#define DM_SERV_ENUMU_OP(DESC, UUID, MACHINE, P1, P2, P3) DM_SERV_U_##DESC = UUID,
enum { DM_SERV_OP(DM_SERV_ENUMU_OP, 0, 0, 0) };
enum {
DM_SERV_OP(DM_SERV_ENUMU_OP, 0, 0, 0, ZWIFT_ENABLED_OP)
};
#define DM_SERV_ENUMM_OP(DESC, UUID, MACHINE, P1, P2, P3) DM_SERV_M_##DESC = DM_MACHINE_##MACHINE,
enum { DM_SERV_OP(DM_SERV_ENUMM_OP, 0, 0, 0) };
enum {
DM_SERV_OP(DM_SERV_ENUMM_OP, 0, 0, 0, ZWIFT_ENABLED_OP)
};
#define DM_SERV_ENUMI_OP(DESC, UUID, MACHINE, P1, P2, P3) DM_SERV_I_##DESC,
enum { DM_SERV_OP(DM_SERV_ENUMI_OP, 0, 0, 0) DM_SERV_I_NUM };
enum {
DM_SERV_OP(DM_SERV_ENUMI_OP, 0, 0, 0, ZWIFT_ENABLED_OP)
DM_SERV_I_NUM
};
#define DM_CHAR_INIT_OP(SDESC, UUID, TYPE, READV, WRITEP, P1, P2, P3) \
if (P1.size() <= DM_SERV_I_##SDESC) { \
@@ -150,35 +171,55 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
QSettings settings;
DirconProcessorService *service;
QList<DirconProcessorService *> services, proc_services;
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
bt = Bike;
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
: DM_MACHINE_TYPE_BIKE;
qDebug() << "Building Dircom Manager";
uint16_t server_base_port =
settings.value(QZSettings::dircon_server_base_port, QZSettings::default_dircon_server_base_port).toUInt();
bool bike_wheel_revs = settings.value(QZSettings::bike_wheel_revs, QZSettings::default_bike_wheel_revs).toBool();
bool zwift_play_emulator = settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool();
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_BUILD_OP, Bike, 0, 0)
writeP2AD9 = new CharacteristicWriteProcessor2AD9(bikeResistanceGain, bikeResistanceOffset, Bike, notif2AD9, this);
writePE005 = new CharacteristicWriteProcessorE005(bikeResistanceGain, bikeResistanceOffset, Bike, this);
DM_CHAR_OP(DM_CHAR_INIT_OP, services, service, 0)
writeP0003 = new CharacteristicWriteProcessor0003(bikeResistanceGain, bikeResistanceOffset, Bike, notif0002, notif0004, this);
if (zwift_play_emulator) {
DM_CHAR_OP(DM_CHAR_INIT_OP, services, service, 0, ZWIFT_CHAR_ENABLED_OP);
} else {
DM_CHAR_OP(DM_CHAR_INIT_OP, services, service, 0, ZWIFT_CHAR_DISABLED_OP);
}
connect(writeP2AD9, SIGNAL(changeInclination(double, double)), this, SIGNAL(changeInclination(double, double)));
connect(writeP2AD9, SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)), this,
SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)));
connect(writePE005, SIGNAL(changeInclination(double, double)), this, SIGNAL(changeInclination(double, double)));
connect(writePE005, SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)), this,
SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)));
if (zwift_play_emulator && writeP0003) {
connect(writeP0003, SIGNAL(changeInclination(double, double)), this, SIGNAL(changeInclination(double, double)));
connect(writeP0003, SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)), this,
SIGNAL(ftmsCharacteristicChanged(QLowEnergyCharacteristic, QByteArray)));
}
QObject::connect(&bikeTimer, &QTimer::timeout, this, &DirconManager::bikeProvider);
QString mac = getMacAddress();
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
if (settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
bikeTimer.start(100ms);
if (zwift_play_emulator || settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
bikeTimer.start(50ms);
else
bikeTimer.start(1s);
}
#define DM_CHAR_NOTIF_NOTIF1_OP(UUID, P1, P2, P3) \
QByteArray all##UUID; \
int rv##UUID = notif##UUID->notify(all##UUID);
int rv##UUID = notif##UUID ? notif##UUID->notify(all##UUID) : 0;
#define DM_CHAR_NOTIF_NOTIF2_OP(UUID, P1, P2, P3) \
if (rv##UUID == CN_OK) \
@@ -186,5 +227,16 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
void DirconManager::bikeProvider() {
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_NOTIF1_OP, 0, 0, 0)
foreach (DirconProcessor *processor, processors) { DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_NOTIF2_OP, processor, 0, 0) }
foreach (DirconProcessor *processor, processors) {
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_NOTIF2_OP, processor, 0, 0)
}
}
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)
return ((bike*)bt)->gears();
return 0;
}

View File

@@ -12,6 +12,7 @@
#include "characteristics/characteristicnotifier2ad9.h"
#include "characteristics/characteristicwriteprocessor2ad9.h"
#include "characteristics/characteristicwriteprocessore005.h"
#include "characteristics/characteristicwriteprocessor0003.h"
#include "devices/dircon/dirconpacket.h"
#include "devices/dircon/dirconprocessor.h"
#include <QObject>
@@ -20,7 +21,8 @@
OP(2AD2, P1, P2, P3) \
OP(2A63, P1, P2, P3) \
OP(2A37, P1, P2, P3) OP(2A5B, P1, P2, P3) OP(2A53, P1, P2, P3) OP(2ACD, P1, P2, P3) OP(2ACC, P1, P2, P3) \
OP(2AD9, P1, P2, P3)
OP(2AD9, P1, P2, P3) \
OP(0002, P1, P2, P3) OP(0004, P1, P2, P3)
#define DM_CHAR_NOTIF_DEFINE_OP(UUID, P1, P2, P3) CharacteristicNotifier##UUID *notif##UUID = 0;
@@ -28,12 +30,15 @@ class DirconManager : public QObject {
Q_OBJECT
QTimer bikeTimer;
CharacteristicWriteProcessor2AD9 *writeP2AD9 = 0;
CharacteristicWriteProcessor0003 *writeP0003 = 0;
CharacteristicWriteProcessorE005 *writePE005 = 0;
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_DEFINE_OP, 0, 0, 0)
QList<DirconProcessor *> processors;
static QString getMacAddress();
bluetoothdevice* bt = 0;
public:
double currentGear();
explicit DirconManager(bluetoothdevice *t, int8_t bikeResistanceOffset = 4, double bikeResistanceGain = 1.0,
QObject *parent = nullptr);
private slots:

View File

@@ -171,22 +171,40 @@ QByteArray DirconPacket::encode(int last_seq_number) {
this->Length = this->uuids.size() * 16;
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
foreach (u, this->uuids) {
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
if(u >= 1 && u <= 4) {
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
} else {
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
}
}
}
} 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));
u = this->uuid;
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
foreach (u, this->uuids) {
if(u >= 1 && u <= 4) {
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
} else {
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
}
foreach (u, this->uuids) {
if(u >= 1 && u <= 4) {
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
} else {
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
}
byteout.append(this->additional_data.at(i++));
}
} else if (((this->Identifier == DPKT_MSGID_READ_CHARACTERISTIC ||
@@ -196,9 +214,15 @@ QByteArray DirconPacket::encode(int last_seq_number) {
this->Length = 16;
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
u = this->uuid;
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
if(u >= 1 && u <= 4) {
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
} else {
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
}
} else if (this->Identifier == DPKT_MSGID_WRITE_CHARACTERISTIC ||
this->Identifier == DPKT_MSGID_UNSOLICITED_CHARACTERISTIC_NOTIFICATION ||
(this->Identifier == DPKT_MSGID_READ_CHARACTERISTIC && !this->isRequest) ||
@@ -206,9 +230,15 @@ QByteArray DirconPacket::encode(int last_seq_number) {
this->Length = 16 + this->additional_data.size();
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
u = this->uuid;
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
if(u >= 1 && u <= 4) {
this->uuid_bytes_zwift_play[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes_zwift_play[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes_zwift_play, 16);
} else {
this->uuid_bytes[DPKT_POS_SH8] = (quint8)(u >> 8);
this->uuid_bytes[DPKT_POS_SH0] = (quint8)(u);
byteout.append((char *)this->uuid_bytes, 16);
}
byteout.append(this->additional_data);
}
return byteout;

View File

@@ -4,10 +4,20 @@
#include <QList>
#include <QObject>
#define ZWIFT_PLAY_ENUM_VALUE 0x0001
#define ZWIFT_PLAY_UUID_STRING "00000001-19CA-4651-86E5-FA29DCDD09D1"
#define ZWIFT_PLAY_CHAR1_ENUM_VALUE 0x0002
#define ZWIFT_PLAY_CHAR2_ENUM_VALUE 0x0003
#define ZWIFT_PLAY_CHAR3_ENUM_VALUE 0x0004
#define ZWIFT_PLAY_CHAR1_UUID_STRING "00000002-19CA-4651-86E5-FA29DCDD09D1"
#define ZWIFT_PLAY_CHAR2_UUID_STRING "00000003-19CA-4651-86E5-FA29DCDD09D1"
#define ZWIFT_PLAY_CHAR3_UUID_STRING "00000004-19CA-4651-86E5-FA29DCDD09D1"
#define DPKT_MESSAGE_HEADER_LENGTH 6
#define DPKT_CHAR_PROP_FLAG_READ 0x01
#define DPKT_CHAR_PROP_FLAG_WRITE 0x02
#define DPKT_CHAR_PROP_FLAG_NOTIFY 0x04
#define DPKT_CHAR_PROP_FLAG_INDICATE 0x08
#define DPKT_MSGID_ERROR 0xFF
#define DPKT_MSGID_DISCOVER_SERVICES 0x01
#define DPKT_MSGID_DISCOVER_CHARACTERISTICS 0x02
@@ -51,6 +61,8 @@ class DirconPacket {
private:
quint8 uuid_bytes[16] = {0x00, 0x00, 0x18, 0x26, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB};
quint8 uuid_bytes_zwift_play[16] = {0x00, 0x00, 0x00, 0x04, 0x19, 0xCA, 0x46, 0x51,
0x86, 0xE5, 0xFA, 0x29, 0xDC, 0xDD, 0x09, 0xD1};
bool checkIsRequest(int last_seq_number);
};

View File

@@ -14,6 +14,16 @@ DirconProcessor::DirconProcessor(const QList<DirconProcessorService *> &my_servi
DirconProcessor::~DirconProcessor() {}
QString DirconProcessor::convertUUIDFromUINT16ToString (quint16 uuid) {
if(uuid == ZWIFT_PLAY_CHAR1_ENUM_VALUE)
return ZWIFT_PLAY_CHAR1_UUID_STRING;
if(uuid == ZWIFT_PLAY_CHAR2_ENUM_VALUE)
return ZWIFT_PLAY_CHAR2_UUID_STRING;
if(uuid == ZWIFT_PLAY_CHAR3_ENUM_VALUE)
return ZWIFT_PLAY_CHAR3_UUID_STRING;
return "";
}
bool DirconProcessor::initServer() {
qDebug() << "Initializing dircon tcp server for" << serverName;
if (!server) {
@@ -55,10 +65,16 @@ void DirconProcessor::initAdvertising() {
mdnsService.addAttribute(QByteArrayLiteral("serial-number"), serialN.toUtf8());
QString ble_uuids;
int i = 0;
foreach (DirconProcessorService *service, services)
ble_uuids += QString(QStringLiteral(DP_BASE_UUID))
.replace("u", QString(QStringLiteral("%1")).arg(service->uuid, 4, 16, QLatin1Char('0'))) +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
foreach (DirconProcessorService *service, services) {
if(service->uuid == ZWIFT_PLAY_ENUM_VALUE) {
ble_uuids += ZWIFT_PLAY_UUID_STRING +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
} else {
ble_uuids += QString(QStringLiteral(DP_BASE_UUID))
.replace("u", QString(QStringLiteral("%1")).arg(service->uuid, 4, 16, QLatin1Char('0'))) +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
}
}
mdnsService.addAttribute(QByteArrayLiteral("ble-service-uuids"), ble_uuids.toUtf8());
mdnsService.setPort(serverPort);
mdnsProvider->update(mdnsService);
@@ -209,7 +225,7 @@ bool DirconProcessor::sendCharacteristicNotification(quint16 uuid, const QByteAr
pkt.uuid = uuid;
for (QHash<QTcpSocket *, DirconProcessorClient *>::iterator i = clientsMap.begin(); i != clientsMap.end(); ++i) {
client = i.value();
if (client->char_notify.indexOf(uuid) >= 0 || !settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool()) {
/*if (client->char_notify.indexOf(uuid) >= 0 || !settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool())*/ {
socket = i.key();
rvs = socket->write(pkt.encode(0)) < 0;
if (rvs)
@@ -252,7 +268,7 @@ void DirconProcessor::tcpDataAvailable() {
qDebug() << "Sending resp for uuid" << serverName << ":" << resp;
if (resp.Identifier != DPKT_MSGID_ERROR) {
QByteArray byteout = resp.encode(pkt.SequenceNumber);
if (byteout.size())
if (byteout.size() && client && client->sock)
client->sock->write(byteout);
}
} else if (rembuf >= 0) {
@@ -261,7 +277,7 @@ void DirconProcessor::tcpDataAvailable() {
resp.ResponseCode = DPKT_RESPCODE_UNEXPECTED_ERROR;
resp.Identifier = pkt.Identifier;
QByteArray byteout = resp.encode(pkt.SequenceNumber);
if (byteout.size())
if (byteout.size() && client && client->sock)
client->sock->write(byteout);
} else
break;

View File

@@ -84,6 +84,7 @@ class DirconProcessor : public QObject {
bool initServer();
void initAdvertising();
DirconPacket processPacket(DirconProcessorClient *client, const DirconPacket &pkt);
QString convertUUIDFromUINT16ToString (quint16 uuid);
public:
~DirconProcessor();

View File

@@ -353,20 +353,16 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
and speed status return;*/
double speed = GetSpeedFromPacket(value);
double kcal;
if (!settings.value(QZSettings::kcal_ignore_builtin, QZSettings::default_kcal_ignore_builtin).toBool())
kcal = GetKcalFromPacket(value);
KCal = GetKcalFromPacket(value);
else {
if (watts())
kcal =
KCal.value() + ((((0.048 * ((double)watts()) + 1.19) *
KCal += ((((0.048 * ((double)watts()) + 1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
now)))); //(( (0.048* Output in watts +1.19) * body
// weight in kg * 3.5) / 200 ) / 60
else
kcal = KCal.value();
}
if(!firstCharacteristicChanged) {
@@ -439,7 +435,7 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
qDebug() << QStringLiteral("Current cadence: ") + QString::number(Cadence.value());
qDebug() << QStringLiteral("Current resistance: ") + QString::number(Resistance.value());
qDebug() << QStringLiteral("Current heart: ") + QString::number(Heart.value());
qDebug() << QStringLiteral("Current KCal: ") + QString::number(kcal);
qDebug() << QStringLiteral("Current KCal: ") + QString::number(KCal.value());
qDebug() << QStringLiteral("Current Distance: ") + QString::number(distance);
qDebug() << QStringLiteral("Current CrankRevs: ") + QString::number(CrankRevs);
qDebug() << QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime);
@@ -456,7 +452,6 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
KCal = kcal;
firstCharacteristicChanged = false;
}

View File

@@ -330,6 +330,7 @@ void domyostreadmill::update() {
if (requestInclination != -100 && lastInclinationChanged.secsTo(QDateTime::currentDateTime()) > inclination_delay_seconds) {
lastInclinationChanged = QDateTime::currentDateTime();
// only 0.5 steps ara available
requestInclination = treadmillInclinationOverrideReverse(requestInclination);
requestInclination = qRound(requestInclination * 2.0) / 2.0;
inc = requestInclination;
requestInclination = -100;
@@ -641,7 +642,7 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
and speed status return;*/
double speed = GetSpeedFromPacket(value);
double incline = GetInclinationFromPacket(value);
double incline = treadmillInclinationOverride(GetInclinationFromPacket(value));
double kcal = GetKcalFromPacket(value);
double distance = GetDistanceFromPacket(value);
bool disable_hr_frommachinery =

View File

@@ -37,28 +37,37 @@ echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartSer
void echelonconnectsport::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
bool wait_for_response) {
QSettings settings;
bool useNativeIOS = false;
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
#endif
QEventLoop loop;
QTimer timeout;
// if there are some crash here, maybe it's better to use 2 separate event for the characteristicChanged.
// one for the resistance changed event (spontaneous), and one for the other ones.
if (wait_for_response) {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
} else {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
}
if (!useNativeIOS) {
// if there are some crash here, maybe it's better to use 2 separate event for the characteristicChanged.
// one for the resistance changed event (spontaneous), and one for the other ones.
if (wait_for_response) {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
} else {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
}
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
m_control->state() == QLowEnergyController::UnconnectedState) {
qDebug() << QStringLiteral("writeCharacteristic error because the connection is closed");
return;
}
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
m_control->state() == QLowEnergyController::UnconnectedState) {
qDebug() << QStringLiteral("writeCharacteristic error because the connection is closed");
return;
}
if (!gattWriteCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattWriteCharacteristic is invalid");
return;
if (!gattWriteCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattWriteCharacteristic is invalid");
return;
}
}
if (writeBuffer) {
@@ -66,14 +75,25 @@ void echelonconnectsport::writeCharacteristic(uint8_t *data, uint8_t data_len, c
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
#ifdef Q_OS_IOS
if (useNativeIOS) {
#ifndef IO_UNDER_QT
iOS_echelonConnectSport->echelonConnectSport_WriteCharacteristic((unsigned char*)writeBuffer->data(), data_len);
#endif
} else
#endif
{
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
}
if (!disable_log) {
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
QStringLiteral(" // ") + info;
}
loop.exec();
if (!useNativeIOS) {
loop.exec();
}
}
void echelonconnectsport::forceResistance(resistance_t requestResistance) {
@@ -105,7 +125,14 @@ void echelonconnectsport::sendPoll() {
}
void echelonconnectsport::update() {
if (m_control->state() == QLowEnergyController::UnconnectedState) {
QSettings settings;
bool useNativeIOS = false;
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
#endif
if (!useNativeIOS && m_control->state() == QLowEnergyController::UnconnectedState) {
emit disconnected();
return;
}
@@ -113,12 +140,17 @@ void echelonconnectsport::update() {
if (initRequest) {
initRequest = false;
btinit();
} else if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService && gattWriteCharacteristic.isValid() &&
gattNotify1Characteristic.isValid() && gattNotify2Characteristic.isValid() && initDone) {
} else if ((useNativeIOS ||
(bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() &&
gattNotify1Characteristic.isValid() &&
gattNotify2Characteristic.isValid())) &&
initDone) {
update_metrics(true, watts());
// sending poll every 2 seconds
// sending poll every 2 seconds
if (sec1Update++ >= (2000 / refresh->interval())) {
sec1Update = 0;
sendPoll();
@@ -140,7 +172,7 @@ void echelonconnectsport::update() {
if (requestStart != -1) {
qDebug() << QStringLiteral("starting...");
// btinit();
// btinit();
requestStart = -1;
emit bikeStarted();
@@ -206,19 +238,24 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
bool useNativeIOS = false;
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
#endif
qDebug() << " << " + newValue.toHex(' ');
lastPacket = newValue;
// resistance value is in another frame
// resistance value is in another frame
if (newValue.length() == 5 && ((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd2) {
resistance_t res = newValue.at(3);
if (settings.value(QZSettings::gears_from_bike, QZSettings::default_gears_from_bike).toBool()) {
qDebug() << QStringLiteral("gears_from_bike") << res << Resistance.value() << gears()
<< lastRawRequestedResistanceValue << lastRequestedResistance().value();
if (
// if the resistance is different from the previous one
// if the resistance is different from the previous one
res != qRound(Resistance.value()) &&
// and the last target resistance is different from the current one or there is no any pending last
// requested resistance
@@ -294,18 +331,18 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
// these useless lines are needed to calculate the AVG resistance and AVG peloton resistance since
// echelon just send the resistance values when it changes
// these useless lines are needed to calculate the AVG resistance and AVG peloton resistance since
// echelon just send the resistance values when it changes
Resistance = Resistance.value();
m_pelotonResistance = m_pelotonResistance.value();
@@ -318,7 +355,7 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
qDebug() << QStringLiteral("Last CrankEventTime: ") + QString::number(LastCrankEventTime);
qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts());
if (m_control->error() != QLowEnergyController::NoError) {
if (!useNativeIOS && m_control && m_control->error() != QLowEnergyController::NoError) {
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
}
}
@@ -366,79 +403,90 @@ void echelonconnectsport::btinit() {
}
void echelonconnectsport::stateChanged(QLowEnergyService::ServiceState state) {
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66"));
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66"));
QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66"));
QSettings settings;
bool useNativeIOS = false;
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
#endif
if (state == QLowEnergyService::ServiceDiscovered) {
// qDebug() << gattCommunicationChannelService->characteristics();
if (!useNativeIOS) {
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66"));
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66"));
QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66"));
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotify1Characteristic.isValid());
Q_ASSERT(gattNotify2Characteristic.isValid());
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
// establish hook into notifications
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
&echelonconnectsport::characteristicChanged);
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
&echelonconnectsport::characteristicWritten);
connect(gattCommunicationChannelService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &echelonconnectsport::errorService);
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
&echelonconnectsport::descriptorWritten);
if (state == QLowEnergyService::ServiceDiscovered) {
// qDebug() << gattCommunicationChannelService->characteristics();
// ******************************************* virtual bike init *************************************
if (!firstStateChanged && !this->hasVirtualDevice()
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotify1Characteristic.isValid());
Q_ASSERT(gattNotify2Characteristic.isValid());
// establish hook into notifications
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
&echelonconnectsport::characteristicChanged);
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
&echelonconnectsport::characteristicWritten);
connect(gattCommunicationChannelService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &echelonconnectsport::errorService);
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
&echelonconnectsport::descriptorWritten);
}
}
// ******************************************* virtual bike init *************************************
if (!firstStateChanged && !this->hasVirtualDevice()
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
&& !h
#endif
#endif
) {
QSettings settings;
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
QSettings settings;
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
#endif
#endif
if (virtual_device_enabled) {
if (virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
} else {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike =
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
if (virtual_device_enabled) {
if (virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
// connect(virtualRower,&virtualrower::debug ,this,&echelonrower::debug);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
} else {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike =
new virtualbike(this, noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
// connect(virtualBike,&virtualbike::debug ,this,&echelonconnectsport::debug);
connect(virtualBike, &virtualbike::changeInclination, this, &echelonconnectsport::changeInclination);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
firstStateChanged = 1;
// ********************************************************************************************************
}
firstStateChanged = 1;
// ********************************************************************************************************
if (!useNativeIOS && state == QLowEnergyService::ServiceDiscovered) {
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
@@ -465,17 +513,26 @@ void echelonconnectsport::characteristicWritten(const QLowEnergyCharacteristic &
void echelonconnectsport::serviceScanDone(void) {
qDebug() << QStringLiteral("serviceScanDone");
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0bf669f1-45f2-11e7-9598-0800200c9a66"));
QSettings settings;
bool useNativeIOS = false;
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
&echelonconnectsport::stateChanged);
if(gattCommunicationChannelService != nullptr) {
gattCommunicationChannelService->discoverDetails();
} else {
if(homeform::singleton())
homeform::singleton()->setToastRequested("Bluetooth Service Error! Restart the bike!");
m_control->disconnectFromDevice();
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
#endif
if (!useNativeIOS) {
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0bf669f1-45f2-11e7-9598-0800200c9a66"));
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
&echelonconnectsport::stateChanged);
if(gattCommunicationChannelService != nullptr) {
gattCommunicationChannelService->discoverDetails();
} else {
if(homeform::singleton())
homeform::singleton()->setToastRequested("Bluetooth Service Error! Restart the bike!");
m_control->disconnectFromDevice();
}
}
}
@@ -494,6 +551,21 @@ void echelonconnectsport::error(QLowEnergyController::Error err) {
void echelonconnectsport::deviceDiscovered(const QBluetoothDeviceInfo &device) {
qDebug() << QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
device.address().toString() + ')';
QSettings settings;
bool useNativeIOS = false;
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
if (useNativeIOS) {
#ifndef IO_UNDER_QT
iOS_echelonConnectSport = new lockscreen();
iOS_echelonConnectSport->echelonConnectSport(device.name().toStdString().c_str(), this);
return;
#endif
}
#endif
if (device.name().startsWith(QStringLiteral("ECH"))) {
bluetoothDevice = device;
@@ -524,13 +596,23 @@ void echelonconnectsport::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit disconnected();
});
// Connect
// Connect
m_control->connectToDevice();
return;
}
}
bool echelonconnectsport::connected() {
QSettings settings;
bool useNativeIOS = false;
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
if (useNativeIOS) {
return true;
}
#endif
if (!m_control) {
return false;
}
@@ -657,10 +739,21 @@ uint16_t echelonconnectsport::wattsFromResistance(double resistance) {
void echelonconnectsport::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;
if (state == QLowEnergyController::UnconnectedState && m_control) {
QSettings settings;
bool useNativeIOS = false;
#ifdef Q_OS_IOS
useNativeIOS = settings.value(QZSettings::ios_btdevice_native, QZSettings::default_ios_btdevice_native).toBool();
#endif
if (state == QLowEnergyController::UnconnectedState
&& (m_control || useNativeIOS)
) {
lastResistanceBeforeDisconnection = Resistance.value();
qDebug() << QStringLiteral("trying to connect back again...");
initDone = false;
m_control->connectToDevice();
if(!useNativeIOS)
m_control->connectToDevice();
}
}

View File

@@ -82,6 +82,7 @@ class echelonconnectsport : public bike {
#ifdef Q_OS_IOS
lockscreen *h = 0;
lockscreen* iOS_echelonConnectSport = nullptr;
#endif
Q_SIGNALS:
@@ -89,14 +90,14 @@ class echelonconnectsport : public bike {
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void controllerStateChanged(QLowEnergyController::ControllerState state);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);

View File

@@ -0,0 +1,474 @@
#include "echelonstairclimber.h"
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#endif
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualtreadmill.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <chrono>
using namespace std::chrono_literals;
echelonstairclimber::echelonstairclimber(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
refresh = new QTimer(this);
initDone = false;
connect(refresh, &QTimer::timeout, this, &echelonstairclimber::update);
refresh->start(pollDeviceTime);
}
void echelonstairclimber::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
bool wait_for_response) {
QEventLoop loop;
QTimer timeout;
if (wait_for_response) {
connect(this, &echelonstairclimber::packetReceived, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
} else {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
}
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
m_control->state() == QLowEnergyController::UnconnectedState) {
emit debug(QStringLiteral("writeCharacteristic error because the connection is closed"));
return;
}
if (writeBuffer) {
delete writeBuffer;
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
QStringLiteral(" // ") + info);
}
loop.exec();
if (timeout.isActive() == false) {
emit debug(QStringLiteral(" exit for timeout"));
}
}
void echelonstairclimber::updateDisplay(uint16_t elapsed) {}
void echelonstairclimber::forceSpeed(double requestSpeed) {
uint8_t noOpData[] = {0xf0, 0xb2, 0x02, 0x00, 0x00, 0x00};
noOpData[3] = (uint8_t)(((uint16_t)(requestSpeed * 1000.0)) >> 8);
noOpData[4] = ((uint8_t)(requestSpeed * 1000.0));
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
noOpData[5] += noOpData[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force speed"), false, true);
}
void echelonstairclimber::forceIncline(double requestIncline) {
uint8_t noOpData[] = {0xf0, 0xb1, 0x01, 0x00, 0x00};
noOpData[3] = requestIncline;
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force incline"), false, true);
}
void echelonstairclimber::sendPoll() {
uint8_t noOpData[] = {0xf0, 0xa0, 0x01, 0x00, 0x00};
noOpData[3] = counterPoll;
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("noOp"), false, true);
counterPoll++;
if (!counterPoll) {
counterPoll = 1;
}
}
void echelonstairclimber::changeInclinationRequested(double grade, double percentage) {
if (percentage < 0)
percentage = 0;
changeInclination(grade, percentage);
}
void echelonstairclimber::update() {
if (m_control->state() == QLowEnergyController::UnconnectedState) {
emit disconnected();
return;
}
if (initRequest) {
initRequest = false;
btinit();
} else if (/*bluetoothDevice.isValid() &&*/
m_control->state() == QLowEnergyController::DiscoveredState && gattCommunicationChannelService &&
gattWriteCharacteristic.isValid() && gattNotify1Characteristic.isValid() &&
gattNotify2Characteristic.isValid() && initDone) {
QSettings settings;
// ******************************************* virtual treadmill init *************************************
if (!firstInit && !this->hasVirtualDevice()) {
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_force_bike =
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike)
.toBool();
if (virtual_device_enabled) {
if (!virtual_device_force_bike) {
debug("creating virtual treadmill interface...");
auto virtualTreadMill = new virtualtreadmill(this, noHeartService);
connect(virtualTreadMill, &virtualtreadmill::debug, this, &echelonstairclimber::debug);
connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
&echelonstairclimber::changeInclinationRequested);
this->setVirtualDevice(virtualTreadMill, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
debug("creating virtual bike interface...");
auto virtualBike = new virtualbike(this);
connect(virtualBike, &virtualbike::changeInclination, this,
&echelonstairclimber::changeInclinationRequested);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
}
firstInit = 1;
}
}
// ********************************************************************************************************
update_metrics(true, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
// updating the treadmill console every second
if (sec1Update++ >= (2000 / refresh->interval())) {
sec1Update = 0;
sendPoll();
}
/*
if (requestSpeed != -1) {
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
forceSpeed(requestSpeed);
}
requestSpeed = -1;
}
if (requestInclination != -100) {
if (requestInclination < 0)
requestInclination = 0;
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
requestInclination <= 15) {
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
forceIncline(requestInclination);
}
requestInclination = -100;
}
if (requestStart != -1) {
emit debug(QStringLiteral("starting..."));
if (lastSpeed == 0.0) {
lastSpeed = 0.5;
}
uint8_t initData3[] = {0xf0, 0xb0, 0x01, 0x01, 0xa2};
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("start"), false, true);
if(stride4) {
uint8_t initData0[] = {0xf0, 0xa5, 0x00, 0x95};
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("start"), false, false);
}
uint8_t initData4[] = {0xf0, 0xd0, 0x01, 0x00, 0xc1};
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("start"), false, false);
uint8_t initData5[] = {0xf0, 0xd0, 0x01, 0x11, 0xd2};
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("start"), false, false);
if(stride4) {
uint8_t initData0[] = {0xf0, 0xd3, 0x02, 0x01, 0xf4, 0xba};
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("start"), false, false);
}
lastStart = QDateTime::currentMSecsSinceEpoch();
requestStart = -1;
emit tapeStarted();
}
if (requestStop != -1) {
emit debug(QStringLiteral("stopping..."));
uint8_t initData3[] = {0xf0, 0xb0, 0x01, 0x00, 0xa1};
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("stop"), false, true);
requestStop = -1;
lastStop = QDateTime::currentMSecsSinceEpoch();
}
if (requestFanSpeed != -1) {
emit debug(QStringLiteral("changing fan speed..."));
// sendChangeFanSpeed(requestFanSpeed);
requestFanSpeed = -1;
}
if (requestIncreaseFan != -1) {
emit debug(QStringLiteral("increasing fan speed..."));
// sendChangeFanSpeed(FanSpeed + 1);
requestIncreaseFan = -1;
} else if (requestDecreaseFan != -1) {
emit debug(QStringLiteral("decreasing fan speed..."));
// sendChangeFanSpeed(FanSpeed - 1);
requestDecreaseFan = -1;
}*/
}
}
void echelonstairclimber::serviceDiscovered(const QBluetoothUuid &gatt) {
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
}
void echelonstairclimber::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QDateTime now = QDateTime::currentDateTime();
// qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
Q_UNUSED(characteristic);
QByteArray value = newValue;
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
lastPacket = newValue;
if (((unsigned char)newValue.at(0)) == 0xf0 && ((unsigned char)newValue.at(1)) == 0xd1) {
uint16_t convertedData = (newValue.at(5) << 8) | ((uint8_t)newValue.at(6));
StepCount = convertedData;
convertedData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
elevationAcc = convertedData;
Cadence = (uint8_t)newValue.at(9);
Speed = Cadence.value() / 3.2;
qDebug() << QStringLiteral("Current Floors: ") + QString::number(StepCount.value() / 10.0);
qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value());
qDebug() << QStringLiteral("Current StepCount: ") + QString::number(StepCount.value());
qDebug() << QStringLiteral("Current elevationAcc: ") + QString::number(elevationAcc.value());
return;
}
/*if (newValue.length() != 21)
return;*/
/*if ((uint8_t)(newValue.at(0)) != 0xf0 && (uint8_t)(newValue.at(1)) != 0xd1)
return;*/
if (!firstCharacteristicChanged) {
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()))
KCal +=
((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(
now)))); //(( (0.048* Output in watts +1.19) * body weight in
// kg * 3.5) / 200 ) / 60
Distance += ((Speed.value() / 3600.0) /
(1000.0 / (lastTimeCharacteristicChanged.msecsTo(now))));
}
if ((uint8_t)newValue.at(1) == 0xD1 && newValue.length() > 11)
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
Heart = (uint8_t)KeepAwakeHelper::heart();
else
#endif
{
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
uint8_t heart = ((uint8_t)newValue.at(11));
if (heart == 0) {
update_hr_from_external();
} else {
Heart = heart;
}
}
}
qDebug() << QStringLiteral("Current Heart: ") + QString::number(Heart.value());
qDebug() << QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value());
qDebug() << QStringLiteral("Current Watt: ") +
QString::number(watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
if (m_control->error() != QLowEnergyController::NoError)
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
lastTimeCharacteristicChanged = now;
firstCharacteristicChanged = false;
}
void echelonstairclimber::btinit() {
uint8_t initData0[] = {0xf0, 0xa4, 0x00, 0x94};
uint8_t initData1[] = {0xf0, 0xe0, 0x85, 0xf8, 0x03, 0x68, 0xd2, 0x8a};
uint8_t initData2[] = {0xf0, 0xe0, 0x38, 0x04, 0x03, 0xb5, 0x14, 0xd8};
uint8_t initData3[] = {0xf0, 0xa1, 0x00, 0x91};
uint8_t initData4[] = {0xf0, 0xa3, 0x00, 0x93};
uint8_t initData5[] = {0xf0, 0xb0, 0x01, 0x01, 0xa2};
uint8_t initData6[] = {0xf0, 0xa5, 0x00, 0x95};
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true);
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
writeCharacteristic(initData0, sizeof(initData0), QStringLiteral("init"), false, true);
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true);
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
writeCharacteristic(initData3, sizeof(initData3), QStringLiteral("init"), false, true);
writeCharacteristic(initData4, sizeof(initData4), QStringLiteral("init"), false, true);
writeCharacteristic(initData5, sizeof(initData5), QStringLiteral("init"), false, true);
writeCharacteristic(initData6, sizeof(initData6), QStringLiteral("init"), false, true);
initDone = true;
}
void echelonstairclimber::stateChanged(QLowEnergyService::ServiceState state) {
QBluetoothUuid _gattWriteCharacteristicId(QStringLiteral("0bf669f2-45f2-11e7-9598-0800200c9a66"));
QBluetoothUuid _gattNotify1CharacteristicId(QStringLiteral("0bf669f3-45f2-11e7-9598-0800200c9a66"));
QBluetoothUuid _gattNotify2CharacteristicId(QStringLiteral("0bf669f4-45f2-11e7-9598-0800200c9a66"));
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
if (state == QLowEnergyService::ServiceDiscovered) {
// qDebug() << gattCommunicationChannelService->characteristics();
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotify1Characteristic = gattCommunicationChannelService->characteristic(_gattNotify1CharacteristicId);
gattNotify2Characteristic = gattCommunicationChannelService->characteristic(_gattNotify2CharacteristicId);
Q_ASSERT(gattWriteCharacteristic.isValid());
Q_ASSERT(gattNotify1Characteristic.isValid());
Q_ASSERT(gattNotify2Characteristic.isValid());
// establish hook into notifications
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
&echelonstairclimber::characteristicChanged);
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, this,
&echelonstairclimber::characteristicWritten);
connect(gattCommunicationChannelService, SIGNAL(error(QLowEnergyService::ServiceError)), this,
SLOT(errorService(QLowEnergyService::ServiceError)));
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
&echelonstairclimber::descriptorWritten);
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(
gattNotify1Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
gattCommunicationChannelService->writeDescriptor(
gattNotify2Characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
void echelonstairclimber::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
initRequest = true;
emit connectedAndDiscovered();
}
void echelonstairclimber::characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
Q_UNUSED(characteristic);
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
}
void echelonstairclimber::serviceScanDone(void) {
qDebug() << QStringLiteral("serviceScanDone");
auto services_list = m_control->services();
for (const QBluetoothUuid &s : qAsConst(services_list)) {
qDebug() << s << "service found!";
}
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("0bf669f1-45f2-11e7-9598-0800200c9a66"));
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
if(gattCommunicationChannelService == nullptr) {
qDebug() << "invalid service";
return;
}
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &echelonstairclimber::stateChanged);
gattCommunicationChannelService->discoverDetails();
}
void echelonstairclimber::errorService(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
emit debug(QStringLiteral("echelonstairclimber::errorService ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString());
}
void echelonstairclimber::error(QLowEnergyController::Error err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
emit debug(QStringLiteral("echelonstairclimber::error ") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString());
}
void echelonstairclimber::deviceDiscovered(const QBluetoothDeviceInfo &device) {
{
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &echelonstairclimber::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &echelonstairclimber::serviceScanDone);
connect(m_control, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(error(QLowEnergyController::Error)));
connect(m_control, &QLowEnergyController::stateChanged, this, &echelonstairclimber::controllerStateChanged);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
emit debug(QStringLiteral("Cannot connect to remote device."));
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
emit debug(QStringLiteral("Controller connected. Search services..."));
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
emit debug(QStringLiteral("LowEnergy controller disconnected"));
emit disconnected();
});
// Connect
m_control->connectToDevice();
return;
}
}
void echelonstairclimber::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;
if (state == QLowEnergyController::UnconnectedState && m_control) {
qDebug() << QStringLiteral("trying to connect back again...");
initDone = false;
m_control->connectToDevice();
}
}
bool echelonstairclimber::connected() {
if (!m_control) {
return false;
}
return m_control->state() == QLowEnergyController::DiscoveredState;
}

View File

@@ -0,0 +1,104 @@
#ifndef ECHELONSTAIRCLIMBER_H
#define ECHELONSTAIRCLIMBER_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QDateTime>
#include <QObject>
#include <QString>
#include "stairclimber.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class echelonstairclimber : public stairclimber {
Q_OBJECT
public:
echelonstairclimber(uint32_t poolDeviceTime = 200, bool noConsole = false, bool noHeartService = false,
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
bool connected() override;
private:
double GetSpeedFromPacket(QByteArray packet);
double GetInclinationFromPacket(QByteArray packet);
double GetKcalFromPacket(QByteArray packet);
double GetDistanceFromPacket(QByteArray packet);
void forceSpeed(double requestSpeed);
void forceIncline(double requestIncline);
void updateDisplay(uint16_t elapsed);
void btinit();
void sendPoll();
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
bool wait_for_response = false);
void startDiscover();
bool noConsole = false;
bool noHeartService = false;
uint32_t pollDeviceTime = 200;
uint8_t sec1Update = 0;
uint8_t firstInit = 0;
uint8_t counterPoll = 1;
QByteArray lastPacket;
QDateTime lastTimeCharacteristicChanged;
bool firstCharacteristicChanged = true;
QTimer *refresh;
QLowEnergyService *gattCommunicationChannelService = nullptr;
QLowEnergyCharacteristic gattWriteCharacteristic;
QLowEnergyCharacteristic gattNotify1Characteristic;
QLowEnergyCharacteristic gattNotify2Characteristic;
bool initDone = false;
bool initRequest = false;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif
signals:
void disconnected();
void debug(QString string);
void speedChanged(double speed);
void packetReceived();
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void changeInclinationRequested(double grade, double percentage);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // ECHELONSTAIRCLIMBER_H

View File

@@ -0,0 +1,304 @@
#include "homeform.h"
#include "elitesquarecontroller.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QEventLoop>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <QThread>
using namespace std::chrono_literals;
#ifdef Q_OS_IOS
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
#endif
// Define static constants
const QString elitesquarecontroller::DEVICE_NAME = "SQUARE";
const QBluetoothUuid elitesquarecontroller::SERVICE_UUID = QBluetoothUuid(QStringLiteral("347b0001-7635-408b-8918-8ff3949ce592"));
const QBluetoothUuid elitesquarecontroller::CHARACTERISTIC_UUID = QBluetoothUuid(QStringLiteral("347b0045-7635-408b-8918-8ff3949ce592"));
elitesquarecontroller::elitesquarecontroller(bluetoothdevice *parentDevice) {
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
this->parentDevice = parentDevice;
// Initialize button state vector (24 buttons from 0-23)
buttonState.resize(24);
buttonState.fill(0);
// Set up refresh timer
refresh = new QTimer(this);
connect(refresh, &QTimer::timeout, this, &elitesquarecontroller::update);
refresh->start(1000ms);
}
void elitesquarecontroller::update() {
// Just a simple heartbeat check - no handshake needed for Elite Square
if (m_control && m_control->state() == QLowEnergyController::UnconnectedState) {
// Try to reconnect if disconnected
qDebug() << QStringLiteral("Elite Square disconnected, attempting to reconnect...");
m_control->connectToDevice();
}
}
void elitesquarecontroller::serviceDiscovered(const QBluetoothUuid &gatt) {
emit debug(QStringLiteral("serviceDiscovered ") + gatt.toString());
// Check if the discovered service is the Elite Square service
if (gatt == SERVICE_UUID) {
qDebug() << QStringLiteral("Elite Square service discovered");
}
}
void elitesquarecontroller::disconnectBluetooth() {
qDebug() << QStringLiteral("elitesquarecontroller::disconnect") << m_control;
if (m_control) {
m_control->disconnectFromDevice();
}
}
void elitesquarecontroller::characteristicChanged(const QLowEnergyCharacteristic &characteristic,
const QByteArray &newValue) {
Q_UNUSED(characteristic);
emit packetReceived();
qDebug() << QStringLiteral(" << ") << newValue.toHex(' ') << QString(newValue);
// Process the Elite Square button data
if (characteristic.uuid() == CHARACTERISTIC_UUID) {
// Process the raw bytes directly
parseButtonData(newValue);
}
}
void elitesquarecontroller::parseButtonData(const QByteArray &data) {
// The data comes as raw bytes, with 11 bytes total
if (data.size() < 11) {
qDebug() << QStringLiteral("Invalid button data size: ") << data.size();
return;
}
// Log the hex data for better debugging
qDebug() << QStringLiteral("Processing button data: ") << data.toHex(' ');
// Extract the important bytes (based on the provided Android code)
uint8_t leftByte = data[5];
uint8_t rightByte = data[10];
// Extract the nibbles from these bytes
uint8_t leftLargeValue = (leftByte >> 4) & 0xF; // Left Shift Button 1
uint8_t leftSmallValue = leftByte & 0xF; // Left Shift Button 2
uint8_t rightLargeValue = (rightByte >> 4) & 0xF; // Right Shift Button 1
uint8_t rightSmallValue = rightByte & 0xF; // Right Shift Button 2
qDebug() << QStringLiteral("Button values: leftLarge=") << leftLargeValue
<< QStringLiteral(" leftSmall=") << leftSmallValue
<< QStringLiteral(" rightLarge=") << rightLargeValue
<< QStringLiteral(" rightSmall=") << rightSmallValue;
// Check if the left large shift button (Left Shift Button 1) is pressed
// According to the code, odd values indicate press events
if (leftLargeValue != buttonState[BUTTON_LEFT_SHIFT_1] && leftLargeValue % 2 == 1) {
qDebug() << QStringLiteral("Left Shift Button 1 pressed (shift down 1)");
emit minus();
}
buttonState[BUTTON_LEFT_SHIFT_1] = leftLargeValue;
// Check if the left small shift button (Left Shift Button 2) is pressed
if (leftSmallValue != buttonState[BUTTON_LEFT_SHIFT_2] && leftSmallValue % 2 == 1) {
qDebug() << QStringLiteral("Left Shift Button 2 pressed (shift down 3)");
// Emit minus three times for triple downshift
emit minus();
emit minus();
emit minus();
}
buttonState[BUTTON_LEFT_SHIFT_2] = leftSmallValue;
// Check if the right large shift button (Right Shift Button 1) is pressed
if (rightLargeValue != buttonState[BUTTON_RIGHT_SHIFT_1] && rightLargeValue % 2 == 1) {
qDebug() << QStringLiteral("Right Shift Button 1 pressed (shift up 1)");
emit plus();
}
buttonState[BUTTON_RIGHT_SHIFT_1] = rightLargeValue;
// Check if the right small shift button (Right Shift Button 2) is pressed
if (rightSmallValue != buttonState[BUTTON_RIGHT_SHIFT_2] && rightSmallValue % 2 == 1) {
qDebug() << QStringLiteral("Right Shift Button 2 pressed (shift up 3)");
// Emit plus three times for triple upshift
emit plus();
emit plus();
emit plus();
}
buttonState[BUTTON_RIGHT_SHIFT_2] = rightSmallValue;
// Check for steering buttons and other controls
// Assuming byte 3 might contain steering information as in our previous implementation
uint8_t controlByte = data[3];
// These conditions would need to be adjusted based on actual behavior
if (controlByte == 0x60 && data[3] != buttonState[BUTTON_X]) {
emit steeringLeft(true);
buttonState[BUTTON_X] = controlByte;
} else if (controlByte == 0x20 && data[3] != buttonState[BUTTON_CIRCLE]) {
// Need to make sure this doesn't conflict with shift button detection
emit steeringRight(true);
buttonState[BUTTON_CIRCLE] = controlByte;
} else if (controlByte == 0x00) {
emit steeringLeft(false);
emit steeringRight(false);
buttonState[BUTTON_X] = 0;
buttonState[BUTTON_CIRCLE] = 0;
}
}
void elitesquarecontroller::stateChanged(QLowEnergyService::ServiceState state) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
qDebug() << QStringLiteral("stateChanged") << s->serviceUuid() << s->state();
if (s->state() != QLowEnergyService::ServiceDiscovered && s->state() != QLowEnergyService::InvalidService) {
qDebug() << QStringLiteral("not all services discovered");
return;
}
}
if (state != QLowEnergyService::ServiceState::ServiceDiscovered) {
qDebug() << QStringLiteral("ignoring this state");
return;
}
qDebug() << QStringLiteral("all services discovered!");
for (QLowEnergyService *s : qAsConst(gattCommunicationChannelService)) {
if (s->state() == QLowEnergyService::ServiceDiscovered) {
// establish hook into notifications
connect(s, &QLowEnergyService::characteristicChanged, this, &elitesquarecontroller::characteristicChanged);
connect(
s, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &elitesquarecontroller::errorService);
connect(s, &QLowEnergyService::descriptorWritten, this, &elitesquarecontroller::descriptorWritten);
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
// Check if this is the Elite Square service
if (s->serviceUuid() == SERVICE_UUID) {
gattService = s;
// Find the notification characteristic
auto characteristics_list = s->characteristics();
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
if (c.uuid() == CHARACTERISTIC_UUID) {
gattNotifyCharacteristic = c;
// Subscribe to notifications
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
if (c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration).isValid()) {
s->writeDescriptor(c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
connectionEstablished = true;
qDebug() << QStringLiteral("Elite Square notification subscribed!");
} else {
qDebug() << QStringLiteral("ClientCharacteristicConfiguration is not valid");
}
}
}
}
}
}
}
}
void elitesquarecontroller::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' '));
}
void elitesquarecontroller::serviceScanDone(void) {
emit debug(QStringLiteral("serviceScanDone"));
auto services_list = m_control->services();
for (const QBluetoothUuid &s : qAsConst(services_list)) {
gattCommunicationChannelService.append(m_control->createServiceObject(s));
if (gattCommunicationChannelService.constLast()) {
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
&elitesquarecontroller::stateChanged);
gattCommunicationChannelService.constLast()->discoverDetails();
} else {
m_control->disconnectFromDevice();
}
}
}
void elitesquarecontroller::errorService(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
emit debug(QStringLiteral("elitesquarecontroller::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString());
}
void elitesquarecontroller::error(QLowEnergyController::Error err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
emit debug(QStringLiteral("elitesquarecontroller::error") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString());
}
void elitesquarecontroller::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit debug(QStringLiteral("Found new device: ") + device.name() + QStringLiteral(" (") +
device.address().toString() + ')');
// Check if this is the Elite Square device
if (device.name() == DEVICE_NAME) {
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &elitesquarecontroller::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &elitesquarecontroller::serviceScanDone);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, &elitesquarecontroller::error);
connect(m_control, &QLowEnergyController::stateChanged, this, &elitesquarecontroller::controllerStateChanged);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
emit debug(QStringLiteral("Cannot connect to Elite Square device."));
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
emit debug(QStringLiteral("Elite Square controller connected. Searching services..."));
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
emit debug(QStringLiteral("Elite Square controller disconnected"));
connectionEstablished = false;
emit disconnected();
});
// Connect to the device
m_control->connectToDevice();
}
}
bool elitesquarecontroller::connected() {
if (!m_control) {
return false;
}
return connectionEstablished && (m_control->state() == QLowEnergyController::DiscoveredState);
}
void elitesquarecontroller::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;
if (state == QLowEnergyController::UnconnectedState && m_control) {
qDebug() << QStringLiteral("trying to connect back again...");
connectionEstablished = false;
m_control->connectToDevice();
}
}

View File

@@ -0,0 +1,120 @@
#ifndef ELITESQUARECONTROLLER_H
#define ELITESQUARECONTROLLER_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QObject>
#include <QTime>
#include "devices/bluetoothdevice.h"
#include "zwift_play/abstractZapDevice.h"
// Button positions on Elite Square controller
enum EliteSquareButton {
BUTTON_NONE = 0,
// 1 and 2 are N/A
BUTTON_UP = 3,
BUTTON_LEFT = 4,
BUTTON_DOWN = 5,
BUTTON_RIGHT = 6,
BUTTON_X = 7,
BUTTON_SQUARE = 8,
BUTTON_LEFT_CAMPAGNOLO = 9,
BUTTON_LEFT_BRAKE = 10,
BUTTON_LEFT_SHIFT_1 = 11,
BUTTON_LEFT_SHIFT_2 = 12,
BUTTON_Y = 13,
BUTTON_A = 14,
BUTTON_B = 15,
BUTTON_Z = 16,
BUTTON_CIRCLE = 17,
BUTTON_TRIANGLE = 18,
// 19 is N/A
BUTTON_RIGHT_CAMPAGNOLO = 20,
BUTTON_RIGHT_BRAKE = 21,
BUTTON_RIGHT_SHIFT_1 = 22,
BUTTON_RIGHT_SHIFT_2 = 23
};
// Button state
enum EliteSquareButtonState {
RELEASED = 0,
PRESSED = 1
};
class elitesquarecontroller : public bluetoothdevice {
Q_OBJECT
public:
// Constants for Elite Square device
static const QString DEVICE_NAME;
static const QBluetoothUuid SERVICE_UUID;
static const QBluetoothUuid CHARACTERISTIC_UUID;
elitesquarecontroller(bluetoothdevice *parentDevice);
bool connected() override;
private:
QList<QLowEnergyService *> gattCommunicationChannelService;
QLowEnergyCharacteristic gattNotifyCharacteristic;
QLowEnergyService *gattService;
bluetoothdevice *parentDevice = nullptr;
bool connectionEstablished = false;
QTimer *refresh;
// Last known state of each button (0-23)
QVector<int> buttonState;
// Helper function to parse the button data
void parseButtonData(const QByteArray &data);
signals:
void disconnected();
void debug(QString string);
void packetReceived();
// Match AbstractZapDevice signals
void plus(); // Gear up/increase
void minus(); // Gear down/decrease
void steeringLeft(bool active); // X button and Left Campagnolo
void steeringRight(bool active); // Circle button and Right Campagnolo
void buttonActivated(int buttonId, bool pressed); // General button signal
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
void disconnectBluetooth();
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
};
#endif // ELITESQUARECONTROLLER_H

View File

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

@@ -48,10 +48,13 @@ void fakebike::update() {
if (requestPower != -1) {
// bepo70: don't know if this conversion is really needed, i would do it anyway.
m_watt = (double)requestPower;
Cadence = requestPower;
m_watt = (double)requestPower * (1.0 + (((double)rand() / RAND_MAX) * 0.4 - 0.2));
if(requestPower)
Cadence = 50 + (static_cast<double>(rand()) / RAND_MAX) * 50;
else
Cadence = 0;
emit debug(QStringLiteral("writing power ") + QString::number(requestPower));
requestPower = -1;
//requestPower = -1;
// bepo70: Disregard the current inclination for calculating speed. When the video
// has a high inclination you have to give many power to get the desired playback speed,
// if inclination is very low little more power gives a quite high speed jump.

View File

@@ -512,7 +512,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index)))) / 10.0;
emit resistanceRead(Resistance.value());
m_pelotonResistance = Resistance.value();
m_pelotonResistance = bikeResistanceToPeloton(Resistance.value());
index += 2;
qDebug() << QStringLiteral("Current Resistance: ") + QString::number(Resistance.value());
}
@@ -633,7 +633,7 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
} else {
m_pelotonResistance = (100 * Resistance.value()) / max_resistance;
m_pelotonResistance = bikeResistanceToPeloton(Resistance.value());
}
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
@@ -736,6 +736,19 @@ void fitplusbike::characteristicChanged(const QLowEnergyCharacteristic &characte
}
}
resistance_t fitplusbike::pelotonToBikeResistance(int pelotonResistance) {
QSettings settings;
double adjustedPelotonResistance = (pelotonResistance - settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble()) /
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble();
return (adjustedPelotonResistance * max_resistance) / 100;
}
double fitplusbike::bikeResistanceToPeloton(double resistance) {
QSettings settings;
return (((resistance * 100) / max_resistance) * settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
}
void fitplusbike::btinit() {
QSettings settings;
@@ -1003,6 +1016,7 @@ void fitplusbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
merach_MRK = true;
} else if (device.name().toUpper().startsWith("H9110 OSAKA")) {
qDebug() << QStringLiteral("H9110 OSAKA workaround enabled!");
max_resistance = 32;
H9110_OSAKA = true;
}

View File

@@ -38,6 +38,7 @@ class fitplusbike : public bike {
public:
fitplusbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset, double bikeResistanceGain);
resistance_t maxResistance() override { return max_resistance; }
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
bool connected() override;
resistance_t resistanceFromPowerRequest(uint16_t power) override;
@@ -49,6 +50,7 @@ class fitplusbike : public bike {
void startDiscover();
void forceResistance(resistance_t requestResistance);
void sendPoll();
double bikeResistanceToPeloton(double resistance);
uint16_t watts() override;
uint16_t wattsFromResistance(double resistance);

View File

@@ -210,7 +210,7 @@ void fitshowtreadmill::update() {
if (requestInclination != inc) {
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
inc = requestInclination;
double speed = currentSpeed().value();
double speed = currentSpeed().valueRaw();
if (requestSpeed != -1) {
speed = requestSpeed;
requestSpeed = -1;

View File

@@ -34,6 +34,7 @@ ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResis
this->bikeResistanceGain = bikeResistanceGain;
this->bikeResistanceOffset = bikeResistanceOffset;
initDone = false;
ergModeSupported = true; // by default ftms devices SHOULD have ergMode supported
connect(refresh, &QTimer::timeout, this, &ftmsbike::update);
refresh->start(settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt());
wheelCircumference::GearTable g;
@@ -129,14 +130,17 @@ void ftmsbike::init() {
if (initDone)
return;
if(ICSE) {
uint8_t write[] = {FTMS_REQUEST_CONTROL};
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
write[0] = {FTMS_RESET};
ret = writeCharacteristic(write, sizeof(write), "reset", false, true);
}
uint8_t write[] = {FTMS_REQUEST_CONTROL};
bool ret = writeCharacteristic(write, sizeof(write), "requestControl", false, true);
if(resistance_lvl_mode && DIRETO_XR) {
setWheelDiameter(2070.0);
} else {
write[0] = {FTMS_START_RESUME};
ret = writeCharacteristic(write, sizeof(write), "start simulation", false, true);
}
write[0] = {FTMS_START_RESUME};
ret = writeCharacteristic(write, sizeof(write), "start simulation", false, true);
if(ret) {
initDone = true;
@@ -145,10 +149,6 @@ void ftmsbike::init() {
}
ftmsbike::~ftmsbike() {
// Set wheel circumference back to 2070 when object is destroyed
if (DIRETO_XR) {
setWheelDiameter(2070.0);
}
}
void ftmsbike::zwiftPlayInit() {
@@ -194,7 +194,7 @@ void ftmsbike::zwiftPlayInit() {
}
void ftmsbike::forcePower(int16_t requestPower) {
if(resistance_lvl_mode) {
if(resistance_lvl_mode || TITAN_7000) {
forceResistance(resistanceFromPowerRequest(requestPower));
} else {
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
@@ -232,18 +232,31 @@ resistance_t ftmsbike::resistanceFromPowerRequest(uint16_t power) {
if (power < wattsFromResistance(1))
return 1;
else
return max_resistance;
if(DU30_bike)
return max_resistance;
else
return _ergTable.getMaxResistance();
}
void ftmsbike::forceResistance(resistance_t requestResistance) {
QSettings settings;
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool() &&
resistance_lvl_mode == false && _3G_Cardio_RB == false && JFBK5_0 == false) {
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x28, 0x19};
double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset);
requestResistance = fr;
if(ergModeNotSupported) {
requestResistance = _inclinationResistanceTable.estimateInclination(requestResistance) * 10.0;
qDebug() << "ergMode Not Supported so the resistance will be" << requestResistance;
} else {
requestResistance = fr;
}
if(TITAN_7000)
Resistance = requestResistance;
write[3] = ((uint16_t)requestResistance * 10) & 0xFF;
write[4] = ((uint16_t)requestResistance * 10) >> 8;
@@ -251,6 +264,9 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
writeCharacteristic(write, sizeof(write),
QStringLiteral("forceResistance ") + QString::number(requestResistance));
} else {
if(SL010)
Resistance = requestResistance;
if(JFBK5_0 || DIRETO_XR) {
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00, 0x00};
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
@@ -259,7 +275,7 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
QStringLiteral("forceResistance ") + QString::number(requestResistance));
} else {
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00};
if(_3G_Cardio_RB)
if(_3G_Cardio_RB || SL010)
requestResistance = requestResistance * 10;
write[1] = ((uint8_t)(requestResistance));
writeCharacteristic(write, sizeof(write),
@@ -269,6 +285,9 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
}
void ftmsbike::update() {
QSettings settings;
if (m_control->state() == QLowEnergyController::UnconnectedState) {
emit disconnected();
return;
@@ -276,6 +295,13 @@ void ftmsbike::update() {
if (initRequest) {
zwiftPlayInit();
if(ICSE)
requestResistance = 1; // to force the engine to send every second a target inclination
// when we are emulating the zwift protocol, zwift doesn't senf the start simulation frames, so we have to send them
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool())
init();
initRequest = false;
} else if (bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState //&&
@@ -299,8 +325,7 @@ void ftmsbike::update() {
forceResistance(currentResistance().value());
}
auto virtualBike = this->VirtualBike();
QSettings settings;
auto virtualBike = this->VirtualBike();
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
if (requestResistance != -1 || lastGearValue != gears()) {
@@ -311,32 +336,30 @@ void ftmsbike::update() {
requestResistance = 1;
}
if (requestResistance != currentResistance().value() || lastGearValue != gears()) {
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
double gearMultiplier = 5;
if(REEBOK)
gearMultiplier = 1;
resistance_t rR = requestResistance + (gears() * gearMultiplier);
if (rR != currentResistance().value() || lastGearValue != gears()) {
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
qDebug() << QStringLiteral("writing resistance ") << requestResistance << ergModeNotSupported << requestPower << resistance_lvl_mode;
// if the FTMS is connected, the ftmsCharacteristicChanged event will do all the stuff because it's a
// FTMS bike. This condition handles the peloton requests
if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike || resistance_lvl_mode) &&
(requestPower == 0 || requestPower == -1)) {
// FTMS bike. This condition handles the peloton requests
if (((virtualBike && !virtualBike->ftmsDeviceConnected()) || !virtualBike || resistance_lvl_mode || ergModeNotSupported) &&
(requestPower == 0 || requestPower == -1 || resistance_lvl_mode || ergModeNotSupported)) {
init();
if(DIRETO_XR && gears_zwift_ratio)
setWheelDiameter(wheelCircumference::gearsToWheelDiameter(gears()));
else {
forceResistance(requestResistance + (gears() * 5));
if(DIRETO_XR)
Resistance = requestResistance;
}
forceResistance(rR);
}
}
requestResistance = -1;
if(!ICSE)
requestResistance = -1;
}
if((virtualBike && virtualBike->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100 && lastPacketFromFTMS.length() >= 7) {
if(DIRETO_XR && gears_zwift_ratio) {
setWheelDiameter(wheelCircumference::gearsToWheelDiameter(gears()));
} else {
qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' ');
ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS);
}
qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' ');
ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS);
}
if(zwiftPlayService && gears_zwift_ratio && lastGearValue != gears()) {
@@ -391,7 +414,8 @@ void ftmsbike::update() {
lastGearValue = gears();
if (requestPower != -1) {
// if a classic request of power from zwift or any other platform is coming, will be transfereed on the ftmsCharacteristicChanged applying the gear mod too
if (requestPower != -1 && (!virtualBike || !virtualBike->ftmsDeviceConnected() || (zwiftPlayService != nullptr && gears_zwift_ratio))) {
qDebug() << QStringLiteral("writing power") << requestPower;
init();
forcePower(requestPower);
@@ -473,6 +497,26 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
return;
}
if(T2 && characteristic.uuid() == QBluetoothUuid(QStringLiteral("6e400003-b5a3-f393-e0a9-e50e24dcca9e")) && newValue.length() == 62) {
int16_t gears = ((int16_t)(((int16_t)((uint8_t)newValue.at(55)) << 8) |
(int16_t)((uint8_t)newValue.at(54))));
qDebug() << QStringLiteral("T2 gears event, actual gear") << gears << QStringLiteral("previous value") << T2_lastGear;
if (gears < T2_lastGear) {
for (int i = 0; i < T2_lastGear - gears; ++i) {
gearDown();
}
} else if (gears > T2_lastGear) {
for (int i = 0; i < gears - T2_lastGear; ++i) {
gearUp();
}
}
T2_lastGear = gears;
return;
}
// Wattbike Atom First Generation - Display Gears
if(WATTBIKE && characteristic.uuid() == QBluetoothUuid(QStringLiteral("b4cc1224-bc02-4cae-adb9-1217ad2860d1")) &&
newValue.length() > 3 && newValue.at(1) == 0x03 && (uint8_t)newValue.at(2) == 0xb6) {
@@ -580,10 +624,12 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
double d = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
index += 2;
if(Resistance.value() > 0) {
if(d > 0) {
if(BIKE_)
d = d / 10.0;
Resistance = d;
// for this bike, i will use the resistance that I set directly because the bike sends a different ratio.
if(!SL010 && !TITAN_7000)
Resistance = d;
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
emit resistanceRead(Resistance.value());
resistance_received = true;
@@ -598,15 +644,19 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
double cr = 97.62165482;
if (Cadence.value() && m_watt.value()) {
m_pelotonResistance =
(((sqrt(pow(br, 2.0) - 4.0 * ar *
(cr - (m_watt.value() * 132.0 /
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
br) /
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
if (!resistance_received && !DU30_bike) {
if(YS_G1MPLUS) {
m_pelotonResistance = Resistance.value(); // 1:1 ratio
} else {
m_pelotonResistance =
(((sqrt(pow(br, 2.0) - 4.0 * ar *
(cr - (m_watt.value() * 132.0 /
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
br) /
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
}
if (!resistance_received && !DU30_bike && !SL010) {
Resistance = m_pelotonResistance;
emit resistanceRead(Resistance.value());
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
@@ -695,6 +745,181 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
}
lastRefreshCharacteristicChanged2AD2 = now;
ftmsFrameReceived = true;
} else if (characteristic.uuid() == QBluetoothUuid::CyclingPowerMeasurement && !ftmsFrameReceived) {
uint16_t flags = (((uint16_t)((uint8_t)newValue.at(1)) << 8) | (uint16_t)((uint8_t)newValue.at(0)));
bool cadence_present = false;
bool wheel_revs = false;
bool crank_rev_present = false;
uint16_t time_division = 1024;
uint8_t index = 4;
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
if (newValue.length() > 3) {
m_watt = (((uint16_t)((uint8_t)newValue.at(3)) << 8) | (uint16_t)((uint8_t)newValue.at(2)));
}
emit powerChanged(m_watt.value());
emit debug(QStringLiteral("Current watt: ") + QString::number(m_watt.value()));
}
if(THINK_X) {
if ((flags & 0x1) == 0x01) // Pedal Power Balance Present
{
index += 1;
}
if ((flags & 0x2) == 0x02) // Pedal Power Balance Reference
{
}
if ((flags & 0x4) == 0x04) // Accumulated Torque Present
{
index += 2;
}
if ((flags & 0x8) == 0x08) // Accumulated Torque Source
{
}
if ((flags & 0x10) == 0x10) // Wheel Revolution Data Present
{
cadence_present = true;
wheel_revs = true;
}
if ((flags & 0x20) == 0x20) // Crank Revolution Data Present
{
cadence_present = true;
crank_rev_present = true;
}
if (cadence_present) {
if (wheel_revs && !crank_rev_present) {
time_division = 2048;
CrankRevs =
(((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) |
((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
((uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index)));
index += 4;
LastCrankEventTime =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
index += 2; // wheel event time
} else if (wheel_revs && crank_rev_present) {
index += 4; // wheel revs
index += 2; // wheel event time
}
if (crank_rev_present) {
CrankRevs =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
index += 2;
LastCrankEventTime =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
index += 2;
}
int16_t deltaT = LastCrankEventTime - oldLastCrankEventTime;
if (deltaT < 0) {
deltaT = LastCrankEventTime + time_division - oldLastCrankEventTime;
}
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
if (CrankRevs != oldCrankRevs && deltaT) {
double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * time_division * 60;
if (!crank_rev_present)
cadence =
cadence /
2; // I really don't like this, there is no relationship between wheel rev and crank rev
if (cadence >= 0) {
Cadence = cadence;
}
lastGoodCadence = now;
} else if (lastGoodCadence.msecsTo(now) > 2000) {
Cadence = 0;
}
}
qDebug() << QStringLiteral("Current Cadence: ") << Cadence.value() << CrankRevs << oldCrankRevs << deltaT
<< time_division << LastCrankEventTime << oldLastCrankEventTime;
oldLastCrankEventTime = LastCrankEventTime;
oldCrankRevs = CrankRevs;
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = Cadence.value() * settings
.value(QZSettings::cadence_sensor_speed_ratio,
QZSettings::default_cadence_sensor_speed_ratio)
.toDouble();
} else {
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChangedPower.msecsTo(now)));
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
// if we change this, also change the wattsFromResistance function. We can create a standard function in
// order to have all the costants in one place (I WANT MORE TIME!!!)
double ac = 0.01243107769;
double bc = 1.145964912;
double cc = -23.50977444;
double ar = 0.1469553975;
double br = -5.841344538;
double cr = 97.62165482;
double res =
(((sqrt(pow(br, 2.0) - 4.0 * ar *
(cr - (m_watt.value() * 132.0 /
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
br) /
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
if (isnan(res)) {
if (Cadence.value() > 0) {
// let's keep the last good value
} else {
m_pelotonResistance = 0;
}
} else {
m_pelotonResistance = res;
}
qDebug() << QStringLiteral("Current Peloton Resistance: ") + QString::number(m_pelotonResistance.value());
if (settings.value(QZSettings::schwinn_bike_resistance, QZSettings::default_schwinn_bike_resistance)
.toBool())
Resistance = pelotonToBikeResistance(m_pelotonResistance.value());
else
Resistance = m_pelotonResistance;
emit resistanceRead(Resistance.value());
qDebug() << QStringLiteral("Current Resistance Calculated: ") + QString::number(Resistance.value());
if (watts())
KCal +=
((((0.048 * ((double)watts()) + 1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChangedPower.msecsTo(
now)))); //(( (0.048* Output in watts +1.19) * body weight
// in kg * 3.5) / 200 ) / 60
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
lastRefreshCharacteristicChangedPower = now;
}
}
} else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACE)) {
union flags {
struct {
@@ -789,9 +1014,12 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
}
if (Flags.resistanceLvl) {
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
emit resistanceRead(Resistance.value());
if(!TITAN_7000) {
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
emit resistanceRead(Resistance.value());
resistance_received = true;
}
index += 2;
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
} else if(!DU30_bike) {
@@ -901,6 +1129,9 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
update_hr_from_external();
}
if(resistance_received && requestPower == -1)
_inclinationResistanceTable.collectData(Inclination.value(), Resistance.value(), m_watt.value());
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
@@ -955,7 +1186,18 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
qDebug() << s->serviceUuid() << QStringLiteral("connected!");
if (settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || ICSE || SCH_190U || DOMYOS || SMB1) {
if (ICSE) {
QBluetoothUuid ftmsService((quint16)0x1826);
QBluetoothUuid CSCService((quint16)0x1816);
if (s->serviceUuid() != ftmsService && s->serviceUuid() != CSCService) {
qDebug() << QStringLiteral("ICSE bike wants to be subscribed only to FTMS and CSC services in order "
"to send metrics")
<< s->serviceUuid();
continue;
}
}
if (settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SCH_190U || DOMYOS || SMB1 || FIT_BK) {
QBluetoothUuid ftmsService((quint16)0x1826);
if (s->serviceUuid() != ftmsService) {
qDebug() << QStringLiteral("hammer racer bike wants to be subscribed only to FTMS service in order "
@@ -1015,7 +1257,7 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
}
QBluetoothUuid _zwiftPlayWriteCharControlPointId(QStringLiteral("00000003-19ca-4651-86e5-fa29dcdd09d1"));
if (c.uuid() == _zwiftPlayWriteCharControlPointId && !DIRETO_XR) {
if (c.uuid() == _zwiftPlayWriteCharControlPointId) {
qDebug() << QStringLiteral("Zwift Play service and Control Point found");
zwiftPlayWriteChar = c;
zwiftPlayService = s;
@@ -1028,10 +1270,14 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
settings.setValue(QZSettings::domyosbike_notfmts, true);
if(homeform::singleton())
homeform::singleton()->setToastRequested("Domyos bike presents itself like a FTMS but it's not. Restart QZ to apply the fix, thanks.");
} else if(gattFTMSService == nullptr && PM5) {
settings.setValue(QZSettings::ftms_rower, bluetoothDevice.name());
if(homeform::singleton())
homeform::singleton()->setToastRequested("PM5 rower found. Restart QZ to apply the fix, thanks.");
}
if (gattFTMSService && gattWriteCharControlPointId.isValid() &&
(settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SMB1)) {
(settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SMB1 || FIT_BK)) {
init();
}
@@ -1075,9 +1321,10 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
if (!autoResistance() || resistance_lvl_mode) {
qDebug() << "ignoring routing FTMS packet to the bike from virtualbike because of auto resistance OFF or resistance lvl mode is on"
<< characteristic.uuid() << newValue.toHex(' ');
bool ergModeNotSupported = (requestPower > 0 && !ergModeSupported);
if (!autoResistance() || resistance_lvl_mode || ergModeNotSupported) {
qDebug() << "ignoring routing FTMS packet to the bike from virtualbike because of auto resistance OFF or resistance lvl mode is on or ergModeNotSupported"
<< characteristic.uuid() << newValue.toHex(' ') << ergModeNotSupported << resistance_lvl_mode;
return;
}
@@ -1155,10 +1402,12 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
#endif
writeCharacteristicZwiftPlay((uint8_t*)message.data(), message.length(), "gearInclination", false, false);
return;
} else if(b.at(0) == FTMS_SET_TARGET_POWER && zwiftPlayService != nullptr && gears_zwift_ratio) {
} else if(b.at(0) == FTMS_SET_TARGET_POWER && ((zwiftPlayService != nullptr && gears_zwift_ratio) || !ergModeSupported)) {
qDebug() << "discarding";
return;
} else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
}
// gears on erg mode is quite useless and it's confusing
/* else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
lastPacketFromFTMS.clear();
for(int i=0; i<b.length(); i++)
lastPacketFromFTMS.append(b.at(i));
@@ -1170,7 +1419,7 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
b[1] = power & 0xFF;
b[2] = power >> 8;
qDebug() << "applying gears mod" << gears() << gearsZwiftRatio() << power;
}
}*/
writeCharacteristic((uint8_t*)b.data(), b.length(), "injectWrite ", false, true);
}
@@ -1273,6 +1522,7 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if((bluetoothDevice.name().toUpper().startsWith("SCH_190U"))) {
qDebug() << QStringLiteral("SCH_190U found");
SCH_190U = true;
max_resistance = 100;
} else if(bluetoothDevice.name().toUpper().startsWith("D2RIDE")) {
qDebug() << QStringLiteral("D2RIDE found");
D2RIDE = true;
@@ -1301,6 +1551,40 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((bluetoothDevice.name().toUpper().startsWith("LYDSTO"))) {
qDebug() << QStringLiteral("LYDSTO found");
LYDSTO = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("SL010-"))) {
qDebug() << QStringLiteral("SL010 found");
SL010 = true;
max_resistance = 25;
resistance_lvl_mode = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("REEBOK"))) {
qDebug() << QStringLiteral("REEBOK found");
REEBOK = true;
max_resistance = 32;
resistance_lvl_mode = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("TITAN 7000"))) {
qDebug() << QStringLiteral("Titan 7000 found");
TITAN_7000 = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("T2 "))) {
qDebug() << QStringLiteral("T2 found");
T2 = true;
} else if ((bluetoothDevice.name().toUpper().startsWith(QStringLiteral("FIT-BK-")))) {
qDebug() << QStringLiteral("FIT-BK found");
FIT_BK = true;
ergModeSupported = false; // this bike doesn't have ERG mode natively
} else if ((bluetoothDevice.name().toUpper().startsWith(QStringLiteral("EXPERT-SX9")))) {
qDebug() << QStringLiteral("EXPERT-SX9 found");
EXPERT_SX9 = true;
ergModeSupported = false; // this bike doesn't have ERG mode natively
} else if (((bluetoothDevice.name().toUpper().startsWith("YS_G1MPLUS")))) {
qDebug() << QStringLiteral("YS_G1MPLUS found");
YS_G1MPLUS = true;
max_resistance = 100;
} else if (bluetoothDevice.name().toUpper().startsWith(QStringLiteral("PM5"))) {
PM5 = true;
qDebug() << QStringLiteral("PM5 found");
} else if(device.name().toUpper().startsWith(QStringLiteral("THINK X")) || device.name().toUpper().startsWith(QStringLiteral("THINK-"))) {
THINK_X = true;
qDebug() << "THINK X workaround enabled!";
}
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
@@ -1379,7 +1663,7 @@ double ftmsbike::maxGears() {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
if((zwiftPlayService != nullptr || DIRETO_XR) && gears_zwift_ratio) {
if((zwiftPlayService != nullptr) && gears_zwift_ratio) {
wheelCircumference::GearTable g;
return g.maxGears;
} else if(WATTBIKE) {
@@ -1393,7 +1677,7 @@ double ftmsbike::minGears() {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
if((zwiftPlayService != nullptr || DIRETO_XR) && gears_zwift_ratio ) {
if((zwiftPlayService != nullptr) && gears_zwift_ratio ) {
return 1;
} else if(WATTBIKE) {
return 1;

View File

@@ -28,6 +28,7 @@
#include "wheelcircumference.h"
#include "devices/bike.h"
#include "inclinationresistancetable.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
@@ -104,8 +105,10 @@ class ftmsbike : public bike {
uint8_t sec1Update = 0;
QByteArray lastPacket;
QByteArray lastPacketFromFTMS;
QDateTime lastRefreshCharacteristicChangedPower = QDateTime::currentDateTime();
QDateTime lastRefreshCharacteristicChanged2AD2 = QDateTime::currentDateTime();
QDateTime lastRefreshCharacteristicChanged2ACE = QDateTime::currentDateTime();
bool ftmsFrameReceived = false;
uint8_t firstStateChanged = 0;
int8_t bikeResistanceOffset = 4;
double bikeResistanceGain = 1.0;
@@ -122,6 +125,7 @@ class ftmsbike : public bike {
bool resistance_lvl_mode = false;
bool resistance_received = false;
inclinationResistanceTable _inclinationResistanceTable;
bool DU30_bike = false;
bool ICSE = false;
@@ -137,9 +141,25 @@ class ftmsbike : public bike {
bool BIKE_ = false;
bool SMB1 = false;
bool LYDSTO = false;
bool SL010 = false;
bool REEBOK = false;
bool TITAN_7000 = false;
bool T2 = false;
bool FIT_BK = false;
bool YS_G1MPLUS = false;
bool EXPERT_SX9 = false;
bool PM5 = false;
bool THINK_X = false;
int16_t T2_lastGear = 0;
uint8_t battery_level = 0;
uint16_t oldLastCrankEventTime = 0;
uint16_t oldCrankRevs = 0;
QDateTime lastGoodCadence = QDateTime::currentDateTime();
double lastRawRequestedInclinationValue = -100;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif

View File

@@ -78,7 +78,7 @@ void ftmsrower::update() {
}
if (initRequest) {
if(I_ROWER) {
if(I_ROWER || ROWER) {
uint8_t write[] = {FTMS_REQUEST_CONTROL};
writeCharacteristic(write, sizeof(write), "start", false, true);
@@ -412,7 +412,7 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
connect(s, &QLowEnergyService::descriptorWritten, this, &ftmsrower::descriptorWritten);
connect(s, &QLowEnergyService::descriptorRead, this, &ftmsrower::descriptorRead);
if (I_ROWER) {
if (I_ROWER || ROWER) {
QBluetoothUuid ftmsService((quint16)0x1826);
if (s->serviceUuid() != ftmsService) {
qDebug() << QStringLiteral("I-ROWER wants to be subscribed only to FTMS service in order to send metrics")
@@ -612,7 +612,10 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
qDebug() << "DFIT_L_R found!";
} else if (device.name().toUpper().startsWith(QStringLiteral("I-ROWER"))) {
I_ROWER = true;
qDebug() << "I_ROWER found!";
qDebug() << "I_ROWER found!";
} else if (device.name().toUpper().startsWith(QStringLiteral("IROWER "))) {
ROWER = true;
qDebug() << "ROWER found!";
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
PM5 = true;
qDebug() << "PM5 found!";

View File

@@ -73,6 +73,7 @@ class ftmsrower : public rower {
bool WATER_ROWER = false;
bool DFIT_L_R = false;
bool I_ROWER = false;
bool ROWER = false;
QDateTime lastStroke = QDateTime::currentDateTime();
double lastStrokesCount = 0;

View File

@@ -927,6 +927,10 @@ void horizontreadmill::update() {
// updateDisplay(elapsed);
}
// this treadmill can't go below 1
if(mobvoi_tmp_treadmill && requestSpeed < 1)
requestSpeed = -1;
if (requestSpeed != -1) {
bool minSpeed =
fabs(requestSpeed - float_one_point_round(currentSpeed().value())) >= (minStepSpeed() - 0.09);
@@ -1216,7 +1220,7 @@ void horizontreadmill::forceSpeed(double requestSpeed) {
}
} else if (gattFTMSService) {
// for the Tecnogym Myrun
if(!anplus_treadmill && !trx3500_treadmill && !wellfit_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO) {
if(!anplus_treadmill && !trx3500_treadmill && !wellfit_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO && !T3G_PRO && !T3G_ELITE) {
uint8_t write[] = {FTMS_REQUEST_CONTROL};
writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), "requestControl", false,
false);
@@ -1286,7 +1290,7 @@ void horizontreadmill::forceIncline(double requestIncline) {
}
} else if (gattFTMSService) {
// for the Tecnogym Myrun
if(!anplus_treadmill && !trx3500_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO) {
if(!anplus_treadmill && !trx3500_treadmill && !mobvoi_tmp_treadmill && !SW_TREADMILL && !ICONCEPT_FTMS_treadmill && !YPOO_MINI_PRO && !T3G_PRO && !T3G_ELITE) {
uint8_t write[] = {FTMS_REQUEST_CONTROL};
writeCharacteristic(gattFTMSService, gattWriteCharControlPointId, write, sizeof(write), "requestControl", false,
false);
@@ -1432,6 +1436,44 @@ void horizontreadmill::forceIncline(double requestIncline) {
writeS[1] = 0x00;
writeS[2] = 0x00;
}
} else if(T3G_PRO || T3G_ELITE) {
if(requestInclination > 0 && requestInclination < 1) {
writeS[1] = 0x10;
writeS[2] = 0x00;
} else if(requestInclination >= 1 && requestInclination < 2) {
writeS[1] = 0x1a;
writeS[2] = 0x00;
} else if(requestInclination >= 2 && requestInclination < 3) {
writeS[1] = 0x24;
writeS[2] = 0x00;
} else if(requestInclination >= 3 && requestInclination < 4) {
writeS[1] = 0x2e;
writeS[2] = 0x00;
} else if(requestInclination >= 4 && requestInclination < 5) {
writeS[1] = 0x38;
writeS[2] = 0x00;
} else if(requestInclination >= 5 && requestInclination < 6) {
writeS[1] = 0x42;
writeS[2] = 0x00;
} else if(requestInclination >= 6 && requestInclination < 7) {
writeS[1] = 0x4c;
writeS[2] = 0x00;
} else if(requestInclination >= 7 && requestInclination < 8) {
writeS[1] = 0x56;
writeS[2] = 0x00;
} else if(requestInclination >= 8 && requestInclination < 9) {
writeS[1] = 0x60;
writeS[2] = 0x00;
} else if(requestInclination >= 9 && requestInclination < 10) {
writeS[1] = 0x6A;
writeS[2] = 0x00;
} else if(requestInclination >= 10 && requestInclination < 11) {
writeS[1] = 0x74;
writeS[2] = 0x00;
} else {
writeS[1] = 0x00;
writeS[2] = 0x00;
}
} else {
if(HORIZON_78AT_treadmill)
requestIncline = requestIncline / 2.0;
@@ -1741,6 +1783,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
}
} else if (characteristic.uuid() == QBluetoothUuid((quint16)0x2ACD)) {
bool horizon_treadmill_7_0_at_24 = settings.value(QZSettings::horizon_treadmill_7_0_at_24, QZSettings::default_horizon_treadmill_7_0_at_24).toBool();
lastPacket = newValue;
// default flags for this treadmill is 84 04
@@ -1781,7 +1824,8 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
// this treadmill sends the speed in miles!
speed *= miles_conversion;
}
parseSpeed(speed);
if(!mobvoi_tmp_treadmill || (mobvoi_tmp_treadmill && !horizonPaused))
parseSpeed(speed);
index += 2;
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
}
@@ -1914,16 +1958,37 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
} else {
emit debug(QStringLiteral("Error on parsing heart!"));
}
// index += 1; //NOTE: clang-analyzer-deadcode.DeadStores
index += 1;
}
}
if (Flags.metabolic) {
index += 1;
// todo
}
if (Flags.elapsedTime) {
// todo
if (index + 1 < newValue.length()) {
static uint16_t old_local_elapsed = 0;
static QDateTime last_local_elapsed_change = QDateTime::currentDateTime();
uint16_t local_elapsed = ((((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
qDebug() << QStringLiteral("Local Time Elapsed") << local_elapsed;
// this treadmill sends here if the user is really running
if(old_local_elapsed != local_elapsed) {
last_local_elapsed_change = QDateTime::currentDateTime();
horizonPaused = false;
}
QDateTime current_time = QDateTime::currentDateTime();
// Only if more than 2 seconds have passed since the last update
if(mobvoi_tmp_treadmill && last_local_elapsed_change.secsTo(current_time) >= 2) {
Speed = 0;
horizonPaused = true;
qDebug() << "Forcing Speed to 0 since the treadmill saws that the user is not really running";
}
old_local_elapsed = local_elapsed;
}
index += 2;
}
if (Flags.remainingTime) {
@@ -2489,6 +2554,12 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if (device.name().toUpper().startsWith(QStringLiteral("FIT-"))) {
qDebug() << QStringLiteral("FIT- found");
FIT = true;
} else if (device.name().toUpper().startsWith(QStringLiteral("3G PRO "))) {
qDebug() << QStringLiteral("3G PRO");
T3G_PRO = true;
} else if (device.name().toUpper().startsWith(QStringLiteral("3G ELITE "))) {
qDebug() << QStringLiteral("3G ELITE");
T3G_ELITE = true;
}
if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) {
@@ -2609,7 +2680,7 @@ bool horizontreadmill::autoStartWhenSpeedIsGreaterThenZero() {
// the horizon starts with a strange speed, since that i can auto start (maybe the best way to solve this
// is to understand why it's starting with this strange speed)
if (!horizon_paragon_x && !horizon_treadmill_7_8)
if (!horizon_paragon_x && !horizon_treadmill_7_8 && !mobvoi_tmp_treadmill)
return false;
if ((lastStop == 0 || QDateTime::currentMSecsSinceEpoch() > (lastStop + 25000)) && requestStop == -1)
@@ -3213,7 +3284,7 @@ void horizontreadmill::testProfileCRC() {
double horizontreadmill::minStepInclination() {
QSettings settings;
bool toorx_ftms_treadmill = settings.value(QZSettings::toorx_ftms_treadmill, QZSettings::default_toorx_ftms_treadmill).toBool();
if (kettler_treadmill || trx3500_treadmill || toorx_ftms_treadmill || sole_tt8_treadmill || ICONCEPT_FTMS_treadmill || SW_TREADMILL || sole_s77_treadmill || FIT)
if (kettler_treadmill || trx3500_treadmill || toorx_ftms_treadmill || sole_tt8_treadmill || ICONCEPT_FTMS_treadmill || SW_TREADMILL || sole_s77_treadmill || FIT || T3G_PRO || T3G_ELITE)
return 1.0;
else
return 0.5;

View File

@@ -108,6 +108,8 @@ class horizontreadmill : public treadmill {
bool YPOO_MINI_PRO = false;
bool MX_TM = false;
bool FIT = false;
bool T3G_PRO = false;
bool T3G_ELITE = false;
void testProfileCRC();
void updateProfileCRC();

View File

@@ -77,15 +77,13 @@ void kineticinroadbike::writeCharacteristic(uint8_t *data, uint8_t data_len, con
}
void kineticinroadbike::forceResistance(resistance_t requestResistance) {
/*uint8_t noOpData[] = {0xf0, 0xb1, 0x01, 0x00, 0x00};
noOpData[3] = requestResistance;
for (uint8_t i = 0; i < sizeof(noOpData) - 1; i++) {
noOpData[4] += noOpData[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(noOpData, sizeof(noOpData), QStringLiteral("force resistance"), false, true);*/
if (noWriteResistance) {
return;
}
// Use Smart Control brake mode for direct resistance control
smart_control_set_mode_brake_data cmd = smart_control_set_mode_brake_command(requestResistance);
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set brake resistance"), false, true);
}
void kineticinroadbike::update() {
@@ -108,6 +106,23 @@ void kineticinroadbike::update() {
// updateDisplay(elapsed);
}
if (requestInclination != -100) {
qDebug() << QStringLiteral("writing inclination ") + QString::number(requestInclination);
forceInclination(requestInclination);
if(!VirtualBike() || (VirtualBike() && !VirtualBike()->ftmsDeviceConnected())) {
Inclination = requestInclination;
}
requestInclination = -100;
requestResistance = -1; // Clear resistance request when handling inclination
}
if (requestPower != -1) {
qDebug() << QStringLiteral("writing power ") + QString::number(requestPower);
changePower(requestPower);
requestPower = -1;
requestResistance = -1; // Clear resistance request when handling power
}
if (requestResistance != -1) {
if (requestResistance > max_resistance)
requestResistance = max_resistance;
@@ -120,6 +135,7 @@ void kineticinroadbike::update() {
}
requestResistance = -1;
}
if (requestStart != -1) {
qDebug() << QStringLiteral("starting...");
@@ -136,6 +152,42 @@ void kineticinroadbike::update() {
}
}
void kineticinroadbike::changePower(int32_t power) {
if (noWriteResistance) {
return;
}
RequestedPower = power;
if (power < 0)
power = 0;
// Use Smart Control ERG mode for power-based training
smart_control_set_mode_erg_data cmd = smart_control_set_mode_erg_command((uint16_t)power);
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set ERG power"), false, true);
}
void kineticinroadbike::forceInclination(double inclination) {
if (noWriteResistance) {
return;
}
// Update resistance to match inclination (for UI consistency)
Resistance = inclination;
// Smart Control simulation parameters
QSettings settings;
float weightKG = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
float rollingCoeff = 0.004f; // Standard rolling resistance for asphalt
float windCoeff = 0.6f; // Standard wind resistance coefficient
float windSpeedMPS = 0.0f; // No headwind/tailwind
// Use Smart Control simulation mode for inclination control
smart_control_set_mode_simulation_data cmd = smart_control_set_mode_simulation_command(
weightKG, rollingCoeff, windCoeff, (float)inclination, windSpeedMPS);
writeCharacteristic(cmd.bytes, sizeof(cmd.bytes), QStringLiteral("set simulation inclination"), false, true);
}
void kineticinroadbike::serviceDiscovered(const QBluetoothUuid &gatt) {
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
}

View File

@@ -44,10 +44,11 @@ class kineticinroadbike : public bike {
resistance_t pelotonToBikeResistance(int pelotonResistance) override;
resistance_t maxResistance() override { return max_resistance; }
resistance_t resistanceFromPowerRequest(uint16_t power) override;
void changePower(int32_t power) override;
bool connected() override;
private:
const resistance_t max_resistance = 32;
const resistance_t max_resistance = 999;
double bikeResistanceToPeloton(double resistance);
double GetDistanceFromPacket(const QByteArray &packet);
QTime GetElapsedFromPacket(const QByteArray &packet);
@@ -56,6 +57,7 @@ class kineticinroadbike : public bike {
bool wait_for_response = false);
void startDiscover();
void forceResistance(resistance_t requestResistance);
void forceInclination(double inclination);
uint16_t watts() override;
QTimer *refresh;

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