Compare commits

...

850 Commits

Author SHA1 Message Date
Roberto Viola
bf2f6406ed try to have the sensor on garmin connect 2025-06-09 10:24:52 +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
ae280e170a SOLE LCR Bike #3226 2025-02-22 07:08:53 +01:00
Roberto Viola
d2dfb16033 Cyclotronics Smart Trainer #3223 2025-02-21 15:58:11 +01:00
Roberto Viola
64d99748c7 Proform XBike #3214 2025-02-21 14:16:51 +01:00
Roberto Viola
16d90a010b Proform XBike #3214 2025-02-21 13:02:36 +01:00
Roberto Viola
32baab9072 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-02-21 08:25:31 +01:00
Roberto Viola
31dd125263 Peloton API Login Issues (Issue #3217) 2025-02-21 08:25:24 +01:00
Roberto Viola
483fd87ee5 Update main.cpp 2025-02-20 20:03:09 +01:00
Roberto Viola
254786ea5d Gears don't work for mid-work free ride segment (Issue #2897)
https://github.com/cagnulein/qdomyos-zwift/issues/2897#issuecomment-2671383599
2025-02-20 14:52:59 +01:00
Roberto Viola
c06a439c0c Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-02-20 11:48:20 +01:00
Roberto Viola
7b76999c0d Fixing ios bluetooth crash (#3213)
* adding files

* fixing build

* Update project.pbxproj
2025-02-20 11:48:06 +01:00
Roberto Viola
5c0190dffe Titan 7000 #3218 2025-02-20 11:47:29 +01:00
Roberto Viola
aabd2824d3 “Lydsto S1” spinning bike 2025-02-20 10:11:03 +01:00
Roberto Viola
2f7033cd6d Support device JTX Cyclo 5 (FAL-SPORTS0234) (Issue #3208) 2025-02-20 08:24:59 +01:00
Roberto Viola
756fe823f8 “Lydsto S1” spinning bike 2025-02-20 08:16:35 +01:00
Roberto Viola
ed0d163944 Proform XBike #3214 2025-02-19 15:35:29 +01:00
Roberto Viola
b7ee025a6f SPAX-BK ERG not working #3200 2025-02-19 10:44:48 +01:00
Roberto Viola
23dca8ec93 added PELOTON_SECRET_KEY on github secrets 2025-02-19 09:07:33 +01:00
Roberto Viola
fe05cb613f Peloton oauth (#2632)
* starting

* builds?

* Update peloton.cpp

* Update settings.qml

* Update peloton.cpp

* Update peloton.cpp

* trying to login

* Update peloton.cpp

* Update peloton.cpp

* Update peloton.cpp

* workout api returns always void

* fixing auth header on workout

* Update peloton.cpp

* handling new cases

* Update peloton.cpp

* Update peloton.cpp

* Update peloton.cpp

* Update qzsettings.cpp

* adding the peloton connect button

* adding popup to switch to the new api and removed credentials from the settings

* fixing peloton popup

* Update main.qml

* everything should be fine now

* added peloton button on the settings page too

* Update project.pbxproj

* new kingsmith variant treadmill

* Cannot set Virtufit Etappe 2 auto resistance (Issue #3130)

* CycleOps Phantom 5 (Issue #3004)

* Nordictrack commercial 1750 incline calibration incorrect (Issue #3118)

* Update qzsettings.cpp

* airdate and current_pedaling_duration fixed

* fixing spinups in powerzone classes

* Update project.pbxproj

* Update project.pbxproj

* getting wattage and cadence directly from the zwift hub riding data if available

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* 2.18.19

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Update peloton.cpp

* Update project.pbxproj

* Row and Tread Target Pace Issues from 2.18.19 Update #3206
2025-02-19 09:03:02 +01:00
Roberto Viola
c56c6fe5e4 Issue with Elite Direto XR, zwift cog, zwift play, mywhoosh and zwift combo #3191 2025-02-19 08:54:03 +01:00
Roberto Viola
8f7fafa4f2 DIRETO XR limited at 600W #3193 2025-02-19 08:48:08 +01:00
Roberto Viola
ef9ca0bfc8 DIRETO XR limited at 600W #3193 2025-02-19 08:46:34 +01:00
Roberto Viola
34635114df QZ CRUSH AT CONNECTION INITIAL 100 DOMYOS (Issue #3197) 2025-02-19 08:30:07 +01:00
Roberto Viola
3e29dd63df Update proformbike.cpp 2025-02-18 21:21:15 +01:00
Roberto Viola
c575159616 Pro-Form, model PFTL99015.0 #3209 2025-02-18 16:52:35 +01:00
Roberto Viola
ddebfc7e75 Support device JTX Cyclo 5 (FAL-SPORTS0234) #3208 2025-02-18 15:47:41 +01:00
Roberto Viola
2b51e5982a Kettler tour 600 #3207 2025-02-18 14:47:36 +01:00
Roberto Viola
00b616f4f8 QZ Companion on NordicTrack T8.5S is unable to communicate with QZ Fitness #3187 2025-02-18 08:26:59 +01:00
Roberto Viola
38274e1056 QZ CRUSH AT CONNECTION INITIAL 100 DOMYOS (Issue #3197) 2025-02-18 08:16:18 +01:00
Roberto Viola
cf1397cb81 Connecting to Horizon 7.0 #3201 2025-02-17 15:25:40 +01:00
Roberto Viola
b23c1b46ab Log on Thread (#3189) 2025-02-17 15:11:21 +01:00
Roberto Viola
4750ee9214 No heart rate and negative calories when using Apple Watch & Elliptical Skandika Carbon P23 #3198 2025-02-17 12:14:33 +01:00
Roberto Viola
05b39acb3e Christopeit TM3000S treadmill 2025-02-17 10:47:42 +01:00
Roberto Viola
26d2a59ad5 QZ Connects to Merach R11 rower on iOS but no metrics/data are being captured (Issue #3190) 2025-02-17 10:22:47 +01:00
Roberto Viola
1ed382faef QZ CRUSH AT CONNECTION INITIAL 100 DOMYOS (Issue #3197) 2025-02-17 10:06:28 +01:00
Roberto Viola
ebbbd4febb Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-02-17 08:53:31 +01:00
Roberto Viola
95014c3863 No heart rate and negative calories when using Apple Watch & Elliptical Skandika Carbon P23 (Issue #3198) 2025-02-17 08:53:25 +01:00
Roberto Viola
b61f5752d2 Smart bike BH BR9110 #3192 2025-02-16 15:52:44 +01:00
Roberto Viola
4b7533d721 VANRYSEL-HT ftms bike 2025-02-16 15:49:23 +01:00
Roberto Viola
6519e9ae86 avoiding crash on tacxneo2 2025-02-16 09:21:48 +01:00
Roberto Viola
cbc3b9d292 MSVC Print Stack Trace (#3185) 2025-02-15 19:32:00 +01:00
Roberto Viola
1dde627a4c Sportstech sBike Lite 2025-02-14 17:34:16 +01:00
Roberto Viola
06727f23e4 Proform Bike PFEVEX71316.0 #3157 2025-02-14 14:30:55 +01:00
Roberto Viola
1c8279d2fc Treadmill and Power Sensor Speed Forcing (Issue #3152) 2025-02-13 10:19:24 +01:00
Roberto Viola
aa0193b41e Pafer treadmill #2985 2025-02-13 10:01:26 +01:00
Roberto Viola
fcf6a8b586 fixing 0 metrics to apple watch to ipad bridge 2025-02-13 08:55:33 +01:00
Roberto Viola
574c51bcec Does it support Crosstrainer Skandika Carbon P23? #3175 2025-02-13 08:03:07 +01:00
Roberto Viola
01fa8602a0 Trying to fix android crash 2025-02-12 20:44:19 +01:00
Roberto Viola
b006e8cc2b Update proformbike.cpp 2025-02-12 13:36:20 +01:00
Roberto Viola
e8486364a3 adding help to CLI 2025-02-12 12:15:02 +01:00
sirfergy
f72b6b04ce Enable setting power sensor via command line (#3106)
* Enable setting power sensor via command line

* Remove weird whitespace

* Update main.cpp

---------

Co-authored-by: Roberto Viola <Cagnulein@gmail.com>
2025-02-12 12:05:17 +01:00
Roberto Viola
1152b4d9b2 Proform Bike PFEVEX71316.0 #3157 2025-02-12 09:46:29 +01:00
Roberto Viola
219c4e2491 Treadmill and Power Sensor Speed Forcing (Issue #3152) 2025-02-12 08:39:41 +01:00
Roberto Viola
47d78f4464 Yesoul Treadmill support #3174 2025-02-12 08:33:12 +01:00
Roberto Viola
bebfd03ae9 Treadmill and Power Sensor Speed Forcing (Issue #3152) 2025-02-11 15:20:31 +01:00
Roberto Viola
0bf98491cb getting wattage and cadence directly from the zwift hub riding data if available 2025-02-11 15:05:28 +01:00
Roberto Viola
63e4c627b3 Proform Bike PFEVEX71316.0 #3157 2025-02-11 14:54:49 +01:00
Roberto Viola
a37e3c8287 getting wattage and cadence directly from the zwift hub riding data if available 2025-02-11 09:09:27 +00:00
Roberto Viola
45ab560d08 adding more info about upload on strava failed 2025-02-11 09:01:12 +01:00
Roberto Viola
3d1846cbe8 Proform Bike PFEVEX71316.0 #3157 2025-02-11 08:52:18 +01:00
Roberto Viola
936bbe2372 Treadmill and Power Sensor Speed Forcing #3152 2025-02-10 17:09:33 +01:00
Tomáš Janoušek
963a3fbb97 skillbike: allow 3-digit bikes (#3165)
My TechnoGym Skillbike is called "BIKE 861" and QZ wouldn't detect it
previously because it assumed all such bikes have 4-digit names.

The fix is to relax requirement and detect any /BIKE \d+/ as
technogymBike.
2025-02-10 16:39:28 +01:00
Roberto Viola
b4161da81a Run Elevation Written By Zwift Elevation Data Instead Of Treadmill Data #3160 2025-02-10 16:33:58 +01:00
Roberto Viola
be65d915e3 Proform Bike PFEVEX71316.0 #3157 2025-02-10 10:08:00 +01:00
Roberto Viola
22fb9df723 Treadmill and Power Sensor Usage #3152 2025-02-10 09:28:28 +01:00
Roberto Viola
8709f81b16 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-02-09 18:28:46 +01:00
Roberto Viola
347ede62b2 adding debug on the ipad bridge 2025-02-09 18:28:25 +01:00
Roberto Viola
c4a913d317 Yoroto rower 2025-02-09 15:42:56 +01:00
Roberto Viola
1706fde7ab No power when pedalling with Van Rysel D500 turbo trainer. #3065 (#3083) 2025-02-08 17:54:21 +01:00
Roberto Viola
60e8d37624 fixing-raspberry-64bit-segfault-build (#3084)
* Update main.yml

* Update main.yml

* Update main.yml

https://github.com/docker/setup-qemu-action/issues/188

* 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
2025-02-08 11:33:05 +01:00
Roberto Viola
519bc38fb6 sole f85 doesn't handle 0.5 inclination step 2025-02-07 21:22:08 +01:00
Roberto Viola
375d223f66 fixed domyos bike settings 2025-02-07 20:37:33 +01:00
Roberto Viola
f16e0649cb added a setting to enable the ignore ftms setting for domyos bikes 2025-02-07 18:47:47 +01:00
Roberto Viola
9e685cde37 Proform Bike PFEVEX71316.0 #3157 2025-02-07 10:31:48 +01:00
Roberto Viola
d32d7cd802 Rouvy: Virtual shifting with Zwift gearing, noticeably harder than physical gearing #3031
https://github.com/cagnulein/qdomyos-zwift/issues/3031#issuecomment-2642230942
2025-02-07 09:24:29 +01:00
Roberto Viola
8961ca860a adding preset buttons for power zones 2025-02-06 09:55:07 +01:00
Roberto Viola
9a3fa7c82f fix crash on iOS 2025-02-05 15:44:46 +01:00
Roberto Viola
183f36bf40 Polar OH1 not connecting consistently with Android tablet / Qdomyos-swift app #3139 (#3144) 2025-02-05 13:08:23 +01:00
Roberto Viola
bac91d14c6 YESOUL G1M Max bike #3149 2025-02-05 10:27:09 +01:00
Roberto Viola
7455729225 Elite Drivo II 2025-02-05 08:56:44 +01:00
Roberto Viola
223b6b7a0e manually adjusting the resistance, incorrect behavior! (Issue #3145) 2025-02-04 16:35:47 +01:00
Roberto Viola
825555a34f Upload Strava from WIndows crash (#3143)
* Update homeform.cpp

* aggiunta /RTC1

* Update qdomyos-zwift.pri
2025-02-04 12:47:28 +01:00
Roberto Viola
aeead83510 Nordictrack commercial 1750 incline calibration incorrect (Issue #3118) 2025-02-04 08:34:25 +00:00
Roberto Viola
78a8981006 CycleOps Phantom 5 (Issue #3004) 2025-02-04 07:29:54 +00:00
Roberto Viola
e9bb6bc73b Cannot set Virtufit Etappe 2 auto resistance (Issue #3130) 2025-02-04 07:23:49 +00:00
Roberto Viola
d2354074f8 new kingsmith variant treadmill 2025-02-04 07:21:02 +00:00
Roberto Viola
78ee43cb7d commenting 64bit build image for now 2025-02-03 08:37:11 +01:00
Roberto Viola
952fc914fb QZ crushes right after connection to MERACH S01 (Issue #3136) 2025-02-03 08:35:01 +01:00
Roberto Viola
bc54203cdf Elito Avanti added 2025-02-02 16:01:53 +01:00
Gerd Naschenweng
854846585a Update 10_Installation.md (#3129) 2025-02-01 09:38:38 +01:00
Roberto Viola
309bfb623b preparing for #2632 2025-01-31 16:35:31 +01:00
Roberto Viola
9a99740701 BT Log share for LifeSpan-TM-2000 #3021 2025-01-31 10:47:54 +01:00
Roberto Viola
1f299a1ff1 BT Log share for LifeSpan-TM-2000 #3021 2025-01-31 10:33:52 +01:00
Roberto Viola
8bce3d0541 version 2.18.18 2025-01-31 09:36:34 +01:00
Roberto Viola
06d033d13c adding debug to connection to apple watch 2025-01-31 07:41:05 +01:00
Roberto Viola
654b070c7e Update ftmsbike.cpp
clean time in case for a long period we don't receive values
2025-01-30 16:04:27 +01:00
Roberto Viola
a159c8f072 Technogym Elliptical #3121 2025-01-30 14:54:52 +01:00
Roberto Viola
3846d974af Inclination stops updating in app after about 30 minutes (but treadmill adjustment still works) (Issue #2992) 2025-01-30 11:27:09 +01:00
Roberto Viola
7e5fcfb881 adding some logs on the homeform update to understand the lag on some androids 2025-01-30 10:46:54 +01:00
Roberto Viola
4e9cafcd5e Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-01-29 21:18:13 +01:00
Roberto Viola
15f4fc51ce adding watchos symbols 2025-01-29 21:17:55 +01:00
Roberto Viola
d343c1c98c Tacx Neo 3M #3062 2025-01-29 11:34:23 +01:00
Roberto Viola
4a17a8474a CycleOps Phantom 5 #3004 2025-01-29 06:40:31 +01:00
Roberto Viola
2bb1ff0b09 BT Log share for LifeSpan-TM-2000 #3021 2025-01-28 16:39:21 +01:00
Roberto Viola
dec5bd6603 Update bluetooth.cpp 2025-01-28 16:00:18 +01:00
Roberto Viola
282f01b55d Possible bug with NT C2950 IP UDP metrics? (Issue #3079) 2025-01-28 14:42:03 +01:00
Roberto Viola
349be00771 BT Log share for LifeSpan-TM-2000 (Issue #3021) 2025-01-28 11:06:45 +01:00
Roberto Viola
adc47fd19c Update project.pbxproj 2025-01-28 08:45:09 +01:00
Roberto Viola
e876ef97cd CycleOps Phantom 5 #3004 2025-01-28 08:36:28 +01:00
Roberto Viola
903409d962 Senator iPlus treadmill (Issue #3114) 2025-01-28 08:28:08 +01:00
Roberto Viola
9295554195 Tunturi E60 Signature bike #3115 2025-01-28 08:25:59 +01:00
Roberto Viola
8d6cfe03ac Virtufit etappe setting restored 2025-01-27 20:30:42 +01:00
Roberto Viola
70d5051a6f Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-01-26 16:42:36 +01:00
Roberto Viola
124e4ec561 adding in also on WIN32 on qfit 2025-01-26 16:42:34 +01:00
Roberto Viola
036db83321 Fixing crash on windows about fit file writing (#3095)
* Update qfit.cpp

* Update qfit.cpp
2025-01-26 06:15:48 +01:00
Roberto Viola
23dfa67fe5 On iOS devices, the Domyos elliptical bike reads astronomical values above 11km/h. (Issue #3099) 2025-01-25 20:12:09 +01:00
Roberto Viola
79d7a09203 Technogym Skillrun #3097 2025-01-25 14:59:59 +01:00
Roberto Viola
96d9fb485b Create qdomyos-zwift.code-workspace 2025-01-24 13:30:52 +01:00
Roberto Viola
68c4d954ef Create launch.json 2025-01-24 13:28:49 +01:00
Roberto Viola
ef7bedacb8 adding the pdb to win msvc 2025-01-24 12:33:30 +01:00
Roberto Viola
e7a1373305 Update proformtreadmill.cpp 2025-01-24 10:58:28 +01:00
Roberto Viola
cfd06df25e Solution to get Octane Zero Runner ZR8 Elliptical working with Zwift #1338 2025-01-24 08:28:56 +01:00
Roberto Viola
89bc6d0529 Support for Proform 705 CST treadmill (Issue #3072) 2025-01-23 14:49:34 +01:00
Roberto Viola
0446000270 BT Log share for LifeSpan-TM-2000 (Issue #3021) 2025-01-23 14:09:30 +01:00
Roberto Viola
9908e8ca98 Update project.pbxproj 2025-01-23 09:23:11 +01:00
Roberto Viola
326f09c903 BT Log share for LifeSpan-TM-2000 (Issue #3021) 2025-01-22 17:00:07 +01:00
Roberto Viola
2b52206795 improving training effect on garmin
https://github.com/dvmarinoff/Auuki/issues/231#issuecomment-2606479981
2025-01-22 12:07:46 +01:00
Roberto Viola
4cadcddac1 Inclination stops updating in app after about 30 minutes (but treadmill adjustment still works) #2992 2025-01-22 11:52:55 +01:00
Roberto Viola
8910b8bf28 Update main.yml 2025-01-22 10:58:07 +01:00
Roberto Viola
fc3287758e Please add Treadmill: LifeSmart TM4500 #3074 2025-01-22 09:30:36 +01:00
Roberto Viola
c94a03bb23 Update main.yml 2025-01-22 09:24:55 +01:00
Roberto Viola
2c5ba21b99 Update main.yml 2025-01-21 14:14:15 +01:00
Roberto Viola
4f00550400 Update cycleopsphantombike.cpp 2025-01-21 10:56:47 +01:00
Roberto Viola
dfd622c948 Inclination stops updating in app after about 30 minutes (but treadmill adjustment still works) #2992 2025-01-21 08:39:57 +01:00
Roberto Viola
a7d66727f3 Update project.pbxproj 2025-01-20 15:19:39 +01:00
Roberto Viola
06a5c412bd XT385: giving the possibility to use FTMS 2025-01-20 15:14:22 +01:00
Roberto Viola
0a3616ec0e Inclination stops updating in app after about 30 minutes (but treadmill adjustment still works) (Issue #2992) 2025-01-20 09:50:08 +01:00
Roberto Viola
b4226306b0 Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2025-01-20 09:21:14 +01:00
Roberto Viola
4f3353303a Tacx Neo 3M #3062 2025-01-19 16:31:05 +01:00
Roberto Viola
81b832071a Recent compile on master broadcasts device on RaspberryPI as "Pixel 6a" (Issue #3063) 2025-01-19 10:51:49 +01:00
Roberto Viola
d1966df73c Tacx Neo 3m #3062 2025-01-18 21:03:12 +01:00
Roberto Viola
e194291efb Lifespan Fitness SM-720i #3061 2025-01-18 16:23:14 +01:00
Roberto Viola
af88f6cd0d Update project.pbxproj 2025-01-18 08:24:55 +01:00
Roberto Viola
62838da761 Update cycleopsphantombike.cpp 2025-01-18 08:18:58 +01:00
David Mason
7236608f59 Tests for Cyclops Phantom and Pitpat Bikes (#3056) 2025-01-18 08:01:36 +01:00
David Mason
2570f2843c Test timeout exploration (#3048) 2025-01-18 08:00:53 +01:00
Roberto Viola
a9fe9bebaf Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-01-17 13:57:21 +01:00
Roberto Viola
15a7c3abd0 BT Log share for LifeSpan-TM-2000 #3021 2025-01-17 13:57:15 +01:00
Roberto Viola
7872950f65 Update project.pbxproj 2025-01-17 12:00:59 +01:00
Roberto Viola
4a711368e3 Inclination stops updating in app after about 30 minutes (but treadmill adjustment still works) (Issue #2992) 2025-01-17 11:51:45 +01:00
Roberto Viola
fbcc7e4478 BT Log share for LifeSpan-TM-2000 #3021 2025-01-17 09:12:03 +01:00
Roberto Viola
e23af2e5f5 Windows: FIT FILE Save Issue (#3053)
* trying fix

* useless?

* temp file?

* Update qfit.cpp

* Update qfit.cpp

* Revert "trying fix"

This reverts commit 82c752d26f.

* Update qfit.cpp
2025-01-16 10:24:42 +01:00
Roberto Viola
9c6fed4d48 Update project.pbxproj 2025-01-15 22:50:03 +01:00
Roberto Viola
26ac25d3ba Read cadence from Garmin when in -run-cadence-sensor mode for FTMS treadmills (Issue #3042) 2025-01-15 22:49:04 +01:00
Roberto Viola
19beae66bb Update project.pbxproj 2025-01-15 08:38:09 +01:00
Roberto Viola
037f660825 Virtual shifting overshoots/undershoots target resistance, then bounces to expected resistance (Issue #3051) #3031 2025-01-15 08:08:52 +01:00
Roberto Viola
47e719bff0 Commenting temporary Linux tests 2025-01-15 06:25:05 +01:00
Roberto Viola
020f30d8df Option to swap the virtual gear shift buttons in the UI #3049 2025-01-14 13:01:34 +01:00
Roberto Viola
7078508ba9 2.18.17 2025-01-14 10:42:30 +01:00
Roberto Viola
eb002332ed Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2025-01-14 10:28:46 +01:00
Roberto Viola
ea1da07e71 Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2025-01-14 10:06:14 +01:00
Roberto Viola
e1d32cd747 Profiles can't be selected (Issue #3045) 2025-01-14 08:20:42 +01:00
Roberto Viola
1bb3450512 2.18.16 2025-01-13 14:22:55 +01:00
Roberto Viola
ce1a78156e QZ iOS beta app closes and shuts down (Issue #3013) 2025-01-13 11:23:14 +01:00
Roberto Viola
9d95e52d12 Garmin discovers but can't connect to cadence sensor (Issue #3023) (#3038) 2025-01-12 19:40:29 +01:00
Roberto Viola
73c072583a fixing build 2025-01-12 18:12:25 +01:00
Roberto Viola
253e2b7eab Update project.pbxproj 2025-01-12 18:07:38 +01:00
Roberto Viola
650c6de692 Rouvy: Virtual shifting with Zwift gearing, noticeably harder than physical gearing (Issue #3031) 2025-01-12 18:04:51 +01:00
Roberto Viola
1ac4e20efb Update project.pbxproj 2025-01-12 17:58:11 +01:00
Roberto Viola
f08ea4346e BT Log share for LifeSpan-TM-2000 #3021 2025-01-12 16:13:14 +01:00
Roberto Viola
c206886639 BT Log share for LifeSpan-TM-2000 (Issue #3021) 2025-01-12 16:11:25 +01:00
Roberto Viola
61bf953b1a Update project.pbxproj 2025-01-12 14:55:14 +01:00
Roberto Viola
1dcd35e825 Watts Stuck at Max…not going down to 0 (Issue #3025) 2025-01-12 14:46:13 +01:00
Roberto Viola
5a7bb8b103 Horizon cycle 7.0IC-02 no resistance adjustment in Kinomap via QZ app #3035 2025-01-12 14:43:40 +01:00
Roberto Viola
c4be4f068f fixing virtual device menu not loaded 2025-01-12 14:42:23 +01:00
Roberto Viola
4534c334bc Create build-qrc-qml.sh 2025-01-12 09:07:08 +01:00
Roberto Viola
9fa6d6d8b1 auto set speed to 3km/h when using auto inclination (Issue #3034) 2025-01-11 19:24:47 +01:00
sirfergy
a5b34161c1 Missed a character! (#3027) 2025-01-10 22:41:53 +01:00
sirfergy
bf2c6929e1 Add two options to set horizon treadmill settings (#3026) 2025-01-10 21:35:49 +01:00
Roberto Viola
2a451c3120 Proform Trainer 8.0 No Bluetooth control (Issue #3017) 2025-01-10 19:30:22 +01:00
Roberto Viola
1169714908 Resistance scaling kickr core 4303 (Peloton App) (Issue #3020) 2025-01-10 19:03:35 +01:00
Roberto Viola
14de4e4760 Proform Trainer 8.0 No Bluetooth control (Issue #3017) 2025-01-10 13:47:12 +01:00
Roberto Viola
712f527ce0 Update project.pbxproj 2025-01-09 12:40:56 +01:00
Roberto Viola
0631c64ba5 Update bluetooth.cpp 2025-01-09 12:37:32 +01:00
Roberto Viola
85c43db53e cscbike: speed based on power setting enable 2025-01-09 12:37:03 +01:00
Roberto Viola
8394bf3f19 Yesoul v1 FMTS bike #2186 2025-01-09 12:21:37 +01:00
Roberto Viola
bd1f25f016 2.18.15 2025-01-08 14:25:13 +01:00
Roberto Viola
95f340063a Cant control ProForm 505 CST through QZ app or Zwift #3005 2025-01-08 11:11:37 +01:00
Roberto Viola
2be1d82e8d Update project.pbxproj 2025-01-08 09:58:16 +01:00
Roberto Viola
501af18298 CycleOps Phantom 5 #3004 2025-01-08 08:30:25 +01:00
Roberto Viola
724292bd34 Hammer Speed Race X resistance change not working correctly #3002 2025-01-08 08:10:27 +01:00
Roberto Viola
cbbdebdf84 Inclination stops updating in app after about 30 minutes (but treadmill adjustment still works) #2992 2025-01-08 08:01:13 +01:00
Roberto Viola
02c17dcf55 starting CycleOps Phantom 5 #3004 2025-01-07 17:41:22 +01:00
Roberto Viola
23d1f9d8c0 No data from Domyos Training Bike 900 #2973 2025-01-07 15:03:26 +01:00
Roberto Viola
f4e0d3596d Horizon 5.0 Bike Compatibility #3001 2025-01-07 11:20:25 +01:00
Roberto Viola
3b012bc946 Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2025-01-07 09:21:23 +01:00
Roberto Viola
33a5a2c80f Horizon 5.0 Bike Compatibility #3001 2025-01-07 09:06:53 +01:00
Roberto Viola
e8b481d517 S22i connects and reports info to QZ, but Auto-Resistance is not being controlled by QZ App (Issue #2909) 2025-01-05 09:21:19 +01:00
Roberto Viola
dcfa58b3a9 Horizon 5.0u bike #2984 2025-01-04 20:04:24 +01:00
Roberto Viola
fd4106cf00 No data from Domyos Training Bike 900 #2973 2025-01-04 05:43:31 +01:00
Roberto Viola
87dddac5f4 added garmin_bluetooth_compatibility for treadmills 2025-01-04 05:26:51 +01:00
Roberto Viola
5488af7e35 DeerRun S500 Bike Integration #2932 2025-01-02 14:48:03 +01:00
Roberto Viola
0a3bd56f15 Care Fitness Rowing no data in QZ (Issue #2874) 2025-01-02 14:37:21 +01:00
Roberto Viola
a5ae8f994b Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2025-01-02 14:32:12 +01:00
Roberto Viola
6a0b3e7fc4 DeerRun S500 Bike Integration #2932 2025-01-01 14:32:15 +01:00
Gerd Naschenweng
a7620c38d0 Included QZ service monitoring (#2960) 2024-12-30 11:23:09 +01:00
Roberto Viola
0060e316dc Update project.pbxproj 2024-12-29 10:59:31 +01:00
Roberto Viola
b71321f301 Treadmill Live Charts #2955 2024-12-29 10:57:20 +01:00
Roberto Viola
c99ef80d78 Speed up Settings page (#2951)
* it works, but i need to check all the accordionelement

* fixing layouts

* Update settings.qml
2024-12-28 22:17:49 +01:00
Roberto Viola
2adf3fe27b SwitchDeletages in the settings now are changing only if the user clicks on the indicator (ios default behaviour) 2024-12-28 18:51:24 +01:00
Roberto Viola
ade033eb59 Tactile Feedback for Zwift Play controllers (and Ride?) #2752 2024-12-28 18:22:11 +01:00
Roberto Viola
42666cf1e9 Update project.pbxproj 2024-12-28 14:39:17 +01:00
Roberto Viola
530f11f67c Tactile Feedback for Zwift Play controllers (and Ride?) (Issue #2752) 2024-12-28 13:48:57 +01:00
Roberto Viola
0391db60aa Raspberry kickr run (#2941) 2024-12-28 13:03:57 +01:00
Roberto Viola
486c90a112 Tactile Feedback for Zwift Play controllers (and Ride?) (Issue #2752) (#2753)
* Tactile Feedback for Zwift Play controllers (and Ride?) (Issue #2752)

* it works!

* vibrate on the right controller
2024-12-28 11:45:13 +01:00
Roberto Viola
d1767797d7 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-12-27 17:41:25 +01:00
Roberto Viola
fbe03d23f3 Smart Trainer Magene T200 #2947 2024-12-27 17:39:53 +01:00
Roberto Viola
361280c131 Issue with QZ 2.18 (982) and Zwift Play in SIM Mode (Issue #2944) 2024-12-27 17:35:43 +01:00
Roberto Viola
04e0fc6e7c Update README.md 2024-12-27 14:25:24 +01:00
Roberto Viola
ab52eee127 Issue with QZ 2.18 (982) and Zwift Play in SIM Mode #2944 2024-12-27 13:50:15 +01:00
Roberto Viola
94825252f7 Proform 1500 Pro Treadmill #2943 2024-12-27 10:23:14 +01:00
Roberto Viola
93f13817be Proform 1500 Pro Treadmill #2943 2024-12-27 10:14:41 +01:00
Roberto Viola
739ea4e841 Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-12-26 14:44:12 +01:00
Roberto Viola
fe3ad9ffb4 Open Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2024-12-26 14:25:58 +01:00
Roberto Viola
8fce809ee9 Update project.pbxproj 2024-12-26 14:10:21 +01:00
Roberto Viola
c156cbff99 DeerRun S500 Bike Integration (Issue #2932) 2024-12-26 14:01:39 +01:00
Roberto Viola
268be8e0f5 Delay in Strava Upload and Treadmill Pace Coloring Off #2933 2024-12-26 13:11:32 +01:00
Roberto Viola
5581e1c0e1 Update project.pbxproj 2024-12-25 11:19:23 +01:00
Roberto Viola
7fea2d442f Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-12-25 11:17:52 +01:00
Roberto Viola
74276764a6 2.18.12 2024-12-25 10:45:10 +01:00
Roberto Viola
a3e54782bb Peloton Treadmill Pace Levels #2469 2024-12-25 10:41:11 +01:00
Roberto Viola
b7bc80b2a3 Update project.pbxproj 2024-12-25 10:34:45 +01:00
Roberto Viola
b869a41f3d fixing build 2024-12-25 10:28:52 +01:00
Roberto Viola
9c7954945f Update kineticinroadbike.cpp 2024-12-24 12:30:50 +01:00
Roberto Viola
13cd666718 fixing msvc 2024-12-24 12:23:59 +01:00
Roberto Viola
c3e627e85b Update SmartControl.cpp 2024-12-24 12:02:13 +01:00
Roberto Viola
f23c24ae9b fixing build 2024-12-24 11:53:47 +01:00
Roberto Viola
d27da35beb fixing build 2024-12-24 11:49:40 +01:00
Roberto Viola
6457b205e4 fixing build 2024-12-24 11:42:56 +01:00
Roberto Viola
bea7b61dcc adding the original kineticinroad bike sdk 2024-12-24 11:16:38 +01:00
Roberto Viola
2cc8d51a6c Update project.pbxproj 2024-12-24 10:19:26 +01:00
Roberto Viola
5410b806bb Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-12-24 10:16:15 +01:00
Roberto Viola
b937d8bd71 Pooboo Bike #2935 2024-12-24 09:50:50 +01:00
Roberto Viola
cd25cfab8e 2.18.11 2024-12-23 18:54:53 +01:00
Roberto Viola
229e6ad461 Fixing build 2024-12-23 11:02:33 +01:00
Roberto Viola
977cae1cbd Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-12-23 10:09:49 +01:00
Roberto Viola
c8a9be2ca6 Revert "using variables instead of timer"
This reverts commit 445646fe02.
2024-12-23 10:04:05 +01:00
Roberto Viola
c3acf82a9b Revert "everal Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541)"
This reverts commit ddfc60bbf5.
2024-12-23 10:03:57 +01:00
Roberto Viola
ddfc60bbf5 everal Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541)
https://github.com/cagnulein/qdomyos-zwift/issues/2541#issuecomment-2557707654
2024-12-21 16:24:52 +01:00
Roberto Viola
445646fe02 using variables instead of timer
Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541)
2024-12-21 16:09:48 +01:00
Roberto Viola
3dd3c8fb40 Update project.pbxproj 2024-12-21 16:00:45 +01:00
Roberto Viola
fb390b3618 Revert "Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541)"
This reverts commit f6f9a95f06.
2024-12-21 15:12:59 +01:00
Roberto Viola
ca5fb75f3a Revert "handling ERG mode for VFSPINBIKE"
This reverts commit e881ce5f0f.
2024-12-20 14:30:26 +01:00
Roberto Viola
e881ce5f0f handling ERG mode for VFSPINBIKE 2024-12-20 12:21:22 +01:00
Roberto Viola
8002e47551 VFSPINBIKE model workaround for ERG mode started 2024-12-20 08:43:53 +01:00
Roberto Viola
5b66b5705d Support for iConsole based rowing machines, e.g. the "Baltic Rower Pro" (Issue #2901) 2024-12-19 15:17:43 +01:00
Roberto Viola
d1c5521d2a setting to disable the treadmill tag to have the inclination on strava 2024-12-19 10:14:29 +01:00
Roberto Viola
74151edfb3 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-12-19 10:00:10 +01:00
Roberto Viola
00f6747d7d Toputure TP1 treadmill #2918 2024-12-19 10:00:05 +01:00
Roberto Viola
0101955ad3 updating usb serial for android
first build on mac pro for android
2024-12-19 09:58:34 +01:00
Roberto Viola
f6f9a95f06 Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541)
https://github.com/cagnulein/qdomyos-zwift/issues/2541#issuecomment-2551961488
2024-12-18 21:39:29 +01:00
Roberto Viola
3d82b89db0 Sole S77 Support #1283 2024-12-18 21:04:54 +01:00
Roberto Viola
8c7b549a45 Sole S77 Support #1283 2024-12-18 19:02:56 +01:00
Roberto Viola
3ad4dc1cfe Sole S77 Support #1283 2024-12-18 18:59:42 +01:00
Roberto Viola
7524314f74 version 2.18.10 2024-12-18 15:17:29 +01:00
Roberto Viola
94545e8958 Update project.pbxproj 2024-12-18 15:15:29 +01:00
Roberto Viola
2c74b2d2e2 nextrow for power training
https://github.com/cagnulein/qdomyos-zwift/issues/2915#issuecomment-2551377819
2024-12-18 14:59:01 +01:00
Roberto Viola
108c190254 S22i connects and reports info to QZ, but Auto-Resistance is not being controlled by QZ App (Issue #2909) 2024-12-18 14:55:26 +01:00
Roberto Viola
466209307e heart rate and wahoo kickr fan toast when connected 2024-12-18 14:34:33 +01:00
Roberto Viola
acccba59dc fixed overlapping lines in the nextrow
https://github.com/cagnulein/qdomyos-zwift/issues/2915#issuecomment-2551013411
2024-12-18 12:12:07 +01:00
Roberto Viola
9325e2f9d1 handling distance for next rows
https://github.com/cagnulein/qdomyos-zwift/issues/2915#issuecomment-2550839841
2024-12-18 10:42:48 +01:00
Roberto Viola
36ebff2667 handling repeat tag in the xml
tunturi t90 inclination not working
#2915
2024-12-18 10:04:26 +01:00
Roberto Viola
6d0d08b5fb OSC: Open Sound Control (#2449)
* starting

* hardcoding ip

* adding fields

* adding setting

* added /QZ/Resistance on write

* finalizing!

* Update osc.cpp

* Update homeform.h

* Update osc.h

* fixing settings

* port added

* Update osc.cpp
2024-12-18 08:41:27 +01:00
Roberto Viola
e695a1e291 tunturi t90 inclination not working #2915 2024-12-17 19:00:59 +01:00
Roberto Viola
133488221b Update kineticinroadbike.cpp 2024-12-17 09:48:57 +01:00
Roberto Viola
b186b672ea kineticinroadbike 2024-12-14 14:04:19 +01:00
Roberto Viola
2badef3daf YPOO-mini pro treadmill #2905 2024-12-14 09:11:27 +01:00
Roberto Viola
f8700296fb Update kineticinroadbike.cpp 2024-12-13 21:32:41 +01:00
Roberto Viola
0f79fb56c7 Peloton Treadmill Pace Levels #2469 2024-12-13 20:44:22 +01:00
Roberto Viola
d8412c95d4 only if a resistance value is greater than 0 will set the resistance received in the ftms bike 2024-12-13 17:23:44 +01:00
Roberto Viola
469c239eed typo on the settings 2024-12-13 17:12:56 +01:00
Roberto Viola
7fad542553 YPOO-mini pro treadmill #2905 2024-12-13 15:21:03 +01:00
Roberto Viola
d0c0aeab84 Update project.pbxproj 2024-12-13 14:55:01 +01:00
Roberto Viola
9fd7123649 Peloton Treadmill Pace Levels #2469 2024-12-13 14:28:32 +01:00
Roberto Viola
5b922043ec YPOO-mini pro treadmill #2905 2024-12-13 13:36:09 +01:00
Roberto Viola
2953589ece fixing build 2024-12-13 12:02:39 +01:00
Roberto Viola
5836990903 Update kineticinroadbike.cpp 2024-12-13 10:44:50 +01:00
Roberto Viola
acd7e24382 kinetic inroad bike
https://github.com/kinetic-fit/kinetic-sdk-java/blob/master/com/kinetic/sdk/inride/InRide.java
2024-12-13 10:11:57 +01:00
Roberto Viola
71827e0546 Update project.pbxproj 2024-12-11 12:11:44 +01:00
Roberto Viola
7e8139e5a5 Treadmill incline multiplied on QZ output [BUG] #2511
ProAction Nydo (BH Fitness) threadmill doesent incline
2024-12-11 09:54:26 +01:00
Roberto Viola
20d2b6ec9e Zycle ZPro #2899 2024-12-11 09:30:32 +01:00
Roberto Viola
be7d0e58a7 Care Fitness Rowing no data in QZ (Issue #2874) 2024-12-11 08:49:23 +01:00
nix155
f20c449279 Fixed VNC server functionality (#2898)
Co-authored-by: Andrey Zotov <azotov@teko.io>
2024-12-10 17:04:08 +01:00
Roberto Viola
bf059715ec Care Fitness Rowing no data in QZ (Issue #2874) 2024-12-10 11:17:53 +01:00
Roberto Viola
98cd3f22a2 unable to connect to NordicTrack 7i #2838 2024-12-09 10:32:51 +01:00
Roberto Viola
bad290d104 Data fields not updating Trojan Pro Spin Bike 2.0, iConsole #2884 2024-12-09 10:24:04 +01:00
Roberto Viola
3c55d025ce Update ftmsbike.cpp (#2888) 2024-12-09 07:32:03 +01:00
nix155
5c775ac5b4 Added build with qtwebglplugin (#2879)
Co-authored-by: Andrey Zotov <azotov@teko.io>
2024-12-05 12:26:56 +01:00
Roberto Viola
9295aa58a7 Care Fitness Rowing no data in QZ #2874 2024-12-05 12:20:58 +01:00
Roberto Viola
96d68bbd39 Update project.pbxproj 2024-12-05 10:29:00 +01:00
Roberto Viola
7ddb6bc1ca Bowflex T9 Not Starting When Connected #2866 2024-12-05 08:56:24 +01:00
Roberto Viola
a0145793a2 Question about treadmill powerzones (Discussion #2873) 2024-12-04 15:45:16 +01:00
Roberto Viola
ecb37d67cc Revert "Question about treadmill powerzones (Discussion #2873)"
This reverts commit 3ae203d7ad.
2024-12-04 14:33:31 +01:00
Roberto Viola
969476f368 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-12-04 13:32:17 +01:00
Roberto Viola
3ae203d7ad Question about treadmill powerzones (Discussion #2873) 2024-12-04 13:32:12 +01:00
Roberto Viola
e979b5aebe Update project.pbxproj 2024-12-04 09:01:46 +01:00
Roberto Viola
d568bccc28 Bowflex T9 Not Starting When Connected #2866 2024-12-04 08:59:51 +01:00
nix155
301429182d Added multi-stage build, unnecessary files removed from the image (#2870) 2024-12-04 06:39:18 +01:00
nix155
8df78b9387 Added files for building and running in Docker with GUI (#2868)
Co-authored-by: Andrey Zotov <azotov@teko.io>
2024-12-03 18:19:55 +01:00
Roberto Viola
ba57309bcd Bowflex T9 Not Starting When Connected #2866 2024-12-03 18:12:54 +01:00
Roberto Viola
8d573b3ee6 Incline from GPX will be rounded to inclination step
Fitness Master T25 incline doesn't work [BUG] #2820
2024-12-03 17:09:23 +01:00
Marcel
ba43ba8c21 Add Life Fitness 95 CSAFE Ellipical (#2863)
* Add Life Fitness 95 CSAFE Ellipical

* Change bautrate type

* Fix data baudrate once more

* alternative way of setting level

* fix windows serial

* fix u_int16_t

* reorder header files

* Fix header setup

* multiple command refresh

* increment allSettingsCount

* Fix android build

* update kalman filter parameters

* formatting fixes

* More formatting fixes

* add setting version
2024-12-03 14:48:28 +01:00
Roberto Viola
47a3c24b03 2.18.9 2024-12-03 14:23:06 +01:00
Roberto Viola
40579fd376 improving safety on mediabuttonreceiver 2024-12-03 14:22:11 +01:00
Roberto Viola
bb17c1cc1a Inclination Gain implementation in QZ for Elite Suito/Rouvy #2850 (#2858) 2024-12-03 11:21:51 +01:00
Roberto Viola
1cc8862a04 proform carbon TL PFTL59722c.0 (Issue #2806) 2024-12-03 10:32:23 +01:00
Roberto Viola
ff4606caa4 Yosuda RC-Max #2861 2024-12-03 08:58:37 +01:00
Roberto Viola
aff12a0462 Bowflex T9 #2860 2024-12-03 08:56:55 +01:00
Roberto Viola
3c5054acbd App not displaying metrics #2835 2024-12-02 14:37:18 +01:00
Roberto Viola
9a854f7810 Buggy behaviour on Raspberry pi 4 and Zwift #2810 2024-12-02 11:39:04 +01:00
Roberto Viola
1e731f7cbe Zwo warmup not fully elapsed in terms of time in running workouts #2847 2024-12-02 08:36:23 +01:00
Roberto Viola
c0cd6234f3 Android 6 crash (#2852)
* Android 6 crash

* Revert "Android 6 crash"

This reverts commit 6d4bfab1c9.

* Update MediaButtonReceiver.java

* Update MediaButtonReceiver.java

* Update MediaButtonReceiver.java
2024-12-01 15:14:59 +01:00
Roberto Viola
9cc79ab33a Update project.pbxproj 2024-12-01 08:09:17 +01:00
Roberto Viola
181de73a13 Revert "Zwo warmup not fully elapsed in terms of time in running workouts (Issue #2847)"
This reverts commit 36f6fa7feb.
2024-12-01 08:08:41 +01:00
Roberto Viola
57e03c39f1 Virtual activity tag in Strava not applicable when sport type is set to walking #2844 2024-12-01 08:07:23 +01:00
Roberto Viola
ff8a89d688 Revert "Virtual activity tag in Strava not applicable when sport type is set to walking (Issue #2844)"
This reverts commit 2b568c6260.
2024-12-01 08:06:18 +01:00
Roberto Viola
36f6fa7feb Zwo warmup not fully elapsed in terms of time in running workouts (Issue #2847) 2024-11-30 14:50:03 +01:00
Roberto Viola
7d7e9cc79d Update project.pbxproj 2024-11-30 14:21:06 +01:00
Roberto Viola
2616ebe229 Fitness Master T25 incline doesn't work [BUG] #2820
always using kcal from QZ to avoid treadmill madness about KCAL
2024-11-30 14:17:18 +01:00
Roberto Viola
2b568c6260 Virtual activity tag in Strava not applicable when sport type is set to walking (Issue #2844) 2024-11-30 14:13:24 +01:00
Roberto Viola
f49f539e71 Update project.pbxproj 2024-11-28 18:02:03 +01:00
Roberto Viola
1845f3a5ae Incorrect Avg watts displayed on power badge (Issue #2843) 2024-11-28 17:57:17 +01:00
Roberto Viola
9aa337cd47 Update truetreadmill.cpp 2024-11-28 16:30:44 +01:00
Roberto Viola
9b12c5c4bf trying to get app to work with Wahoo Gymconnect to control treadmill (Issue #2840) 2024-11-28 16:00:34 +01:00
Roberto Viola
1d12f7e475 Multi-shift for Zwift Click and Play (and Ride?) (Issue #2751) 2024-11-28 13:33:41 +01:00
Roberto Viola
e463ab9aae reverting 438bd2c195 2024-11-28 13:19:02 +01:00
Roberto Viola
baa9de9059 Update project.pbxproj 2024-11-28 12:27:39 +01:00
Roberto Viola
e3b706f537 Kickr Move Bluetooth Not found (Issue #2827) 2024-11-28 12:26:52 +01:00
Roberto Viola
438bd2c195 Multi-shift for Zwift Click and Play (and Ride?) (Issue #2751) 2024-11-28 11:48:18 +01:00
Roberto Viola
5b546911ff NordicTrack GX 4.4 Pro with QZ on Samsung S7 (Android 8) (Discussion #2836) 2024-11-28 10:45:00 +01:00
Roberto Viola
b544e325ce Update project.pbxproj 2024-11-28 08:46:57 +01:00
Roberto Viola
c01fad2e29 Wattbike Atom First Generation - Display Gears #2829 2024-11-28 08:28:37 +01:00
Roberto Viola
a4d2f53207 Fitness Master T25 incline doesn't work [BUG] #2820 2024-11-28 08:21:44 +01:00
Roberto Viola
a8b3fc3129 Update project.pbxproj 2024-11-27 16:56:14 +01:00
Roberto Viola
b7dec6d223 Wattbike Atom First Generation - Display Gears #2829 2024-11-27 16:55:17 +01:00
Roberto Viola
2d76ea554d Fitness Master T25 incline doesn't work [BUG] #2820 2024-11-27 16:51:56 +01:00
Roberto Viola
b0f03dbe0a Update project.pbxproj 2024-11-27 16:12:38 +01:00
Roberto Viola
46429e04b4 using combobox for the nordictrack bikes in the settings 2024-11-27 16:05:49 +01:00
Roberto Viola
7d479b7d88 secondline for gear tile 2024-11-27 14:11:32 +01:00
Roberto Viola
c86651cdf6 proform carbon TL PFTL59722c.0 #2806 2024-11-27 11:18:02 +01:00
Roberto Viola
63bfdba992 Fitness Master T25 incline doesn't work [BUG] #2820 2024-11-27 10:51:50 +01:00
Roberto Viola
6767c42b14 Fitness Master T25 incline doesn't work [BUG] #2820 2024-11-27 10:50:42 +01:00
Roberto Viola
70f2f2ecb5 NEO BIKE SMART added 2024-11-27 09:23:44 +01:00
Roberto Viola
691ec420b0 Wattbike Atom First Generation - Display Gears #2829 2024-11-27 08:58:16 +01:00
Roberto Viola
278f130906 Update project.pbxproj 2024-11-27 07:14:25 +01:00
Roberto Viola
531a88e326 2.18.8 2024-11-27 07:08:58 +01:00
Roberto Viola
d8e3e193b8 fixing zwift pool time 2024-11-26 18:34:32 +01:00
Roberto Viola
cda99d6f21 ZDRIVE trainer added 2024-11-26 16:52:36 +01:00
Roberto Viola
3cf3ae9135 Kickr Climb not move #2823
forcing inclination after a gear change
2024-11-26 16:11:59 +01:00
Roberto Viola
7333361190 Kickr Climb not move #2823 2024-11-26 12:03:27 +01:00
Roberto Viola
f75ebbb47f Update project.pbxproj 2024-11-26 08:06:07 +01:00
Roberto Viola
ce35fad608 Wattbike Atom First Generation - Display Gears #2829 2024-11-26 08:03:17 +01:00
Roberto Viola
8160d711a1 Fitness Master T25 incline doesn't work [BUG] #2820 2024-11-26 07:55:17 +01:00
Roberto Viola
2a595ced57 Update project.pbxproj 2024-11-25 16:30:43 +01:00
Roberto Viola
9cb14b91ba Compatibility with Technogym Skillbike #2805 2024-11-25 16:29:33 +01:00
Roberto Viola
92e008b3c8 don't add value to erg table if the cadence is 0 2024-11-25 16:00:59 +01:00
Roberto Viola
f6d8924e18 Continuos beeping when speed is modified by the %T. speed tile (Issue #2825) 2024-11-25 13:38:47 +01:00
Roberto Viola
ef955a03bc Kickr Move Bluetooth Not found #2827 2024-11-25 12:10:08 +01:00
Roberto Viola
67c12f7342 Continuos beeping when speed is modified by the %T. speed tile (Issue #2825) 2024-11-25 11:35:12 +01:00
Roberto Viola
5cfbc6855b Continuos beeping when speed is modified by the %T. speed tile (Issue #2825) 2024-11-25 10:51:15 +01:00
Roberto Viola
aa9e9e20fe Change speed or PID when a workouts is load an is running (Issue #2813) 2024-11-25 08:36:29 +01:00
Roberto Viola
417f9c370d Compatibility with Technogym Skillbike #2805 2024-11-25 08:26:32 +01:00
Roberto Viola
3f3bdbb83e forcing ftms default value cause zwift ride don't like 0 2024-11-25 08:21:30 +01:00
Roberto Viola
c1c58a7a4d Update project.pbxproj 2024-11-22 15:05:42 +01:00
Roberto Viola
a6a3dd4c28 Compatibility with Technogym Skillbike #2805 2024-11-22 13:21:45 +01:00
Marcel
55c3dbe3b6 Move csafe lib to separate folder and add commands (#2811) 2024-11-22 13:00:31 +01:00
Roberto Viola
50e0c5aab9 Update project.pbxproj 2024-11-22 10:36:59 +01:00
Roberto Viola
fde0566abf version 2.18.7 2024-11-22 10:34:20 +01:00
Roberto Viola
be83fcbddb Compatibility with Technogym Skillbike #2805 2024-11-22 10:32:23 +01:00
Roberto Viola
a7ccc1997a Compatibility with Technogym Skillbike #2805 2024-11-22 10:03:38 +01:00
Roberto Viola
f6339a9f70 proform carbon TL PFTL59722c.0 #2806 2024-11-22 08:18:43 +01:00
Roberto Viola
6d15d61f68 Update project.pbxproj 2024-11-21 16:59:32 +01:00
Roberto Viola
7b0b81694c Compatibility with Technogym Skillbike #2805 2024-11-21 16:50:28 +01:00
Roberto Viola
e9ab643aab proform carbon TL PFTL59722c.0 #2806 2024-11-21 15:45:19 +01:00
Roberto Viola
2e1ef17861 watt averaging also for charts 2024-11-21 15:31:44 +01:00
Roberto Viola
2527a3d303 adding watts calculated from the homeform in the log 2024-11-21 13:48:00 +01:00
Roberto Viola
62973d0564 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-11-21 10:44:04 +01:00
Roberto Viola
59c428a14b Update project.pbxproj 2024-11-21 10:43:39 +01:00
Roberto Viola
064a4de214 https://github.com/cagnulein/qdomyos-zwift/issues/2541#issuecomment-2489558440 2024-11-21 10:43:20 +01:00
Roberto Viola
998d95f6b2 Nordictrack T series 5 treadmill #2795 2024-11-21 10:21:37 +01:00
Roberto Viola
e609d9ea93 cscbike battery level implemented 2024-11-21 10:16:39 +01:00
Roberto Viola
0d191e6f02 D2RIDE battery ignored 2024-11-21 10:16:24 +01:00
Roberto Viola
3cb3aa4d1c Update project.pbxproj 2024-11-20 15:50:35 +01:00
Roberto Viola
f580e98d26 fixing live power zone chart 2024-11-20 15:50:11 +01:00
Roberto Viola
e1ad8aab73 restoring gears_zwift_ratio variable in the ftms bike
Zwift hub gear custom
#2757
2024-11-20 15:29:00 +01:00
Roberto Viola
340cdd9323 Program doesnt work with CardioPower Ergo 5 (Issue #2804) 2024-11-20 14:25:12 +01:00
Roberto Viola
82ba5debcd fixing workout live chart for powerzones 2024-11-19 16:11:12 +01:00
Roberto Viola
d9a677b4ca Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-11-19 15:20:36 +01:00
Roberto Viola
cd56463286 Cadence Sensor As A Treadmill #2789 2024-11-19 15:20:32 +01:00
Roberto Viola
3faa726fcd Update mqttpublisher.cpp 2024-11-19 12:52:37 +01:00
Roberto Viola
ef938df79f Time shift in PZ rides (Issue #2690) 2024-11-19 12:24:41 +01:00
Roberto Viola
470ca0a98e Update stagesbike.cpp 2024-11-19 11:12:50 +01:00
Roberto Viola
fe9bb7e26a fixing wrong cadence value for cadence in the power pedal sensor 2024-11-19 09:49:04 +01:00
Roberto Viola
a30b1f5298 2.18.6 2024-11-19 09:37:56 +01:00
Marcel
e7196d3033 Fix port on csafe rower (#2794) 2024-11-18 14:46:33 +01:00
Roberto Viola
09977ac703 MQTT Support for Homeassistant (Issue #2759) 2024-11-18 14:28:09 +01:00
Roberto Viola
ea9ed85bf7 Master T40 treadmill #2640 2024-11-18 13:46:11 +01:00
Roberto Viola
1f3d819b24 Update project.pbxproj 2024-11-18 12:28:31 +01:00
Roberto Viola
096a025b79 MQTT Support for Homeassistant (Issue #2759) 2024-11-18 12:25:50 +01:00
Roberto Viola
0926f0b484 MQTT Support for Homeassistant (Issue #2759) 2024-11-18 12:14:35 +01:00
Roberto Viola
b6f6641204 Peloton Auto Start! (#2784) 2024-11-18 11:49:10 +01:00
Roberto Viola
65418c6e9a Peloton Auto Start! (#2784) 2024-11-18 11:46:45 +01:00
Roberto Viola
6abd0e677b Magene T300 Plus bike trainer #2793 2024-11-18 10:27:38 +01:00
Roberto Viola
ecfae5f416 Update 10_Installation.md 2024-11-18 08:49:37 +01:00
Roberto Viola
e2f4d4e376 Technogym Cycle #2788 2024-11-16 14:45:03 +01:00
Roberto Viola
fb8de4606a Sram disabled from the settings due to crypto 2024-11-15 21:23:05 +01:00
Roberto Viola
09af8f98b3 MQTT Support for Homeassistant (Issue #2759) 2024-11-15 16:57:34 +01:00
Roberto Viola
70abcee27d MQTT Support for Homeassistant (Issue #2759) 2024-11-15 16:42:57 +01:00
Roberto Viola
d0deb6bee5 Peloton Auto Start! (#2784)
* it works!

* done!
2024-11-15 14:41:27 +01:00
Roberto Viola
3494349961 MQTT Support for Homeassistant (Issue #2759) (#2766)
* adding qmqtt libraries

* Update qmqttsubscription_p.h

* Update qmqttclient_p.h

* Revert "Update qmqttclient_p.h"

This reverts commit 7629972927.

* Revert "Update qmqttsubscription_p.h"

This reverts commit 9c52f7363e.

* Update qdomyos-zwift.pri

* adding class for mqtt

* Update mqttpublisher.cpp

* Update mqttpublisher.cpp

* fixing build

* adding settings and also works on raspberry

* working also on ios!

* done!

* trying to fix windows build

* Update qmqttmessage.h

* fixing windows build

* Update qmqttglobal.h
2024-11-15 12:29:03 +01:00
Roberto Viola
5abc9ac9c0 fixing get gears from zwift for new firmware version of zwift ride 2024-11-15 09:01:47 +01:00
Roberto Viola
d0360ea87b Fix Protobuf Windows on Github Actions (#2780) 2024-11-14 17:16:50 +01:00
Roberto Viola
d31c5c4d53 Pause button on qz won't reset the speed tile (Issue #2781) 2024-11-14 16:39:09 +01:00
Roberto Viola
f2f49464fc trying to fix win build 2024-11-14 15:10:22 +01:00
Marcel
f6d0e068f6 Fix bluetooth nor working in RPI zero 2w (#2778) 2024-11-14 11:33:04 +01:00
Roberto Viola
17f699234c 2.18.5 2024-11-13 12:40:17 +01:00
Roberto Viola
bf4c07aba4 ftms bike automatically if it has a zwift play service 2024-11-13 12:34:21 +01:00
Roberto Viola
a7fadf55aa Update project.pbxproj 2024-11-13 11:20:24 +01:00
Roberto Viola
4912807ae4 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-11-13 11:13:54 +01:00
Roberto Viola
4a61c34d58 NordicTrack Treadmill C960i #2770 2024-11-13 11:13:45 +01:00
Roberto Viola
109dc901e9 Zwift hub gear custom (#2757)
* first raw version

* Update project.pbxproj

* Update virtualbike_zwift.swift

* fixing formula

* fixing casting to double

* need to center the values in the table

* Update gears.qml

* Revert "Update gears.qml"

This reverts commit 0f149448b3.

* Update gears.qml

* i need to save the first 3 static objects and use it in the wahoo module

* qml finally saves the settings correctly

* completed?

* Update project.pbxproj

* fixing gear conversion

* adding max and minGears

* fixing UI and settings

* kickr core to wahookickr class

* ftms wheel circumference for gears

* implementing

* Update wahookickrsnapbike.cpp

* Update ftmsbike.cpp

* first custom gear test

* adding inclination custom message too

* Update ftmsbike.cpp

* implemented protobuf

* protobuf also for the gears

* Update ftmsbike.cpp

* Update project.pbxproj

* reverting tacxneo wheel diameter and ftms standard wheel diamater in order to merge it

* fixing mingears and maxgears

* adding android part

* Update main.cpp

* Update main.cpp

* Update main.cpp

* Update project.pbxproj

* fixing android build

* fixing build

* fixing android build

* Update main.cpp

* removed debug
2024-11-13 11:13:16 +01:00
Roberto Viola
9b8b6643f8 RPM from Magene Speed Sensor not reading properly (Issue #2769) 2024-11-13 10:05:41 +01:00
Roberto Viola
a26e79dac7 Raspberry PI Build Fix (#2771)
* Update main.yml

* Update main.yml
2024-11-13 09:33:42 +01:00
Roberto Viola
291c5d68f2 SRAM AXS Controller for Virtual Gearing #2768 2024-11-12 16:29:33 +01:00
Roberto Viola
c85e838e33 Proform Treadmill C700 #2762 2024-11-12 10:36:50 +01:00
Roberto Viola
94975e0117 Proform Performance 400i #2741
speed in kph?
2024-11-12 09:53:59 +01:00
Roberto Viola
eebb9359a6 Proform 575i (Issue #2652) 2024-11-12 09:43:14 +01:00
Roberto Viola
40e6609297 Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541)
https://github.com/cagnulein/qdomyos-zwift/issues/2541#issuecomment-2468340941
2024-11-11 15:57:39 +01:00
Roberto Viola
af028504eb Proform 575i #2652 2024-11-11 11:24:07 +01:00
Roberto Viola
bfe4564659 Update proformtreadmill.cpp 2024-11-11 10:08:36 +01:00
Roberto Viola
85cdcaa457 Master T40 treadmill #2640 2024-11-11 09:42:01 +01:00
Roberto Viola
cd5c10835c Proform Performance 400i #2741
stop better handling?
2024-11-11 09:22:06 +01:00
Roberto Viola
ce97c92645 Custom gearing ranges/ratios #2671
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11190939
2024-11-09 16:08:23 +01:00
Roberto Viola
d27b3b9ba0 fixing build 2024-11-09 12:46:01 +01:00
Roberto Viola
0f96923006 Update project.pbxproj 2024-11-09 12:38:28 +01:00
Roberto Viola
bc7ac73de9 Custom gearing ranges/ratios #2671
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11190939
2024-11-09 12:37:21 +01:00
Roberto Viola
8d2fe9dd25 trxappgateusbtreadmill wrong kcal 2024-11-09 12:18:03 +01:00
Roberto Viola
2d99106254 Proform Performance 400i #2741 2024-11-08 16:23:03 +01:00
Roberto Viola
04d01efd0f Update project.pbxproj 2024-11-08 15:00:27 +01:00
Roberto Viola
da10fff966 Proform Performance 400i #2741 2024-11-08 14:58:33 +01:00
Roberto Viola
991ee8674a Update project.pbxproj 2024-11-07 20:23:20 +01:00
Roberto Viola
7f24950498 wizard control for wifi devices added 2024-11-07 14:34:14 +01:00
Roberto Viola
405fb415d4 wizard control for wifi devices added 2024-11-07 13:59:58 +01:00
Roberto Viola
0aa4c27e02 Custom gearing ranges/ratios #2671
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11176772
2024-11-07 12:53:14 +01:00
Roberto Viola
add12366a0 Custom gearing ranges/ratios #2671
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11176772
2024-11-07 12:34:52 +01:00
Roberto Viola
2c496f6fe2 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-11-07 11:49:51 +01:00
Roberto Viola
1381c19723 2.18.3 2024-11-07 11:49:29 +01:00
Roberto Viola
a8ddfc8b70 Garmin HR over ANT+ not collected in QZ (Issue #2740) (#2747)
* Garmin HR over ANT+ not collected in QZ (Issue #2740)

* Update HeartChannelController.java

* Update build.gradle

* Update HeartChannelController.java

* Update build.gradle

* Create android-antplus-plugin-lib-release_3.9.0.aar

* Update HeartChannelController.java

* Update HeartChannelController.java

* Update HeartChannelController.java
2024-11-07 11:43:57 +01:00
Roberto Viola
6680ff1cd9 Echelon Rower Sport 2, not displaying stats (Issue #2749) 2024-11-06 21:04:43 +01:00
Roberto Viola
863dc6378c Update project.pbxproj 2024-11-06 18:46:09 +01:00
Roberto Viola
be0465d094 Capitol Sports Infinity Pro 4 treadmill (Issue #2748) 2024-11-06 18:41:41 +01:00
Roberto Viola
42d2dcef7e Capitol Sports Infinity Pro 4 treadmill 2024-11-06 16:36:10 +01:00
Roberto Viola
c70fd8608d Master T40 treadmill (Issue #2640) 2024-11-06 16:30:25 +01:00
Roberto Viola
e217f929e8 Zwift gear 4 does not change QZ gear or resistance (Issue #2746) 2024-11-06 14:43:05 +01:00
Roberto Viola
b95c51a2be Custom gearing ranges/ratios (Discussion #2671) 2024-11-06 14:02:10 +01:00
Roberto Viola
e3b635d107 Life fitness ic7 spin bike #2745 2024-11-06 12:07:47 +01:00
Roberto Viola
ae1642d052 relaxing bluetooth name on android 2024-11-06 11:46:16 +01:00
Roberto Viola
13e9f49a8b improving speed for gears for wahoo devices 2024-11-06 09:31:47 +01:00
Roberto Viola
13f174fb1e relaxing android bluetooth name control 2024-11-06 09:03:27 +01:00
Roberto Viola
3ef9e6304d adding some debug to virtualbike_zwift.swift 2024-11-06 08:47:53 +01:00
Roberto Viola
398909b809 Update project.pbxproj 2024-11-06 08:13:31 +01:00
Roberto Viola
dc140a3dd5 Proform Performance 400i #2741 2024-11-06 08:13:09 +01:00
Roberto Viola
b2fe23c32e Update project.pbxproj 2024-11-06 06:52:40 +01:00
Roberto Viola
46f0761f50 Improvements made for Proform 705 CST support (Issue #2719) (#2723)
* Improvements made for Proform 705 CST support (Issue #2719)

* comment removed

* Update proformtreadmill.cpp

* Update proformtreadmill.cpp
2024-11-06 06:45:52 +01:00
MRKrinetic
63258f2029 Revised README Layout and Links for Improved Readability (#2742)
* Update README.md

readme updated in a readble format Tested on, Reference, Blog.

* Update README.md

updated readme in readable format

* Update README.md

updated readme file in readble format
2024-11-05 16:49:42 +01:00
Roberto Viola
eeaf8fbe19 Proform Performance 400i #2741 2024-11-05 16:38:32 +01:00
Roberto Viola
934e6dfa57 bug unevenness not fair gpx #2733 2024-11-05 12:40:16 +01:00
Roberto Viola
a9dd04f4fa 2.18.2 2024-11-05 12:07:12 +01:00
Roberto Viola
34df54b96c Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-11-05 11:27:37 +01:00
Roberto Viola
e5ebd3c925 Bowflex M9 #2739 2024-11-05 11:27:32 +01:00
Roberto Viola
e99660ce40 Elite Tuo 2024-11-04 21:03:43 +01:00
Roberto Viola
d5357ed1c3 Technogym Myrun Treadmill rfcomm (#2732) 2024-11-04 20:26:22 +01:00
Roberto Viola
5a357c43e0 switching to the newest macos 2024-11-04 15:53:02 +01:00
Roberto Viola
287ef5bdc7 Proform 575i #2652 2024-11-04 15:51:51 +01:00
Roberto Viola
89db56ae58 Revert "Renpho smart bike r-q002 n (Issue #2401) (#2409)"
This reverts commit 4a33008c61.
2024-11-04 15:42:01 +01:00
Roberto Viola
8f8aa888ca Wahoo Kickr RUN on Android (#2718)
* it doesn't show as controllable yet

* Update virtualtreadmill_zwift.swift

* trying on android

* should be ok on android so

* Revert "should be ok on android so"

This reverts commit 638c99ba83.

* adding inclination parsing

* Revert "Update virtualtreadmill_zwift.swift"

This reverts commit b3a388199e.

* Revert "it doesn't show as controllable yet"

This reverts commit 370ea4e62f.
2024-11-04 15:33:02 +01:00
Roberto Viola
f9321a7bde SHAWN PAVLIN ASCEND S2 Erg Mode (Issue #2660) 2024-11-04 14:20:09 +01:00
Roberto Viola
4c59d2e2cb PID Heart function parameters (Discussion #2731) 2024-11-04 13:41:00 +01:00
Roberto Viola
26d346bdf1 don't ask me again location services (#2716)
* don't ask me again location services

* Update homeform.cpp

* Update Home.qml
2024-11-04 13:21:37 +01:00
Roberto Viola
ba741ada31 QZPI as virtual bike for linux
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11131220
2024-11-04 09:45:46 +01:00
Roberto Viola
2e8c4ebf9a I'm unable to connect my EXERCYCLE H9365R V2 BH correctly. (Issue #2727) 2024-11-02 19:45:38 +01:00
Roberto Viola
579e30683a Update gears.qml 2024-11-02 06:28:31 +01:00
David Mason
b1f893e944 Test updates 2024-10-31 (#2720) 2024-11-01 07:29:09 +01:00
Roberto Viola
415e305415 Custom gearing ranges/ratios (Discussion #2671)
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11113470
2024-10-31 16:53:09 +01:00
Roberto Viola
adb6928772 specific gear value at startup #2671 2024-10-31 16:47:21 +01:00
Roberto Viola
dc70bd1513 adding (but not using) the setuserconfiguration for the tacx 2024-10-31 15:47:37 +01:00
Roberto Viola
57f6a7d1a5 Update project.pbxproj 2024-10-31 13:21:49 +01:00
Roberto Viola
deafbd45d0 Custom gearing ranges/ratios (Discussion #2671)
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11110674
2024-10-31 13:20:45 +01:00
Roberto Viola
b2fa338e03 fixing preset gears
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11109001
2024-10-31 10:01:28 +01:00
Roberto Viola
4e6a98e789 fixing tests 2024-10-31 09:20:23 +01:00
Roberto Viola
72ca19e3e7 Wellfit treadmill support (issue #2659) 2024-10-31 09:19:29 +01:00
Roberto Viola
2c7dac1837 CYCPLUS FTMS bike
https://github.com/cagnulein/qdomyos-zwift/discussions/2671#discussioncomment-11108283
2024-10-31 08:31:21 +01:00
Roberto Viola
de22d58e75 fixing tests 2024-10-31 08:17:28 +01:00
Roberto Viola
2f6d5415cc Update devicetestdataindex.cpp 2024-10-31 07:04:38 +01:00
Roberto Viola
b8101ffa76 Gear UI on zwift #2391 (#2524)
* trying on ios

* Update virtualbike_zwift.swift

* dynamic gears

* trying also on dircon but it doesn't work

* adding the android part

* gears works on android too!

* Update virtualbike.cpp

* setting added

* char 1224 removed

* Update project.pbxproj

* Update dirconmanager.cpp

* Update virtualbike.cpp

* Update virtualbike.cpp
2024-10-31 06:25:55 +01:00
Roberto Viola
281590cf63 Wahoo Custom gearing ranges/ratios (#2682)
* first raw version

* Update project.pbxproj

* Update virtualbike_zwift.swift

* fixing formula

* fixing casting to double

* need to center the values in the table

* Update gears.qml

* Revert "Update gears.qml"

This reverts commit 0f149448b3.

* Update gears.qml

* i need to save the first 3 static objects and use it in the wahoo module

* qml finally saves the settings correctly

* completed?

* Update project.pbxproj

* fixing gear conversion

* adding max and minGears

* fixing UI and settings

* kickr core to wahookickr class
2024-10-31 05:21:43 +01:00
Roberto Viola
6759fb9ec0 I_ROWER fix #842 2024-10-30 14:21:51 +01:00
Roberto Viola
c7dad4f1ad Keiser M3i Heart Rate is Erratic (Issue #2711) 2024-10-30 09:38:00 +01:00
Roberto Viola
d29726632b handling resistance_lvl_mode on ftmsbike with zwift for don't affect wahoo climb 2024-10-29 14:15:26 +01:00
Roberto Viola
95e5c58a92 I_ROWER fix #842 2024-10-29 13:56:34 +01:00
Roberto Viola
8c5a3693c8 trying handling workout state for PM4 (#2398) 2024-10-28 20:21:51 +01:00
Roberto Viola
7aa1061b06 zonehr="0" ignored from xml training program file (Issue #2703) 2024-10-28 13:38:24 +01:00
Roberto Viola
46bd172d59 zonehr="0" ignored from xml training program file (Issue #2703) 2024-10-28 11:31:37 +01:00
Roberto Viola
d4dbaf5c57 Adidas Treadmill #1705 (#2705) 2024-10-28 09:45:49 +01:00
Roberto Viola
6b4d47c79d virtufit etappe 2.0i #2706 2024-10-28 08:59:02 +01:00
Roberto Viola
1bd32ade9f virtufit iConsole HTR 2.1 #2704 2024-10-27 20:22:16 +01:00
Roberto Viola
5e1f3abd14 virtufit iConsole HTR 2.1 #2704 2024-10-27 19:55:36 +01:00
Roberto Viola
5bf7ecab64 Update horizontreadmill.cpp 2024-10-27 06:54:42 +01:00
Roberto Viola
dd75df0af8 TRX4500 iOS connecting issue? 2024-10-27 06:47:10 +01:00
Roberto Viola
72de08f9a3 Schwinn 590U / 190U #2701 2024-10-27 06:42:17 +01:00
Roberto Viola
13213edb4f Elite Qubo Digital Smart B+ #1834 2024-10-27 06:38:22 +01:00
Roberto Viola
872b618ea1 Update stagesbike.cpp 2024-10-25 12:08:41 +02:00
Roberto Viola
78e7fe76c6 Elite Qubo Digital Smart B+ #1834 2024-10-25 11:30:57 +02:00
Roberto Viola
980245bbfc Wellfit treadmill support (issue #2659) 2024-10-24 19:15:40 +02:00
Roberto Viola
2fd98a0be0 adding some debug log on the peloton methods 2024-10-24 16:01:39 +02:00
Roberto Viola
700f5debe5 Update virtualbike.cpp 2024-10-24 15:47:41 +02:00
Roberto Viola
06b4604e59 HOI Cross+ from Kettler #2694 2024-10-24 15:24:22 +02:00
Roberto Viola
a6fd6b71d6 Inconsistent resistance with 'Get Gears from Zwift' setting in latest version of QZ. (Issue #2680) 2024-10-24 15:17:31 +02:00
Roberto Viola
da194caf7c Raspberry Binaries and Images (#2673) 2024-10-24 13:43:34 +02:00
Roberto Viola
fa45e1040f HOI Cross+ from Kettler #2694 2024-10-24 11:46:57 +02:00
Roberto Viola
09f0357763 Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-10-24 11:38:00 +02:00
Roberto Viola
f82e106fc1 Wellfit treadmill support (issue #2659) 2024-10-24 10:58:35 +02:00
Roberto Viola
906431b3a6 Controls not working on mobvoi treadmill plus #2683 2024-10-24 10:29:13 +02:00
Roberto Viola
e82a76492a Inconsistent resistance with 'Get Gears from Zwift' setting in latest version of QZ. (Issue #2680) 2024-10-23 20:53:20 +02:00
Roberto Viola
8c75c01017 Workoutdoors treadmill compatibility (#2693) 2024-10-23 20:05:34 +02:00
Roberto Viola
dfabd2b414 Update project.pbxproj 2024-10-23 11:42:13 +02:00
Roberto Viola
8199dea809 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-23 11:25:48 +02:00
Roberto Viola
1cb20088b2 I_ROWER fix #842 2024-10-23 10:58:34 +02:00
Roberto Viola
203a9e5ca5 resistance against negative gradient #2677 2024-10-23 10:15:34 +02:00
Roberto Viola
5a70586756 resistance against negative gradient #2677 2024-10-23 10:02:20 +02:00
Roberto Viola
478beca96d JUSTO device added 2024-10-23 08:18:35 +02:00
Roberto Viola
637b57158a Wellfit treadmill support #2659 (#2686)
* n Wellfit treadmill support #2659

* Update horizontreadmill.cpp
2024-10-22 17:34:05 +02:00
Roberto Viola
a2009fa91f Inconsistent resistance with 'Get Gears from Zwift' setting in latest version of QZ. #2680 2024-10-22 16:40:35 +02:00
Roberto Viola
4ee5fa3b00 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-22 15:32:41 +02:00
Roberto Viola
36dde79dac Zwift Cog Virtual Gearing in ERG mode (Issue #2685) 2024-10-22 10:03:24 +02:00
Roberto Viola
9071d8a000 Update project.pbxproj 2024-10-21 17:35:18 +02:00
Roberto Viola
1c815e9d47 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-21 17:29:31 +02:00
Roberto Viola
9b16779293 Update project.pbxproj 2024-10-21 15:41:55 +02:00
Roberto Viola
c702477dc8 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-21 15:38:03 +02:00
Roberto Viola
b54df1d299 Proform 575i (Issue #2652) 2024-10-21 12:06:57 +02:00
Roberto Viola
548a254262 fixing crash on "TREADMILL" 2024-10-21 07:37:51 +02:00
Roberto Viola
d7330ad654 Elite Kura #2679 2024-10-20 15:56:01 +02:00
Roberto Viola
cfe02d3489 Cannot connect to ESLinker treadmill #2628 2024-10-18 15:00:28 +02:00
Roberto Viola
973bc4309d Cannot connect to ESLinker treadmill #2628 2024-10-18 14:53:31 +02:00
Roberto Viola
2112ed111f 2.18.1 2024-10-18 10:58:49 +02:00
Roberto Viola
95d714ea0c Update project.pbxproj 2024-10-18 09:48:07 +02:00
Roberto Viola
cad60e3343 SHAWN PAVLIN ASCEND S2 Erg Mode (Issue #2660) 2024-10-18 09:47:11 +02:00
Roberto Viola
c1f0640eda Update project.pbxproj 2024-10-18 09:39:31 +02:00
Roberto Viola
d511f0ea95 Zwift play emulator #2391 (#2613)
* trying on ios

* Update virtualbike_zwift.swift

* dynamic gears

* trying also on dircon but it doesn't work

* adding the android part

* gears works on android too!

* Update virtualbike.cpp

* adding zwift play service

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* fixing iOS UUID?

* porting to android too

* Update virtualbike.cpp

* Update virtualbike.cpp

* Update virtualbike.cpp

* Update virtualbike.cpp

* Update virtualbike.cpp

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* zwift play ask 1 passed!

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* seems to work apart the wattage to zwift

* Update virtualbike_zwift.swift

* accolumulated torque but it doesn't seem necessary

* the gearing is working!

* reverting torque not necessary

* increasing UI speed for the gear changing

* Update project.pbxproj

* handling slope

* fixing difficulty

* trying to use the wahoo service for gears also on dircon

* changing gears quickly

* merging modification on android/linux

* Update virtualbike.cpp

* fixing android

* fixing starting gears with the new zwift version

* adding setting for enabling it

* Update project.pbxproj

* Update virtualbike_zwift.swift

* Update virtualbike_zwift.swift

* fixing gears formatting

* Update project.pbxproj
2024-10-18 09:03:12 +02:00
Roberto Viola
96ad01f78c Update project.pbxproj 2024-10-17 17:21:35 +02:00
Roberto Viola
bdb5ed9ec1 Cannot connect to ESLinker treadmill #2628 2024-10-17 17:17:11 +02:00
Roberto Viola
49b054330c Update esliCannot connect to ESLinker treadmill (Issue #2628)nkertreadmill.cpp 2024-10-17 17:07:14 +02:00
Roberto Viola
40feaa010d Update project.pbxproj 2024-10-17 16:14:49 +02:00
Roberto Viola
fbae0a48dc Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-17 15:04:22 +02:00
Roberto Viola
84a0f93cc1 Update project.pbxproj 2024-10-17 14:16:07 +02:00
Roberto Viola
642a89548c Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-17 14:08:20 +02:00
Roberto Viola
6ac19bd6b5 life fitness ic7 spin bike distance and kcal error 2024-10-17 14:03:56 +02:00
Roberto Viola
5eaf54ccf1 Proform 575i #2652 2024-10-17 09:23:20 +02:00
Roberto Viola
78f64180c7 Proform 575i #2652 2024-10-16 08:29:15 +02:00
Roberto Viola
d921c426e4 Wellfit treadmill support #2659 2024-10-15 17:51:34 +02:00
Roberto Viola
efb09e7a81 adding description in the hr settings 2024-10-15 15:35:57 +02:00
David Mason
cb8939849b Manufacturer update of test project (#2654)
* #2653 refactored test project

* #2653 doc file update

* #2653 added documentation on class members
Changed some variable names.
Deleted member object in destructor
2024-10-15 09:30:48 +02:00
Roberto Viola
60e990a6c4 2.18.0 android 2024-10-14 16:43:27 +02:00
Roberto Viola
7c258dc4a4 Update project.pbxproj 2024-10-14 16:40:50 +02:00
Roberto Viola
bae7abb765 Proform 575i (Issue #2652) 2024-10-14 16:31:43 +02:00
Roberto Viola
9b5eee64d8 Use Volume buttons for manually changing speed #2657 2024-10-14 13:49:50 +02:00
Roberto Viola
edfdc0ae6c Use Volume buttons for manually changing speed (Issue #2657) 2024-10-14 13:35:58 +02:00
Roberto Viola
ff7bc5dbec Tacx Neo 2 freewheel keeps spinning #2650 2024-10-14 10:24:40 +02:00
Sunguk Lee
cd918f3664 Initial implement start/stop control of KingSmith R2 when press start/pause/stop buttons (#813) 2024-10-12 07:02:40 +02:00
Roberto Viola
88ba9563ad Xiaomi treadmill x21 new type #2649 2024-10-11 18:54:16 +02:00
Roberto Viola
b12a3d39a7 Xiaomi treadmill x21 new type #2649 2024-10-11 18:19:07 +02:00
Roberto Viola
0bc0885439 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-10-10 09:30:04 +02:00
Roberto Viola
9b38e93cf4 Kingsmith K12 #2642 2024-10-10 09:29:58 +02:00
Roberto Viola
e87687f175 Master T40 treadmill #2640 2024-10-09 16:40:43 +02:00
Roberto Viola
1e681de8a3 Update project.pbxproj 2024-10-09 16:07:36 +02:00
Roberto Viola
27bf0667fa Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-09 15:52:45 +02:00
Roberto Viola
732cfce4a0 fixing gears on proformwifibike 2024-10-09 14:14:32 +02:00
Roberto Viola
516f301822 Update project.pbxproj 2024-10-09 14:14:07 +02:00
Roberto Viola
21a1a7b765 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-09 09:39:04 +02:00
Roberto Viola
f1e57967d3 Jetblack Vitctory Compatibility added 2024-10-08 16:33:53 +02:00
Roberto Viola
c6bf70b3e1 added the ability to use resistance instead of inclination on ftmsbike 2024-10-08 15:42:18 +02:00
Roberto Viola
f2e9f5b28a Update project.pbxproj 2024-10-08 14:42:47 +02:00
Roberto Viola
02737c8b41 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-10-08 13:56:42 +02:00
Roberto Viola
2455298bb1 Update project.pbxproj 2024-10-08 13:56:30 +02:00
Roberto Viola
779afb5b17 DeerRun Treadmill integration #2621 2024-10-08 09:01:35 +02:00
Roberto Viola
969843dde4 Adding “Next Rows” metric on QZ AI app (Issue #2584) 2024-10-07 15:42:06 +02:00
Roberto Viola
f371a5337d Adding “Next Rows” metric on QZ AI app (Issue #2584) 2024-10-07 15:30:43 +02:00
Roberto Viola
ef9e97a588 Update project.pbxproj 2024-10-07 14:57:46 +02:00
Roberto Viola
41de930b49 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2024-10-07 14:51:54 +02:00
Roberto Viola
4e1adee102 Training programs UI improvement (Issue #1537) 2024-10-07 14:49:57 +02:00
Roberto Viola
c66f623173 Update project.pbxproj 2024-10-07 14:29:34 +02:00
Roberto Viola
625f62b057 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-07 14:22:30 +02:00
Roberto Viola
a996cb32b9 add support for Proform Sport 7.0 treadmill (Issue #2635) 2024-10-07 11:58:50 +02:00
Roberto Viola
d1fd8f6a70 Step count must be the double to match Apple Watch counts 2024-10-05 20:52:40 +02:00
Roberto Viola
4a33008c61 Renpho smart bike r-q002 n (Issue #2401) (#2409) 2024-10-05 07:45:19 +02:00
Roberto Viola
9d808b28a4 Update project.pbxproj 2024-10-04 12:08:57 +02:00
Roberto Viola
f69aee817c fixing timeout for proformwifibike 2024-10-04 11:13:35 +02:00
Roberto Viola
b07a75df90 Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-04 10:38:33 +02:00
Roberto Viola
1e2af212ca Cannot connect to ESLinker treadmill (Issue #2628) 2024-10-04 10:37:46 +02:00
Roberto Viola
c36afc3173 Update project.pbxproj 2024-10-03 18:33:30 +02:00
Roberto Viola
1d5d29bf1d Cannot connect to ESLinker treadmill #2628 2024-10-03 16:32:37 +02:00
Roberto Viola
deb5eab79e Speed Adjustment Issue on FS 465D50 Walking Pad (Issue #2630) (#2631)
* Speed Adjustment Issue on FS 465D50 Walking Pad (Issue #2630)

* Revert "Speed Adjustment Issue on FS 465D50 Walking Pad (Issue #2630)"

This reverts commit 4b0a36bb8a.

* Update fitshowtreadmill.cpp

* Update project.pbxproj

* Revert "Update fitshowtreadmill.cpp"

This reverts commit 10d859fa70.

* better fix
2024-10-03 13:13:51 +02:00
Roberto Viola
e767e964ab ]DeerRun Treadmill integration #2621
i need to add it to the bluetooth.cpp
2024-10-03 11:43:35 +02:00
Roberto Viola
eb540dc579 removing gears_zwift_ratio for device that are not supporting it #2608 2024-10-02 21:07:17 +02:00
Roberto Viola
01cd02ef94 Felvon v2 #2627 2024-10-02 16:00:25 +02:00
Roberto Viola
77b2ec46d1 Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-10-01 14:36:33 +02:00
Roberto Viola
cf6b1953e0 Update project.pbxproj 2024-09-30 18:14:36 +02:00
Roberto Viola
533fba4c6e Revert "Can not auto adjust downhill inclination in Kinomap- Sole TT8 treadmill #2625"
This reverts commit 1b597c16dd.
2024-09-30 18:13:25 +02:00
Roberto Viola
60068dea5b Several Issues Using QZ with Rouvy and Zwift Play Controllers #2541 2024-09-30 17:34:21 +02:00
Roberto Viola
1b597c16dd Can not auto adjust downhill inclination in Kinomap- Sole TT8 treadmill #2625 2024-09-30 16:13:13 +02:00
Roberto Viola
5d4b2a1fe1 Update project.pbxproj 2024-09-30 16:12:47 +02:00
Roberto Viola
c39f80bdeb Several Issues Using QZ with Rouvy and Zwift Play Controllers (Issue #2541) 2024-09-30 15:50:56 +02:00
Roberto Viola
a220efa9a4 Update zwiftclickremote.cpp 2024-09-30 14:32:45 +02:00
Roberto Viola
23c803add1 Update project.pbxproj 2024-09-30 14:31:51 +02:00
Roberto Viola
e38d8f24b6 Several Issues Using QZ with Rouvy and Zwift Play Controllers #2541
https://github.com/cagnulein/qdomyos-zwift/issues/2541#issuecomment-2383003316
2024-09-30 14:29:48 +02:00
449 changed files with 54590 additions and 10570 deletions

View File

@@ -1,3 +1,4 @@
# This is a basic workflow to help you get started with Actions
name: CI
@@ -127,6 +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 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
@@ -140,7 +142,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.exe output/
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp "C:/mingw64/bin/libwinpthread-1.dll" .
@@ -167,7 +169,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.exe output/
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp "C:/mingw64/bin/libwinpthread-1.dll" .
@@ -303,6 +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 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
@@ -337,7 +340,7 @@ jobs:
# This workflow contains a single job called "build"
linux-x86-build:
# The type of runner that the job will run on
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
@@ -507,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:
@@ -531,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
@@ -570,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'
@@ -605,6 +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 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
@@ -635,23 +649,96 @@ 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
runs-on: macos-12
runs-on: macos-latest
permissions:
contents: write
@@ -708,6 +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 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
@@ -818,6 +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 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
@@ -832,9 +921,23 @@ jobs:
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 protobuf protobuf-c abseil
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=C:\a\qdomyos-zwift\vcpkg\installed
working-directory: ${{ runner.workspace }}
- name: Build
@@ -847,7 +950,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.exe output/
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp ../../../icons/iOS/iTunesArtwork@2x.png .
@@ -875,7 +978,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.exe output/
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp "C:/mingw64/bin/libwinpthread-1.dll" .
@@ -991,6 +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 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
@@ -1005,9 +1109,23 @@ jobs:
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 protobuf protobuf-c abseil
.\vcpkg\vcpkg install --triplet x64-windows --x-install-root=C:\a\qdomyos-zwift\vcpkg\installed
working-directory: ${{ runner.workspace }}
- name: Build
@@ -1023,7 +1141,7 @@ jobs:
cd src/debug
mkdir output
mkdir appx
cp qdomyos-zwift.exe output/
cp qdomyos-zwift.* output/
cd output
windeployqt --qmldir ../../ qdomyos-zwift.exe
cp ../../../icons/iOS/iTunesArtwork@2x.png .
@@ -1052,14 +1170,318 @@ jobs:
name: windows-msvc2019-ai-server-binary
path: windows-msvc2019-ai-server-binary.zip
raspberry-pi-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Secrets
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
echo "#define LICENSE" >> secret.h
cd ..
- name: Build for Raspberry Pi
uses: docker://arm32v7/debian:bullseye-20241016
with:
args: >
bash -c "
set -ex &&
apt-get update &&
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 &&
export QT_SELECT=qt5 &&
export PATH=/usr/lib/qt5/bin:$PATH &&
cd /github/workspace &&
sed -i '/QtHttpServer/d' qdomyos-zwift.pro &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/#include <QtHttpServer/\/\/#include <QtHttpServer/' {} + &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/QHttpServer/\/\/QHttpServer/' {} + &&
cat qdomyos-zwift.pro &&
qmake &&
make -j$(nproc)
"
- name: Rename binary
run: mv src/qdomyos-zwift src/qdomyos-zwift-32bit
- name: Archive Raspberry Pi binary
uses: actions/upload-artifact@v4
with:
name: raspberry-pi-binary
path: src/qdomyos-zwift-32bit
raspberry-pi-build-and-image-64bit:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:master
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: v0.19.3
- name: Secrets
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
echo "#define LICENSE" >> secret.h
cd ..
- name: Build for Raspberry Pi 64-bit
uses: docker://arm64v8/debian:bullseye-20241016
with:
args: >
bash -c "
set -ex &&
apt-get update &&
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 &&
export QT_SELECT=qt5 &&
export PATH=/usr/lib/qt5/bin:$PATH &&
cd /github/workspace &&
sed -i '/QtHttpServer/d' qdomyos-zwift.pro &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/#include <QtHttpServer/\/\/#include <QtHttpServer/' {} + &&
find src -type f \( -name '*.cpp' -o -name '*.h' \) -exec sed -i 's/QHttpServer/\/\/QHttpServer/' {} + &&
cat qdomyos-zwift.pro &&
qmake &&
make -j$(nproc)
"
- 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=C:\a\qdomyos-zwift\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] # 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:
@@ -1077,11 +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-binary/qdomyos-zwift-64bit

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

16
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"program": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/qdomyos-zwift.exe",
"symbolSearchPath": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/qdomyos-zwift.pdb",
"sourceFileMap": {
"d:/a/qdomyos-zwift/qdomyos-zwift": "c:/work/qdomyos-zwift/",
"compiled_source_path": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/"
}
}
]
}

View File

@@ -96,34 +96,36 @@ Zwift bridge for Treadmills and Bike!
|:---|:---:|:---:|:---:|:---:|---:|
|Resistance shifting with bluetooth remote|X||X|||
|TTS support|X|X|X|X||
|Zwift Play & Click support|X|||||
|MQTT integration|X|X|X|X||
|OpenSoundControl integration|X|X|X|X||
### Installation
You can install it on multiple platforms.
Read the [installation procedure](docs/10_Installation.md)
You can install it on multiple platforms.
Read the [installation procedure](docs/10_Installation.md)
### Tested on
You can run the app on [Macintosh or Linux devices](docs/10_Installation.md). IOS and Android are also supported.
QDomyos-Zwift works on every [FTMS-compatible application](docs/20_supported_devices_and_applications.md), and virtually any [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
The QDomyos-Zwift application can run on [Macintosh or Linux devices](docs/10_Installation.md) iOS, and Android.
It supports any [FTMS-compatible application](docs/20_supported_devices_and_applications.md) software and most [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
### No GUI version
run as
$ sudo ./qdomyos-zwift -no-gui
$ sudo ./qdomyos-zwift -no-gui
### Reference
https://github.com/ProH4Ck/treadmill-bridge
=> GitHub Repository: [QDomyos-Zwift on GitHub](https://github.com/ProH4Ck/treadmill-bridge)
https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/
=> Treadmill Incline Reference: [What Is 10 Degrees in Incline on a Treadmill?](https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/)
Icons used in this documentation come from [flaticon.com](https://www.flaticon.com)
=> Icon Attribution: Icons used in this documentation are from [Flaticon.com](https://www.flaticon.com)
### Blog
https://robertoviola.cloud
=> Related Blog: [Roberto Viola's Blog](https://robertoviola.cloud)

View File

@@ -107,6 +107,7 @@ extension MainController: WorkoutTrackingDelegate {
WorkoutTracking.speed = WatchKitConnection.speed
WorkoutTracking.power = WatchKitConnection.power
WorkoutTracking.cadence = WatchKitConnection.cadence
WorkoutTracking.steps = WatchKitConnection.steps
if Locale.current.measurementSystem != "Metric" {
self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance))")

View File

@@ -27,6 +27,7 @@ class WatchKitConnection: NSObject {
public static var speed = 0.0
public static var cadence = 0.0
public static var power = 0.0
public static var steps = 0
weak var delegate: WatchKitConnectionDelegate?
private override init() {
@@ -76,6 +77,10 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.power = dPower
let dCadence = Double(result["cadence"] as! Double)
WatchKitConnection.cadence = dCadence
if let stepsDouble = result["steps"] as? Double {
let iSteps = Int(stepsDouble)
WatchKitConnection.steps = iSteps
}
}, errorHandler: { (error) in
print(error)
})

View File

@@ -33,6 +33,7 @@ class WorkoutTracking: NSObject {
public static var cadenceSteps = 0
public static var speed = Double()
public static var power = Double()
public static var steps = Int()
public static var cadence = Double()
public static var lastDateMetric = Date()
var sport: Int = 0
@@ -267,8 +268,106 @@ 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: workoutSession.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: workoutSession.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
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, // Use your steps quantity here
start: workoutSession.startDate!,
end: Date())
// Add the steps sample to workout builder
workoutBuilder.add([sampleSteps]) { (success, error) in
if let error = error {
print(error)
}
// End the data collection
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
if let error = error {
print(error)
}
// Finish the workout and save total steps
self.workoutBuilder.finishWorkout { (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(stepsQuantity, forKey: "totalSteps")
}
}
}
guard let quantityTypeDistance = HKQuantityType.quantityType(
forIdentifier: .distanceWalkingRunning) else {
return
@@ -402,7 +501,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 1) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
doubleValue: WorkoutTracking.power)
@@ -445,7 +544,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 2) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
doubleValue: WorkoutTracking.speed * 0.277778)

View File

@@ -0,0 +1,96 @@
# Define build image
FROM ubuntu:latest AS build
# Install essential build dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
git \
ca-certificates \
qtquickcontrols2-5-dev \
qtconnectivity5-dev \
qtbase5-private-dev \
qtpositioning5-dev \
libqt5charts5-dev \
libqt5networkauth5-dev \
libqt5websockets5-dev \
qml-module* \
libqt5texttospeech5-dev \
qtlocation5-dev \
qtmultimedia5-dev \
g++ \
make \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Define runtime image
FROM ubuntu:latest AS runtime
# Install essential runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
libqt5bluetooth5 \
libqt5widgets5 \
libqt5positioning5 \
libqt5xml5 \
libqt5charts5 \
qt5-assistant \
libqt5networkauth5 \
libqt5websockets5 \
qml-module* \
libqt5texttospeech5 \
libqt5location5-plugins \
libqt5multimediawidgets5 \
libqt5multimedia5-plugins \
libqt5multimedia5 \
qml-module-qtquick-controls2 \
libqt5location5 \
bluez \
dbus \
tigervnc-standalone-server \
tigervnc-tools \
libgl1-mesa-dri \
xfonts-base \
x11-xserver-utils \
tigervnc-common \
net-tools \
&& rm -rf /var/lib/apt/lists/*
# Stage 1: Build
FROM build AS builder
# Clone the project and build it
WORKDIR /usr/local/src
RUN git clone --recursive https://github.com/cagnulein/qdomyos-zwift.git
WORKDIR /usr/local/src/qdomyos-zwift
RUN git submodule update --init src/smtpclient/ \
&& git submodule update --init src/qmdnsengine/ \
&& git submodule update --init tst/googletest/
WORKDIR /usr/local/src/qdomyos-zwift/src
RUN qmake qdomyos-zwift.pro \
&& make -j4
# Stage 2: Runtime
FROM runtime
# Copy the built binary to /usr/local/bin
COPY --from=builder /usr/local/src/qdomyos-zwift/src/qdomyos-zwift /usr/local/bin/qdomyos-zwift
# VNC configuration
RUN mkdir -p ~/.vnc && \
echo "securepassword" | vncpasswd -f > ~/.vnc/passwd && \
chmod 600 ~/.vnc/passwd
# .Xauthority configuration
RUN touch /root/.Xauthority
ENV DISPLAY=:99
# Start VNC server with authentication
CMD vncserver :99 -depth 24 -localhost no -xstartup qdomyos-zwift && \
sleep infinity

2
docker/linux_gui_vnc/build.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker build -t qdomyos-zwift-vnc .

View File

@@ -0,0 +1,10 @@
services:
qdomyos-zwift-vnc:
image: qdomyos-zwift-vnc
container_name: qdomyos-zwift-vnc
privileged: true # Required for Bluetooth functionality
network_mode: "host" # Used to access host Bluetooth and D-Bus
volumes:
- /dev:/dev # Forward host devices (for Bluetooth)
- /run/dbus:/run/dbus # Forward D-Bus for Bluetooth interaction
restart: "no" # Do not restart the container automatically

View File

@@ -0,0 +1,95 @@
# Define build image
FROM ubuntu:latest AS build
# Install essential build dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
git \
ca-certificates \
qtquickcontrols2-5-dev \
qtconnectivity5-dev \
qtbase5-private-dev \
qtpositioning5-dev \
libqt5charts5-dev \
libqt5networkauth5-dev \
libqt5websockets5-dev \
qml-module* \
libqt5texttospeech5-dev \
qtlocation5-dev \
qtmultimedia5-dev \
g++ \
make \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Define runtime image
FROM ubuntu:latest AS runtime
# Install essential runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
libqt5bluetooth5 \
libqt5widgets5 \
libqt5positioning5 \
libqt5xml5 \
libqt5charts5 \
qt5-assistant \
libqt5networkauth5 \
libqt5websockets5 \
qml-module* \
libqt5texttospeech5 \
libqt5location5-plugins \
libqt5multimediawidgets5 \
libqt5multimedia5-plugins \
libqt5multimedia5 \
qml-module-qtquick-controls2 \
libqt5location5 \
bluez \
dbus \
&& rm -rf /var/lib/apt/lists/*
# Stage 1: Build
FROM build AS builder
# Define variables for Qt versions
ARG QT_VERSION=5.15
ARG QT_SUBVERSION=5.15.13
ARG QT_WEBPLUGIN_NAME=qtwebglplugin-everywhere-opensource-src
# Build WebGL plugin
WORKDIR /usr/local/src
RUN wget https://download.qt.io/official_releases/qt/${QT_VERSION}/${QT_SUBVERSION}/submodules/${QT_WEBPLUGIN_NAME}-${QT_SUBVERSION}.zip \
&& unzip ${QT_WEBPLUGIN_NAME}-${QT_SUBVERSION}.zip \
&& mv *-${QT_SUBVERSION} qtwebglplugin-everywhere \
&& cd qtwebglplugin-everywhere \
&& qmake \
&& make
# Clone the project and build it
WORKDIR /usr/local/src
RUN git clone --recursive https://github.com/cagnulein/qdomyos-zwift.git
WORKDIR /usr/local/src/qdomyos-zwift
RUN git submodule update --init src/smtpclient/ \
&& git submodule update --init src/qmdnsengine/ \
&& git submodule update --init tst/googletest/
WORKDIR /usr/local/src/qdomyos-zwift/src
RUN qmake qdomyos-zwift.pro \
&& make -j4
# Stage 2: Runtime
FROM runtime
# Copy the built binary to /usr/local/bin
COPY --from=builder /usr/local/src/qdomyos-zwift/src/qdomyos-zwift /usr/local/bin/qdomyos-zwift
# Copy WebGL plugin to the appropriate location
COPY --from=builder /usr/local/src/qtwebglplugin-everywhere/plugins/platforms/libqwebgl.so /usr/lib/x86_64-linux-gnu/qt5/plugins/platforms/libqwebgl.so
# Set the default command to run the application with WebGL
CMD ["qdomyos-zwift", "-qml", "-platform", "webgl:port=8080"]

2
docker/linux_webgl/build.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker build -t qdomyos-zwift-webgl .

View File

@@ -0,0 +1,19 @@
services:
qdomyos-zwift-webgl:
image: qdomyos-zwift-webgl
container_name: qdomyos-zwift-webgl
privileged: true
network_mode: "host"
environment:
- DISPLAY=${DISPLAY}
volumes:
- /dev:/dev
- /run/dbus:/run/dbus
- ./.config:/root/.config
- /tmp/.X11-unix:/tmp/.X11-unix
stdin_open: true
tty: true
restart: "no"
# command: qdomyos-zwift -qml -platform webgl:port=8080
# command: ["qdomyos-zwift", "-no-gui"]

View File

@@ -10,7 +10,7 @@ These instructions build the app itself, not the test project.
```buildoutcfg
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd qdomyos-zwift
$ git submodule update --init src/smtpclient/
@@ -106,7 +106,7 @@ This operation takes a moment to complete.
#### Install qdomyos-zwift from sources
```bash
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
git clone https://github.com/cagnulein/qdomyos-zwift.git
cd qdomyos-zwift
git submodule update --init src/smtpclient/
@@ -117,6 +117,11 @@ qmake qdomyos-zwift.pro
make
```
If you need GUI also do a
```
apt install qml-module*
```
Please note :
- Don't build the application with `-j4` option (this will fail)
- Build operation is circa 45 minutes (subsequent builds are faster)
@@ -172,6 +177,151 @@ If everything is working as expected, **enable your service at boot time** :
Then reboot to check operations (`sudo reboot`)
### (optional) Treadmill Auto-Detection and Service Management
This section provides a reliable way to manage the QZ service based on the treadmill's power state. Using a `bluetoothctl`-based Bash script, this solution ensures the QZ service starts when the treadmill is detected and stops when it is not.
- **Bluetooth Discovery**: Monitors treadmill availability via `bluetoothctl`.
- **Service Control**: Automatically starts and stops the QZ service.
- **Logging**: Tracks treadmill status and actions in a log file.
**Notes:**
- Ensure `bluetoothctl` is installed and working on your system.
- Replace `I_TL` in the script with your treadmill's Bluetooth name. You can find your device name via `bluetoothctl scan on`
- Adjust the sleep interval (`sleep 30`) in the script as needed for your use case.
Step 1: Save the following script as `/root/qz-treadmill-monitor.sh`:
```bash
#!/bin/bash
LOG_FILE="/var/log/qz-treadmill-monitor.log"
TARGET_DEVICE="I_TL"
SCAN_INTERVAL=30 # Time in seconds between checks
SERVICE_NAME="qz"
DEBUG_LOG_DIR="/var/log" # Directory where QZ debug logs are stored
ERROR_MESSAGE="BTLE stateChanged InvalidService"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
is_service_running() {
systemctl is-active --quiet "$SERVICE_NAME"
return $?
}
scan_for_device() {
log "Starting Bluetooth scan for $TARGET_DEVICE..."
# Run bluetoothctl scan in the background and capture output
bluetoothctl scan on &>/dev/null &
SCAN_PID=$!
# Allow some time for devices to appear
sleep 5
# Check if the target device appears in the list
bluetoothctl devices | grep -q "$TARGET_DEVICE"
DEVICE_FOUND=$?
# Stop scanning
kill "$SCAN_PID"
bluetoothctl scan off &>/dev/null
if [ $DEVICE_FOUND -eq 0 ]; then
log "Device '$TARGET_DEVICE' found."
return 0
else
log "Device '$TARGET_DEVICE' not found."
return 1
fi
}
restart_qz_on_error() {
# Get the current date
CURRENT_DATE=$(date '+%a_%b_%d')
# Find the latest QZ debug log file for today
LATEST_LOG=$(ls -t "$DEBUG_LOG_DIR"/debug-"$CURRENT_DATE"_*.log 2>/dev/null | head -n 1)
if [ -z "$LATEST_LOG" ]; then
log "No QZ debug log found for today."
return 0
fi
log "Checking latest log file: $LATEST_LOG for errors..."
# Search the latest log for the error message
if grep -q "$ERROR_MESSAGE" "$LATEST_LOG"; then
log "***** Error detected in QZ log: $ERROR_MESSAGE *****"
log "Restarting QZ service..."
systemctl restart "$SERVICE_NAME"
else
log "No errors detected in $LATEST_LOG."
fi
}
manage_service() {
local device_found=$1
if $device_found; then
if ! is_service_running; then
log "***** Starting QZ service... *****"
systemctl start "$SERVICE_NAME"
else
log "QZ service is already running."
restart_qz_on_error # Check the log for errors when QZ is already running
fi
else
if is_service_running; then
log "***** Stopping QZ service... *****"
systemctl stop "$SERVICE_NAME"
else
log "QZ service is already stopped."
fi
fi
}
while true; do
log "Checking for treadmill status..."
if scan_for_device; then
manage_service true
else
manage_service false
fi
log "Waiting for $SCAN_INTERVAL seconds before next check..."
sleep "$SCAN_INTERVAL"
done
```
Step2: To ensure the script runs continuously, create a systemd service file at `/etc/systemd/system/qz-treadmill-monitor.service`
```bash
[Unit]
Description=QZ Treadmill Monitor Service
After=bluetooth.service
[Service]
Type=simple
ExecStart=/root/qz-treadmill-monitor.sh
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
```
Step 3: Enable and Start the Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable qz-treadmill-monitor
sudo systemctl start qz-treadmill-monitor
```
Monitor logs are written to `/var/log/qz-treadmill-monitor.log`. Use the following command to check logs in real-time:
```bash
sudo tail -f /var/log/qz-treadmill-monitor.log
```
### (optional) Enable overlay FS

View File

@@ -8,23 +8,21 @@ The testing project tst/qdomyos-zwift-tests.pro contains test code that uses the
New devices are added to the main QZ application by creating or modifying a subclass of the bluetoothdevice class.
At minimum, each device has a corresponding BluetoothDeviceTestData subclass in the test project, which is coded to provide information to the test framework to generate tests for device detection and potentially other things.
At minimum, each device has a corresponding BluetoothDeviceTestData object constructed in the DeviceTestDataIndex class in the test project, which is coded to provide information to the test framework to generate tests for device detection and potentially other things.
In the test project
* create a new folder for the device under tst/Devices. This is for anything you define for testing this device.
* add a new class with header file and optionally .cpp file to the project in that folder. Name the class DeviceNameTestData, substituting an appropriate name in place of "DeviceName".
* edit the header file to inherit the class from the BluetoothDeviceTestData abstract subclass appropriate to the device type, i.e. BikeTestData, RowerTestData, EllipticalTestData, TreadmillTestData.
* have this new subclass' constructor pass a unique test name to its superclass.
* add a new device name constant to the DeviceIndex class.
* locate the implementation of DeviceTestDataindex::Initialize and build the test data from a call to DeviceTestDataIndex::RegisterNewDeviceTestData(...)
* pass the device name constant defined in the DeviceIndex class to the call to DeviceTestDataIndex::RegisterNewDeviceTestData(...).
The tests are not organised around real devices that are handled, but the bluetoothdevice subclass that handles them - the "driver" of sorts.
You need to provide the following:
- patterns for valid names (e.g. equals a value, starts with a value, case sensitivity, specific length)
- invalid names to ensure the device is not identified when the name is invalid
- configuration settings that are required for the device to be detected
- configuration settings that are required for the device to be detected, including bluetooth device information configuration
- invalid configurations to test that the device is not detected, e.g. when it's disabled in the settings, but the name is correct
- exclusion devices: if a device with the same name but of a higher priority type is detected, this device should not be detected
- valid and invalid QBluetoothDeviceInfo configurations, e.g. to check the device is only detected when the manufacturer data is set correctly, or certain services are available or not.
- exclusion devices: for example if a device with the same name but of a higher priority type is detected, this device should not be detected
## Tools in the Test Framework
@@ -39,16 +37,18 @@ i.e. a test will
### DeviceDiscoveryInfo
This class contains a set of fields that store strongly typed QSettings values.
It also provides methods to read and write the values it knows about from and to a QSettings object.
This class:
* stores values for a specific subset of the QZSettings keys.
* provides methods to read and write the values it knows about from and to a QSettings object.
* provides a QBluetoothDeviceInfo object configured with the device name currently being tested.
It is used in conjunction with a TestSettings object to write a configuration during a test.
## Writing a device detection test
Because of the way the TestData classes currently work, it may be necessary to define multiple test data classes to cover the various cases.
For example, if any of a list of names is enough to identify a device, or another group of names but with a certain service in the bluetooth device info, that will require multiple classes.
Because of the way the BluetoothDeviceTestDataBuilder currently works, it may be necessary to define multiple test data objects to cover the various cases.
For example, if any of a list of names is enough to identify a device, or another group of names but with a certain service in the bluetooth device info, that will require multiple test data objects.
### Recognition by Name
@@ -68,133 +68,83 @@ Reading this, to identify this device:
In this case, we are not testing the last two, but can test the first two.
In deviceindex.h:
```
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "devices/domyosbike/domyosbike.h"
class DomyosBikeTestData : public BikeTestData {
public:
DomyosBikeTestData() : BikeTestData("Domyos Bike") {
this->addDeviceName("Domyos-Bike", comparison::StartsWith);
this->addInvalidDeviceName("DomyosBridge", comparison::StartsWith);
}
// not used yet
deviceType get_expectedDeviceType() const override { return deviceType::DomyosBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<domyosbike*>(detectedDevice)!=nullptr;
}
};
static const QString DomyosBike;
```
The constructor adds a valid device name, and an invalid one. Various overloads of these methods and other members of the comparison enumeration provide other capabilities for specifying test data. If you add a valid device name that says the name should start with a value, additional names will be added automatically to the valid list with additional characters to test that it is in fact a "starts with" relationship. Also, valid and invalid names will be generated base on whether the comparison is case sensitive or not.
In deviceindex.cpp:
The get_expectedDeviceType() function is not actually used and is part of an unfinished refactoring of the device detection code, whereby the bluetoothdevice object doesn't actually get created intially. You could add a new value to the deviceType enum and return that, but it's not used yet. There's always deviceType::None.
```
DEFINE_DEVICE(DomyosBike, "Domyos Bike");
```
The get_isExpectedDevice(bluetoothdevice *) function must be overridden to indicate if the specified object is of the type expected for this test data.
This pair adds the "friendly name" for the device as a constant, and also adds the key/value pair to an index.
In DeviceTestDataIndex::Initialize():
```
// Domyos bike
RegisterNewDeviceTestData(DeviceIndex::DomyosBike)
->expectDevice<domyosbike>()
->acceptDeviceName("Domyos-Bike", DeviceNameComparison::StartsWith)
->rejectDeviceName("DomyosBridge", DeviceNameComparison::StartsWith);
```
This set of instructions adds a valid device name, and an invalid one. Various overloads of these methods, other methods, and other members of the comparison enumeration provide other capabilities for specifying test data. If you add a valid device name that says the name should start with a value, additional names will be added automatically to the valid list with additional characters to test that it is in fact a "starts with" relationship. Also, valid and invalid names will be generated based on whether the comparison is case sensitive or not.
### Configuration Settings
Consider the CompuTrainerTestData. This device is not detected by name, but only by whether or not it is enabled in the settings.
To specify this in the test data, we override one of the configureSettings methods, the one for the simple case where there is a single valid and a single invalid configuration.
Consider the CompuTrainer bike. This device is not detected by name, but only by whether or not it is enabled in the settings.
To specify this in the test data, we use one of the BluetoothDeviceTestData::configureSettingsWith(...) methods, the one for the simple case where there is a single QZSetting with a specific enabling and disabling value.
Settings from QSettings that contribute to tests should be put into the DeviceDiscoveryInfo class.
For example, for the Computrainer Bike, the "computrainer_serial_port" value from the QSettings determines if the bike should be detected or not.
For example, for the Computrainer Bike, the "computrainer_serialport" value from the QSettings determines if the bike should be detected or not.
The computrainer_serialport QZSettings key should be registered in devicediscoveryinfo.cpp
In devicediscoveryinfo.cpp:
```
class DeviceDiscoveryInfo {
public :
...
QString computrainer_serial_port = nullptr;
...
}
```
void InitializeTrackedSettings() {
The getValues and setValues methods should be updated to include the addition(s):
```
void DeviceDiscoveryInfo::setValues(QSettings &settings, bool clear) const {
if(clear) settings.clear();
...
settings.setValue(QZSettings::computrainer_serialport, this->computrainer_serial_port);
...
}
void DeviceDiscoveryInfo::getValues(QSettings &settings){
...
this->computrainer_serial_port = settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
trackedSettings.insert(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport);
...
}
```
In the following example, the DeviceDiscoveryInfo class has been updated to contain the device's configuration setting (computrainer_serial_port).
- if an enabling configuration is requested (enable==true) a string that is known to be accepted is supplied
- if a disabling configuration is requested (enable==false) an empty string is supplied.
For this test data,
* if enabling configurations are requested, the computrainer_serialport setting will be populated with "COMX"
* if disabling configurations are requested, the computrainer_serialport setting will be populated with ""
This example uses the simpler of 2 configureSettings methods returns true/false to indicate if the configuration should be used for the test.
DeviceTestDataIndex::Initialize():
```
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "devices/computrainerbike/computrainerbike.h"
class CompuTrainerTestData : public BikeTestData {
protected:
bool configureSettings(DeviceDiscoveryInfo& info, bool enable) const override {
info.computrainer_serial_port = enable ? "X":QString();
return true;
}
public:
CompuTrainerTestData() : BikeTestData("CompuTrainer Bike") {
// any name
this->addDeviceName("", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::CompuTrainerBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<computrainerbike*>(detectedDevice)!=nullptr;
}
};
// Computrainer Bike
RegisterNewDeviceTestData(DeviceIndex::ComputrainerBike)
->expectDevice<computrainerbike>()
->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::computrainer_serialport, "COMX", "");
```
Similarly, the Pafers Bike has a simple configuration setting:
```
#include "Devices/Bike/biketestdata.h"
#include "devices/pafersbike/pafersbike.h"
class PafersBikeTestData : public BikeTestData {
protected:
bool configureSettings(DeviceDiscoveryInfo& info, bool enable) const override {
// the treadmill is given priority
info.pafers_treadmill = !enable;
return true;
}
public:
PafersBikeTestData() : BikeTestData("Pafers Bike") {
this->addDeviceName("PAFERS_", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::PafersBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<pafersbike*>(detectedDevice)!=nullptr;
}
};
// Pafers Bike
RegisterNewDeviceTestData(DeviceIndex::PafersBike)
->expectDevice<pafersbike>()
->acceptDeviceName("PAFERS_", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::pafers_treadmill,false);
```
In that case, ```configureSettingsWith(QZSettings::pafers_treadmill,false)``` indicates that the pafers_treadmill setting will be false for enabling configurations and true for disabling ones.
A more complicated example is the Pafers Treadmill. It involves a name match, but also some configuration settings obtained earlier...
```
@@ -212,76 +162,60 @@ bool pafers_treadmill_bh_iboxster_plus =
```
Here the device could be activated due to a name match and various combinations of settings.
For this, the configureSettings function that takes a vector of DeviceDiscoveryInfo objects which is populated with configurations that lead to the specified result (enable = detected, !enable=not detected). Instead of returning a boolean to indicate if a configuration has been supplied, it populates a vector of DeviceDiscoveryInfo objects.
For this, the configureSettingsWith(...) function that takes a lambda function which consumes a vector of DeviceDiscoveryInfo objects which is populated with configurations that lead to the specified result (enable = detected, !enable=not detected).
```
#pragma once
// Pafers Treadmill
RegisterNewDeviceTestData(DeviceIndex::PafersTreadmill)
->expectDevice<paferstreadmill>()
->acceptDeviceName("PAFERS_", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith( [](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void {
DeviceDiscoveryInfo config(info);
#include "Devices/Treadmill/treadmilltestdata.h"
#include "devices/paferstreadmill/paferstreadmill.h"
class PafersTreadmillTestData : public TreadmillTestData {
protected:
void configureSettings(const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations) const override {
DeviceDiscoveryInfo config(info);
if (enable) {
for(int x = 1; x<=3; x++) {
config.pafers_treadmill = x & 1;
config.pafers_treadmill_bh_iboxster_plus = x & 2;
configurations.push_back(config);
}
} else {
config.pafers_treadmill = false;
config.pafers_treadmill_bh_iboxster_plus = false;
configurations.push_back(config);
}
}
public:
PafersTreadmillTestData() : TreadmillTestData("Pafers Treadmill") {
this->addDeviceName("PAFERS_", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::PafersTreadmill; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<paferstreadmill*>(detectedDevice)!=nullptr;
}
};
if (enable) {
for(int x = 1; x<=3; x++) {
config.setValue(QZSettings::pafers_treadmill, x & 1);
config.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, x & 2);
configurations.push_back(config);
}
} else {
config.setValue(QZSettings::pafers_treadmill, false);
config.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, false);
configurations.push_back(config);
}
});
```
### Considering Extra QBluetoothDeviceInfo Content
Detection of some devices requires some specific bluetooth device information.
Supplying enabling and disabling QBluetoothDeviceInfo objects is done using a similar pattern to the multiple configurations scenario.
For example, the M3iBike requires specific manufacturer information.
Supplying enabling and disabling QBluetoothDeviceInfo objects is done by accessing the QBluetoothDeviceInfo member of the DeviceDiscoveryInfo object.
For example, the M3iBike requires specific manufacturer information, using the simpler of the lambda functions accepted by the configureSettingsWith function.
```
void M3IBikeTestData::configureBluetoothDeviceInfos(const QBluetoothDeviceInfo& info, bool enable, std::vector<QBluetoothDeviceInfo>& bluetoothDeviceInfos) const {
// The M3I bike detector looks into the manufacturer data.
// M3I Bike
RegisterNewDeviceTestData(DeviceIndex::M3IBike)
->expectDevice<m3ibike>()
->acceptDeviceName("M3", DeviceNameComparison::StartsWith)
->configureSettingsWith(
[](DeviceDiscoveryInfo& info, bool enable)->void
{
// The M3I bike detector looks into the manufacturer data.
if(!enable) {
info.DeviceInfo()->setManufacturerData(1, QByteArray("Invalid manufacturer data."));
return;
}
QBluetoothDeviceInfo result = info;
if(!enable) {
result.setManufacturerData(1, QByteArray("Invalid manufacturer data."));
bluetoothDeviceInfos.push_back(result);
return;
}
int key=0;
result.setManufacturerData(key++, hex2bytes("02010639009F00000000000000000014008001"));
bluetoothDeviceInfos.push_back(result);
}
int key=0;
info.DeviceInfo()->setManufacturerData(key++, hex2bytes("02010639009F00000000000000000014008001"));
});
```
The test framework populates the incoming QBluetoothDeviceInfo object with a name and a UUID. This is expected to have nothing else defined.
Another example is one of the test data classes for detecting a device that uses the statesbike class:
The test framework populates the incoming QBluetoothDeviceInfo object with a UUID and the name (generated from the acceptDeviceName and rejectDeviceName calls) currently being tested.
This is expected to have nothing else defined.
Another example is one of the test data definitions for detecting a device that uses the stagesbike class:
Detection code from bluetooth.cpp:
@@ -289,37 +223,49 @@ Detection code from bluetooth.cpp:
((b.name().toUpper().startsWith("KICKR CORE")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
```
This condition is actually extracted from a more complicated example where the current test data classes can't cover all the detection criteria in one implementation. This is why this class inherits from StagesBikeTestData rather than BikeTestData directly.
This condition is actually extracted from a more complicated example where the BluetoothDeviceTestData class can't cover all the detection criteria with one instance.
```
class StagesBike3TestData : public StagesBikeTestData {
protected:
void configureBluetoothDeviceInfos(const QBluetoothDeviceInfo& info, bool enable, std::vector<QBluetoothDeviceInfo>& bluetoothDeviceInfos) const override {
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
// Stages Bike General
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
if(enable) {
QBluetoothDeviceInfo result = info;
result.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818)}));
bluetoothDeviceInfos.push_back(result);
} else {
QBluetoothDeviceInfo hasInvalid = info;
hasInvalid.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1826)}));
QBluetoothDeviceInfo hasBoth = hasInvalid;
hasBoth.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818),QBluetoothUuid((quint16)0x1826)}));
//
// ... other stages bike variants
//
bluetoothDeviceInfos.push_back(info); // has neither
bluetoothDeviceInfos.push_back(hasInvalid);
bluetoothDeviceInfos.push_back(hasBoth);
}
}
// Stages Bike (KICKR CORE)
RegisterNewDeviceTestData(DeviceIndex::StagesBike_KICKRCORE)
->expectDevice<stagesbike>()
->acceptDeviceName("KICKR CORE", DeviceNameComparison::StartsWithIgnoreCase)
->excluding(stagesBikeExclusions)
->configureSettingsWith(
[](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void
{
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
public:
StagesBike3TestData() : StagesBikeTestData("Stages Bike (KICKR CORE)") {
if(enable) {
DeviceDiscoveryInfo result = info;
result.addBluetoothService(QBluetoothUuid((quint16)0x1818));
result.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(result);
} else {
DeviceDiscoveryInfo hasNeither = info;
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1818));
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasInvalid = info;
hasInvalid.addBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasBoth = hasInvalid;
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1818));
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(info); // has neither
configurations.push_back(hasInvalid);
configurations.push_back(hasBoth);
}
});
this->addDeviceName("KICKR CORE", comparison::StartsWithIgnoreCase);
}
};
```
In this case, it populates the vector with the single enabling configuration if that's what's been requested, otherwise 3 disabling ones.
@@ -328,7 +274,7 @@ In this case, it populates the vector with the single enabling configuration if
Sometimes there might be ambiguity when multiple devices are available, and the detection code may specify that if the other conditions match, but certain specific kinds of devices (the exclusion devices) have already been detected, the newly matched device should be ignored.
The TestData class can be made to cover this by overriding the configureExclusions() method to add instances of the TestData classes for the exclusion devices to the object's internal list of exclusions.
The test data object can be made to cover this by calling the excluding(...) functions to add type identifiers for the bluetoothdevice classes for the exclusion devices to the object's internal list of exclusions.
Detection code:
@@ -336,39 +282,19 @@ Detection code:
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride &&
!echelonConnectSport && filter) {
```
The configureExclusions code is overridden to specify the exclusion test data objects. Note that the test for a previously detected device of the same type is not included.
The excluding<T>() template function is called to specify the exclusion device type. Note that the test for a previously detected device of the same type is not included.
```
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "Devices/EchelonRower/echelonrowertestdata.h"
#include "Devices/EchelonStrideTreadmill/echelonstridetreadmilltestdata.h"
#include "devices/echelonconnectsport/echelonconnectsport.h"
class EchelonConnectSportBikeTestData : public BikeTestData {
public:
EchelonConnectSportBikeTestData() : BikeTestData("Echelon Connect Sport Bike") {
this->addDeviceName("ECH", comparison::StartsWith);
}
void configureExclusions() override {
this->exclude(new EchelonRowerTestData());
this->exclude(new EchelonStrideTreadmillTestData());
}
deviceType get_expectedDeviceType() const override { return deviceType::EchelonConnectSport; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<echelonconnectsport*>(detectedDevice)!=nullptr;
}
};
// Echelon Connect Sport Bike
RegisterNewDeviceTestData(DeviceIndex::EchelonConnectSportBike)
->expectDevice<echelonconnectsport>()
->acceptDeviceName("ECH", DeviceNameComparison::StartsWith)
->excluding<echelonrower>()
->excluding<echelonstride>();
```
### When a single TestData Class Can't Cover all the Conditions
### When a single test data object can't cover all the conditions
Detection code:
@@ -390,116 +316,81 @@ This presents 3 scenarios for the current test framework.
2. Match the name "KICKR CORE", presence and absence of specific service ids
3. Match the name "ASSIOMA" and the power sensor name setting starts with "Disabled"
The framework is not currently capable of specifying all these scenarios in a single class.
The generated test data is approximately the combinations of these lists: names * settings * bluetoothdeviceInfo * exclusions.
If a combination should not exist, a separate class should be used.
The framework is not currently capable of specifying all these scenarios in a single test data object, without checking the name of the supplied QBluetoothDeviceInfo object against name conditions specified and constructing extra configurations based on that.
The generated test data is approximately the combinations of these lists: names * settings * exclusions.
If a combination should not exist, separate test data objects should be used.
In the example of the StagesBikeTestData classes, the exclusions, which apply to all situations, are implemented in the superclass StagesBikeTestData,
In the example of the Stages Bike test data, the exclusions, which apply to all situations, are implemented in an array of type ids:
```
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "devices/stagesbike/stagesbike.h"
#include "Devices/FTMSBike/ftmsbiketestdata.h"
class StagesBikeTestData : public BikeTestData {
protected:
StagesBikeTestData(std::string testName): BikeTestData(testName) {
}
void configureExclusions() override {
this->exclude(new FTMSBike1TestData());
this->exclude(new FTMSBike2TestData());
}
public:
deviceType get_expectedDeviceType() const override { return deviceType::StagesBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<stagesbike*>(detectedDevice)!=nullptr;
}
};
// Stages Bike General
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
```
The name-match only in one subclass:
The name-match only in one test data instance:
```
class StagesBike1TestData : public StagesBikeTestData {
public:
StagesBike1TestData() : StagesBikeTestData("Stages Bike") {
this->addDeviceName("STAGES ", comparison::StartsWithIgnoreCase);
this->addDeviceName("TACX SATORI", comparison::StartsWithIgnoreCase);
}
};
// Stages Bike
RegisterNewDeviceTestData(DeviceIndex::StagesBike)
->expectDevice<stagesbike>()
->acceptDeviceNames({"STAGES ", "TACX SATORI"}, DeviceNameComparison::StartsWithIgnoreCase)
->acceptDeviceName("QD", DeviceNameComparison::IgnoreCase)
->excluding(stagesBikeExclusions);
```
The name and setting match in another subclass:
The name and setting match in another instance:
```
class StagesBike2TestData : public StagesBikeTestData {
protected:
bool configureSettings(DeviceDiscoveryInfo& info, bool enable) const override {
info.powerSensorName = enable ? "Disabled":"Roberto";
return true;
}
public:
StagesBike2TestData() : StagesBikeTestData("Stages Bike (Assioma / Power Sensor disabled") {
this->addDeviceName("ASSIOMA", comparison::StartsWithIgnoreCase);
}
};
// Stages Bike Stages Bike (Assioma / Power Sensor disabled
RegisterNewDeviceTestData(DeviceIndex::StagesBike_Assioma_PowerSensorDisabled)
->expectDevice<stagesbike>()
->acceptDeviceName("ASSIOMA", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::power_sensor_name, "DisabledX", "XDisabled")
->excluding( stagesBikeExclusions);
```
The name and bluetooth device info configurations in another:
```
// Stages Bike (KICKR CORE)
RegisterNewDeviceTestData(DeviceIndex::StagesBike_KICKRCORE)
->expectDevice<stagesbike>()
->acceptDeviceName("KICKR CORE", DeviceNameComparison::StartsWithIgnoreCase)
->excluding(stagesBikeExclusions)
->configureSettingsWith(
[](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void
{
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
class StagesBike3TestData : public StagesBikeTestData {
protected:
void configureBluetoothDeviceInfos(const QBluetoothDeviceInfo& info, bool enable, std::vector<QBluetoothDeviceInfo>& bluetoothDeviceInfos) const override {
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
if(enable) {
DeviceDiscoveryInfo result = info;
result.addBluetoothService(QBluetoothUuid((quint16)0x1818));
result.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(result);
} else {
DeviceDiscoveryInfo hasNeither = info;
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1818));
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
if(enable) {
QBluetoothDeviceInfo result = info;
result.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818)}));
bluetoothDeviceInfos.push_back(result);
} else {
QBluetoothDeviceInfo hasInvalid = info;
hasInvalid.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1826)}));
QBluetoothDeviceInfo hasBoth = hasInvalid;
hasBoth.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818),QBluetoothUuid((quint16)0x1826)}));
DeviceDiscoveryInfo hasInvalid = info;
hasInvalid.addBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasBoth = hasInvalid;
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1818));
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1826));
bluetoothDeviceInfos.push_back(info); // has neither
bluetoothDeviceInfos.push_back(hasInvalid);
bluetoothDeviceInfos.push_back(hasBoth);
}
}
public:
StagesBike3TestData() : StagesBikeTestData("Stages Bike (KICKR CORE)") {
this->addDeviceName("KICKR CORE", comparison::StartsWithIgnoreCase);
}
};
configurations.push_back(info); // has neither
configurations.push_back(hasInvalid);
configurations.push_back(hasBoth);
}
});
```
## Telling Google Test Where to Look
To register your test data class(es) with Google Test:
- open tst/Devices/devices.h
- add a #include for your new header file(s)
- add your new classes to the BluetoothDeviceTestDataTypes list.
This will add tests for your new device class to test runs of the tests in the BluetoothDeviceTestSuite class, which are about detecting, and not detecting devices in circumstances generated from the TestData classes.
The BluetoothDeviceTestSuite configuration specifies that the test data will be obtained from the DeviceTestDataIndex class, so there's nothing more to do.

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)

View File

@@ -0,0 +1,37 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.associations": {
"list": "cpp",
"chrono": "cpp",
"complex": "cpp",
"functional": "cpp",
"optional": "cpp",
"system_error": "cpp",
"type_traits": "cpp",
"xlocnum": "cpp",
"xtr1common": "cpp",
"qhttpserver": "cpp",
"array": "cpp",
"deque": "cpp",
"map": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"xstring": "cpp",
"algorithm": "cpp",
"xutility": "cpp",
"xlocale": "cpp",
"filesystem": "cpp",
"bitset": "cpp",
"iterator": "cpp",
"xhash": "cpp",
"xtree": "cpp",
"ostream": "cpp",
"locale": "cpp"
}
}
}

View File

@@ -10,59 +10,87 @@ ColumnLayout {
property alias textFont: accordionText.font.family
property alias textFontSize: accordionText.font.pixelSize
property alias indicatRectColor: indicatRect.color
default property alias accordionContent: contentPlaceholder.data
spacing: 0
default property alias accordionContent: contentLoader.sourceComponent
Layout.fillWidth: true;
// Signal emitted when content becomes visible
signal contentBecameVisible()
spacing: 0
Layout.fillWidth: true
Rectangle {
id: accordionHeader
color: "red"
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true;
Layout.fillWidth: true
height: 48
Rectangle{
id:indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
Rectangle {
id: indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
x:34;y:13
x: 34; y: 13
color: "#FFFFFF"
text: rootElement.title
}
Image {
y:13
anchors.right: parent.right
y: 13
anchors.right: parent.right
anchors.rightMargin: 20
width: 30; height: 30
id: indicatImg
source: "qrc:/icons/arrow-collapse-vertical.png"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
rootElement.isOpen = !rootElement.isOpen
if(rootElement.isOpen)
{
if(rootElement.isOpen) {
indicatImg.source = "qrc:/icons/arrow-expand-vertical.png"
}else{
} else {
indicatImg.source = "qrc:/icons/arrow-collapse-vertical.png"
}
}
}
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
// Loader with enhanced visibility handling
Loader {
id: contentLoader
active: rootElement.isOpen
visible: false // Start invisible
Layout.fillWidth: true
asynchronous: false
onLoaded: {
if (item) {
item.Layout.fillWidth = true
visible = true
rootElement.contentBecameVisible()
}
}
// Handle visibility changes
onVisibleChanged: {
if (visible && status === Loader.Ready) {
rootElement.contentBecameVisible()
}
}
}
// Handle accordion closing
onIsOpenChanged: {
if (!isOpen) {
contentLoader.visible = false
}
}
}

View File

@@ -33,6 +33,7 @@ HomeForm {
property bool theme_tile_shadow_enabled: true
property string theme_tile_shadow_color: "#9C27B0"
property int theme_tile_secondline_textsize: 12
property bool skipLocationServicesDialog: false
}
MessageDialog {
@@ -86,14 +87,31 @@ HomeForm {
onTriggered: {if(rootItem.stopRequested) {rootItem.stopRequested = false; inner_stop(); }}
}
property var locationServiceRequsted: false
property bool locationServiceRequsted: false
MessageDialog {
id: locationServicesDialog
text: "Permissions Required"
informativeText: "QZ requires both Bluetooth and Location Services to be enabled.\nLocation Services are necessary on Android to allow the app to find Bluetooth devices.\nThe GPS will not be used.\n\nWould you like to enable them?"
buttons: (MessageDialog.Yes | MessageDialog.No)
onYesClicked: {locationServiceRequsted = true; rootItem.enableLocationServices()}
visible: !rootItem.locationServices() && !locationServiceRequsted
onYesClicked: {
locationServiceRequsted = true
rootItem.enableLocationServices()
}
onNoClicked: remindLocationServicesDialog.visible = true
visible: !rootItem.locationServices() && !locationServiceRequsted && !settings.skipLocationServicesDialog
}
MessageDialog {
id: remindLocationServicesDialog
text: "Reminder Preference"
informativeText: "Would you like to be reminded about enabling Location Services next time?"
buttons: (MessageDialog.Yes | MessageDialog.No)
onYesClicked: settings.skipLocationServicesDialog = false
onNoClicked: settings.skipLocationServicesDialog = true
visible: false
}
MessageDialog {
text: "Restart the app"
informativeText: "To apply the changes, you need to restart the app.\nWould you like to do that now?"

View File

@@ -0,0 +1,20 @@
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 QtQuick.Dialogs 1.0
SwitchDelegate {
id: root
MouseArea {
anchors.fill: parent
onClicked: {
if (mouse.x > parent.width - parent.indicator.width) {
root.checked = !root.checked
root.clicked()
}
}
}
}

26
src/OAuth2.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef OAUTH2_H
#define OAUTH2_H
#include <QString>
#include <QTextStream>
struct OAuth2Parameter {
QString responseType = QStringLiteral("code");
QString approval_prompt = QStringLiteral("force");
inline bool isEmpty() const { return responseType.isEmpty() && approval_prompt.isEmpty(); }
QString toString() const {
QString msg;
QTextStream out(&msg);
out << QStringLiteral("OAuth2Parameter{\n") << QStringLiteral("responseType: ") << this->responseType
<< QStringLiteral("\n") << QStringLiteral("approval_prompt: ") << this->approval_prompt
<< QStringLiteral("\n");
return msg;
}
};
#define _STR(x) #x
#define STRINGIFY(x) _STR(x)
#endif // OAUTH2_H

View File

@@ -22,7 +22,7 @@ ColumnLayout {
}
}
AccordionElement {
StaticAccordionElement {
title: qsTr("Settings folder")
indicatRectColor: Material.color(Material.Grey)
textColor: Material.color(Material.Grey)

View File

@@ -0,0 +1,68 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
ColumnLayout {
id: rootElement
property bool isOpen: false
property string title: ""
property alias color: accordionHeader.color
property alias textColor: accordionText.color
property alias textFont: accordionText.font.family
property alias textFontSize: accordionText.font.pixelSize
property alias indicatRectColor: indicatRect.color
default property alias accordionContent: contentPlaceholder.data
spacing: 0
Layout.fillWidth: true;
Rectangle {
id: accordionHeader
color: "red"
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true;
height: 48
Rectangle{
id:indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
x:34;y:13
color: "#FFFFFF"
text: rootElement.title
}
Image {
y:13
anchors.right: parent.right
anchors.rightMargin: 20
width: 30; height: 30
id: indicatImg
source: "qrc:/icons/arrow-collapse-vertical.png"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
rootElement.isOpen = !rootElement.isOpen
if(rootElement.isOpen)
{
indicatImg.source = "qrc:/icons/arrow-expand-vertical.png"
}else{
indicatImg.source = "qrc:/icons/arrow-collapse-vertical.png"
}
}
}
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
}
}

80
src/WebPelotonAuth.qml Normal file
View File

@@ -0,0 +1,80 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtQuick.Dialogs 1.0
import QtGraphicalEffects 1.12
import Qt.labs.settings 1.0
import QtMultimedia 5.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
width: parent.width
visible: !rootItem.pelotonPopupVisible
url: rootItem.getPelotonAuthUrl
}
Popup {
id: popupPelotonConnectedWeb
parent: Overlay.overlay
enabled: rootItem.pelotonPopupVisible
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnectedWeb.open() }
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)
width: 380
height: 120
modal: true
focus: true
palette.text: "white"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition
{
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
}
exit: Transition
{
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!")
}
}
// 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")

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.16.70" android:versionCode="851" 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.18.25" android:versionCode="1090" 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 -->

View File

@@ -31,7 +31,7 @@ dependencies {
implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
if(amazon == "1") {
// amazon app store
implementation 'com.google.mlkit:text-recognition:16.0.0-beta6'
@@ -50,7 +50,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.github.mik3y:usb-serial-for-android:v3.4.6'
implementation files('libs/usb-serial-for-android-3.8.1.aar')
androidTestImplementation "com.android.support:support-annotations:28.0.0"
implementation 'com.google.android.gms:play-services-wearable:+'

Binary file not shown.

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";
private Activity activity = null;
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.
*/
@@ -155,7 +317,7 @@ public class ChannelService extends Service {
public void openAllChannels() throws ChannelNotAvailableException {
if (Ant.heartRequest && heartChannelController == null)
heartChannelController = new HeartChannelController(acquireChannel());
heartChannelController = new HeartChannelController();
if (Ant.speedRequest) {
if(Ant.treadmill && sdmChannelController == null) {
@@ -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

@@ -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,23 +1,21 @@
/*
* 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 android.util.Log;
import android.content.Context;
import org.cagnulen.qdomyoszwift.QLog;
import android.app.Activity;
// ANT+ Plugin imports
import com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc;
import com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc.DataState;
import com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc.IHeartRateDataReceiver;
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;
// Basic ANT imports for legacy support
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
import com.dsi.ant.channel.IAntChannelEventHandler;
@@ -30,220 +28,103 @@ import com.dsi.ant.message.fromant.ChannelEventMessage;
import com.dsi.ant.message.fromant.MessageFromAntType;
import com.dsi.ant.message.ipc.AntMessageParcel;
// Java imports
import java.math.BigDecimal;
import java.util.EnumSet;
import java.util.Random;
public class HeartChannelController {
// The device type and transmission type to be part of the channel ID message
private static final int CHANNEL_HEART_DEVICE_TYPE = 0x78;
private static final int CHANNEL_HEART_TRANSMISSION_TYPE = 1;
private static final String TAG = HeartChannelController.class.getSimpleName();
private Context context;
private AntPlusHeartRatePcc hrPcc = null;
private PccReleaseHandle<AntPlusHeartRatePcc> releaseHandle = null;
private boolean isConnected = false;
public int heart = 0; // Public to be accessible from ChannelService
// The period and frequency values the channel will be configured to
private static final int CHANNEL_HEART_PERIOD = 8118; // 1 Hz
private static final int CHANNEL_HEART_FREQUENCY = 57;
public HeartChannelController() {
this.context = Ant.activity;
openChannel();
}
private static final String TAG = HeartChannelController.class.getSimpleName();
public boolean openChannel() {
// Request access to first available heart rate device
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, 0, 0, // 0 means first available device
new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
@Override
public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
switch(resultCode) {
case SUCCESS:
hrPcc = result;
isConnected = true;
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
subscribeToHrEvents();
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;
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;
}
}
}
);
private static Random randGen = new Random();
return isConnected;
}
private AntChannel mAntChannel;
private void subscribeToHrEvents() {
if (hrPcc != null) {
hrPcc.subscribeHeartRateDataEvent(new IHeartRateDataReceiver() {
@Override
public void onNewHeartRateData(long estTimestamp, EnumSet<EventFlag> eventFlags,
int computedHeartRate, long heartBeatCount,
BigDecimal heartBeatEventTime, DataState dataState) {
heart = computedHeartRate;
QLog.d(TAG, "Heart Rate: " + heart);
}
});
}
}
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
public void close() {
if (releaseHandle != null) {
releaseHandle.close();
releaseHandle = null;
}
hrPcc = null;
isConnected = false;
QLog.d(TAG, "Channel Closed");
}
public int getHeartRate() {
return heart;
}
private boolean mIsOpen;
int heart = 0;
public HeartChannelController(AntChannel antChannel) {
mAntChannel = antChannel;
openChannel();
}
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
Log.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
// must have the same channel ID, or wildcard (0) is used.
ChannelId channelId = new ChannelId(0,
CHANNEL_HEART_DEVICE_TYPE, CHANNEL_HEART_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. Additional
// features (such as, background scanning and frequency agility) can be enabled
// by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment).
mAntChannel.assign(ChannelType.SLAVE_RECEIVE_ONLY);
/*
* Configures the channel ID, messaging period and rf frequency after assigning,
* then opening the channel.
*
* For any additional ANT features such as proximity search or background scanning, refer to
* the ANT Protocol Doc found at:
* http://www.thisisant.com/resources/ant-message-protocol-and-usage/
*/
mAntChannel.setChannelId(channelId);
mAntChannel.setPeriod(CHANNEL_HEART_PERIOD);
mAntChannel.setRfFrequency(CHANNEL_HEART_FREQUENCY);
mAntChannel.open();
mIsOpen = true;
Log.d(TAG, "Opened channel with device number");
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
// This will release, and therefore unassign if required
channelError("Open failed", e);
}
}
} else {
Log.w(TAG, "No channel available");
}
return mIsOpen;
}
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
Log.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);
}
Log.e(TAG, logString.toString());
mAntChannel.release();
Log.e(TAG, "ANT Command Failed");
}
public void close() {
// TODO kill all our resources
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;
}
Log.e(TAG, "Channel Closed");
}
/**
* Implements the Channel Event Handler Interface so that messages can be
* received and channel death events can be handled.
*/
public class ChannelEventCallback implements IAntChannelEventHandler {
int revCounts = 0;
int ucMessageCount = 0;
byte ucPageChange = 0;
byte ucExtMesgType = 1;
long lastTime = 0;
double way;
int rev;
double remWay;
double wheel = 0.1;
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
Log.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
// Switching on message type to handle different types of messages
switch (messageType) {
// If data message, construct from parcel and update channel data
case BROADCAST_DATA:
// Rx Data
//updateData(new BroadcastDataMessage(antParcel).getPayload());
BroadcastDataMessage m = new BroadcastDataMessage(antParcel);
Log.d(TAG, "BROADCAST_DATA: " + m.getPayload());
heart = m.getPayload()[7];
Log.d(TAG, "BROADCAST_DATA: " + heart);
break;
case ACKNOWLEDGED_DATA:
// Rx Data
//updateData(new AcknowledgedDataMessage(antParcel).getPayload());
Log.d(TAG, "ACKNOWLEDGED_DATA: " + new AcknowledgedDataMessage(antParcel).getPayload());
break;
case CHANNEL_EVENT:
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
Log.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
case TX:
break;
case CHANNEL_COLLISION:
ucPageChange += 0x20;
ucPageChange &= 0xF0;
ucMessageCount += 1;
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
Log.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;
}
}
}
public boolean isConnected() {
return isConnected;
}
}

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,14 +5,15 @@ 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 {
private static MediaButtonReceiver instance;
@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);
@@ -20,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);
}
}
@@ -28,14 +29,39 @@ public class MediaButtonReceiver extends BroadcastReceiver {
private native void nativeOnMediaButtonEvent(int prev, int current, int max);
public static void registerReceiver(Context context) {
if (instance == null) {
instance = new MediaButtonReceiver();
try {
if (instance == null) {
instance = new MediaButtonReceiver();
}
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
if (context == null) {
QLog.e("MediaButtonReceiver", "Context is null, cannot register receiver");
return;
}
if (Build.VERSION.SDK_INT >= 34) {
try {
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
} catch (SecurityException se) {
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
}
} else {
try {
context.registerReceiver(instance, filter);
} catch (SecurityException se) {
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
}
}
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
} catch (IllegalArgumentException e) {
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
} catch (Exception e) {
QLog.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
}
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
Log.d("MediaButtonReceiver", "registerReceiver");
}
public static void unregisterReceiver(Context context) {
if (instance != null) {
context.unregisterReceiver(instance);

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;
@@ -63,25 +63,25 @@ public class QZAdbRemote implements DeviceConnectionListener {
@Override
public void notifyConnectionEstablished(DeviceConnection devConn) {
ADBConnected = true;
Log.i(LOG_TAG, "notifyConnectionEstablished" + lastCommand);
QLog.i(LOG_TAG, "notifyConnectionEstablished" + lastCommand);
}
@Override
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
ADBConnected = false;
Log.e(LOG_TAG, e.getMessage());
QLog.e(LOG_TAG, e.getMessage());
}
@Override
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
ADBConnected = false;
Log.e(LOG_TAG, e.getMessage());
QLog.e(LOG_TAG, e.getMessage());
}
@Override
public void notifyStreamClosed(DeviceConnection devConn) {
ADBConnected = false;
Log.e(LOG_TAG, "notifyStreamClosed");
QLog.e(LOG_TAG, "notifyStreamClosed");
}
@Override
@@ -96,7 +96,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
@@ -161,7 +161,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
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() {
@@ -173,7 +173,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
if (crypto == null)
{
Log.e(LOG_TAG,
QLog.e(LOG_TAG,
"Unable to generate and save RSA key pair");
return;
}
@@ -200,7 +200,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
}
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 +212,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

@@ -0,0 +1,74 @@
package org.cagnulen.qdomyoszwift;
import android.app.ActivityManager;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;
import com.garmin.android.connectiq.IQDevice;
import com.garmin.android.connectiq.exception.InvalidStateException;
import com.garmin.android.connectiq.exception.ServiceUnavailableException;
import android.content.BroadcastReceiver;
import android.content.ContextWrapper;
import android.content.IntentFilter;
import android.widget.Toast;
import org.jetbrains.annotations.Nullable;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.HashMap;
import java.util.List;
public class ZwiftHubBike {
private static Context context;
private static final String TAG = "ZwiftHubBike: ";
public static byte[] inclinationCommand(double inclination) throws InvalidProtocolBufferException {
ZwiftHub.SimulationParam.Builder simulation = ZwiftHub.SimulationParam.newBuilder();
simulation.setInclineX100((int)(inclination * 100.0));
ZwiftHub.HubCommand.Builder command = ZwiftHub.HubCommand.newBuilder();
command.setSimulation(simulation.build());
byte[] data = command.build().toByteArray();
byte[] fullData = new byte[data.length + 1];
fullData[0] = 0x04;
System.arraycopy(data, 0, fullData, 1, data.length);
return fullData;
}
public static byte[] setGearCommand(int gears) throws InvalidProtocolBufferException {
ZwiftHub.PhysicalParam.Builder physical = ZwiftHub.PhysicalParam.newBuilder();
physical.setGearRatioX10000(gears);
ZwiftHub.HubCommand.Builder command = ZwiftHub.HubCommand.newBuilder();
command.setPhysical(physical.build());
byte[] data = command.build().toByteArray();
byte[] fullData = new byte[data.length + 1];
fullData[0] = 0x04;
System.arraycopy(data, 0, fullData, 1, data.length);
return fullData;
}
}

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 {
@@ -113,7 +113,7 @@ public class ShellService extends Service implements DeviceConnectionListener {
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification());
}
} 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;
}
}

View File

@@ -0,0 +1,162 @@
syntax = "proto2";
package org.cagnulen.qdomyoszwift;
//-------------- Zwift Hub messages
// The command code prepending this message is 0x00
// This message is sent always following the change of the gear ratio probably to verify it was received properly
message HubRequest {
optional uint32 DataId = 1; // Value observed 520 and 0, 0 requests general info, 1-7 are the fields# in DeviceInformationContent, 520 requests the gear ratio
// 512 to 534 responds unidentifiable data
}
// The command code prepending this message is 0x03
message HubRidingData {
optional uint32 Power = 1;
optional uint32 Cadence = 2;
optional uint32 SpeedX100 = 3;
optional uint32 HR = 4;
optional uint32 Unknown1 = 5; // Values observed 0 when stopped, 2864, 4060, 4636, 6803
optional uint32 Unknown2 = 6; // Values observed 25714, 30091 (constant during session)
}
message SimulationParam {
optional sint32 Wind = 1; // Wind in m/s * 100. In zwift there is no wind (0). Negative is backwind
optional sint32 InclineX100 = 2; // Incline value * 100
optional uint32 CWa = 3; // Wind coefficient CW * a * 10000. In zwift this is constant 0.51 (5100)
optional uint32 Crr = 4; // Rolling resistance Crr * 100000. In zwift this is constant 0.004 (400)
}
message PhysicalParam {
optional uint32 GearRatioX10000 = 2;
optional uint32 BikeWeightx100 = 4;
optional uint32 RiderWeightx100 = 5;
}
// The command code prepending this message is 0x04
message HubCommand {
optional uint32 PowerTarget = 3;
optional SimulationParam Simulation = 4;
optional PhysicalParam Physical = 5;
}
//---------------- Zwift Play messages
enum PlayButtonStatus {
ON = 0;
OFF = 1;
}
// The command code prepending this message is 0x07
message PlayKeyPadStatus {
optional PlayButtonStatus RightPad = 1;
optional PlayButtonStatus Button_Y_Up = 2;
optional PlayButtonStatus Button_Z_Left = 3;
optional PlayButtonStatus Button_A_Right = 4;
optional PlayButtonStatus Button_B_Down = 5;
optional PlayButtonStatus Button_On = 6;
optional PlayButtonStatus Button_Shift = 7;
optional sint32 Analog_LR = 8;
optional sint32 Analog_UD = 9;
}
message PlayCommandParameters {
optional uint32 param1 = 1;
optional uint32 param2 = 2;
optional uint32 HapticPattern = 3;
}
message PlayCommandContents {
optional PlayCommandParameters CommandParameters = 1;
}
// The command code prepending this message is 0x12
// This is sent to the control point to configure and make the controller vibrate
message PlayCommand {
optional PlayCommandContents CommandContents = 2;
}
// The command code prepending this message is 0x19
// This is sent periodically when there are no button presses
message Idle {
optional uint32 Unknown2 = 2;
}
//----------------- Zwift Ride messages
enum RideButtonMask {
LEFT_BTN = 0x00001;
UP_BTN = 0x00002;
RIGHT_BTN = 0x00004;
DOWN_BTN = 0x00008;
A_BTN = 0x00010;
B_BTN = 0x00020;
Y_BTN = 0x00040;
Z_BTN = 0x00100;
SHFT_UP_L_BTN = 0x00200;
SHFT_DN_L_BTN = 0x00400;
POWERUP_L_BTN = 0x00800;
ONOFF_L_BTN = 0x01000;
SHFT_UP_R_BTN = 0x02000;
SHFT_DN_R_BTN = 0x04000;
POWERUP_R_BTN = 0x10000;
ONOFF_R_BTN = 0x20000;
}
enum RideAnalogLocation {
LEFT = 0;
RIGHT = 1;
UP = 2;
DOWN = 3;
}
message RideAnalogKeyPress {
optional RideAnalogLocation Location = 1;
optional sint32 AnalogValue = 2;
}
message RideAnalogKeyGroup {
repeated RideAnalogKeyPress GroupStatus = 1;
}
// The command code prepending this message is 0x23
message RideKeyPadStatus {
optional uint32 ButtonMap = 1;
optional RideAnalogKeyGroup AnalogButtons = 2;
}
//------------------ Zwift Click messages
// The command code prepending this message is 0x37
message ClickKeyPadStatus {
optional PlayButtonStatus Button_Plus = 1;
optional PlayButtonStatus Button_Minus = 2;
}
//------------------ Device Information requested after connection
// The command code prepending this message is 0x3c
message DeviceInformationContent {
optional uint32 Unknown1 = 1;
repeated uint32 SoftwareVersion = 2;
optional string DeviceName = 3;
optional uint32 Unknown4 = 4;
optional uint32 Unknown5 =5;
optional string SerialNumber = 6;
optional string HardwareVersion = 7;
repeated uint32 ReplyData = 8;
optional uint32 Unknown9 = 9;
optional uint32 Unknown10 = 10;
optional uint32 Unknown13 = 13;
}
message SubContent {
optional DeviceInformationContent Content = 1;
optional uint32 Unknown2 = 2;
optional uint32 Unknown4 = 4;
optional uint32 Unknown5 = 5;
optional uint32 Unknown6 = 6;
}
message DeviceInformation {
optional uint32 InformationId = 1;
optional SubContent SubContent = 2;
}

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;
@@ -119,7 +119,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
public void initializeConnection(){
Log.w(TAG, "initializeConnection start");
QLog.w(TAG, "initializeConnection start");
billingClient = BillingClient.newBuilder(m_context)
.enablePendingPurchases()
.setListener(this)
@@ -127,17 +127,17 @@ public class InAppPurchase implements PurchasesUpdatedListener
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 +191,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult)
{
Log.d(TAG, "Purchase acknowledged ");
QLog.d(TAG, "Purchase acknowledged ");
}
}
);
@@ -199,9 +199,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) {
@@ -216,18 +216,18 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
int responseCode = billingResult.getResponseCode();
Log.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
QLog.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
if (responseCode != RESULT_OK) {
Log.e(TAG, "queryDetails: Couldn't retrieve sku details.");
QLog.e(TAG, "queryDetails: Couldn't retrieve sku details.");
return;
}
if (skuDetailsList == null) {
Log.e(TAG, "queryDetails: No details list in response.");
QLog.e(TAG, "queryDetails: No details list in response.");
return;
}
Log.d(TAG, "onSkuDetailsResponse: skuDetailsList " + skuDetailsList);
QLog.d(TAG, "onSkuDetailsResponse: skuDetailsList " + skuDetailsList);
for (SkuDetails skuDetails : skuDetailsList) {
try {
String queriedProductId = skuDetails.getSku();
@@ -265,7 +265,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
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;
@@ -291,7 +291,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 +312,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());
}
}
};

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

1
src/build-qrc-qml.sh Executable file
View File

@@ -0,0 +1 @@
/Users/cagnulein/Qt/5.15.2/ios/bin/rcc qml.qrc -o ../build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qrc_qml.cpp

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

@@ -31,8 +31,21 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
uint16_t normalizeIncline = 0;
QSettings settings;
bool real_inclination_to_virtual_treamill_bridge = settings.value(QZSettings::real_inclination_to_virtual_treamill_bridge, QZSettings::default_real_inclination_to_virtual_treamill_bridge).toBool();
double inclination = ((treadmill *)Bike)->currentInclination().value();
if(real_inclination_to_virtual_treamill_bridge) {
double offset = settings.value(QZSettings::zwift_inclination_offset,
QZSettings::default_zwift_inclination_offset).toDouble();
double gain = settings.value(QZSettings::zwift_inclination_gain,
QZSettings::default_zwift_inclination_gain).toDouble();
inclination -= offset;
inclination /= gain;
}
if (dt == bluetoothdevice::TREADMILL)
normalizeIncline = (uint32_t)qRound(((treadmill *)Bike)->currentInclination().value() * 10);
normalizeIncline = (uint32_t)qRound(inclination * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
QByteArray inclineBytes;
@@ -40,7 +53,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclineBytes.append(a);
double ramp = 0;
if (dt == bluetoothdevice::TREADMILL)
ramp = qRadiansToDegrees(qAtan(((treadmill *)Bike)->currentInclination().value() / 100));
ramp = qRadiansToDegrees(qAtan(inclination / 100));
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
b = normalizeRamp & 0XFF;

View File

@@ -25,6 +25,8 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
settings.value(QZSettings::zwift_inclination_gain, QZSettings::default_zwift_inclination_gain).toDouble();
double CRRGain = settings.value(QZSettings::CRRGain, QZSettings::default_CRRGain).toDouble();
double CWGain = settings.value(QZSettings::CWGain, QZSettings::default_CWGain).toDouble();
bool zwift_play_emulator = settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool();
double min_inclination = settings.value(QZSettings::min_inclination, QZSettings::default_min_inclination).toDouble();
qDebug() << QStringLiteral("new requested resistance zwift erg grade ") + QString::number(iresistance) +
QStringLiteral(" enabled ") + force_resistance;
@@ -38,6 +40,11 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
percentage = (((qTan(qDegreesToRadians(iresistance / 100.0)) * 100.0) * 2.0) * gain) + offset;
}
if(min_inclination > grade) {
grade = min_inclination;
qDebug() << "grade override due to min_inclination " << min_inclination;
}
/*
Surface Road Crr MTB Crr Gravel Crr (Namebrand) Zwift Gravel Crr
Pavement .004 .01 .008 .008
@@ -60,9 +67,13 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
if (dt == bluetoothdevice::BIKE) {
// if the bike doesn't have the inclination by hardware, i'm simulating inclination with the value received
// form Zwift
if (!((bike *)Bike)->inclinationAvailableByHardware())
Bike->setInclination(grade + CRR_offset + CW_offset);
// from Zwift
if (!((bike *)Bike)->inclinationAvailableByHardware()) {
if(zwift_play_emulator)
Bike->setInclination(grade);
else
Bike->setInclination(grade + CRR_offset + CW_offset);
}
emit changeInclination(grade, percentage);

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

@@ -1,6 +1,7 @@
#include "devices/bike.h"
#include "qdebugfixup.h"
#include "homeform.h"
#include <QSettings>
bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); }
@@ -61,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) + " " +
@@ -95,18 +107,51 @@ double bike::gears() {
}
return m_gears + gears_offset;
}
void bike::setGears(double gears) {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
double gears_offset = settings.value(QZSettings::gears_offset, QZSettings::default_gears_offset).toDouble();
gears -= gears_offset;
qDebug() << "setGears" << gears;
// Check for boundaries and emit failure signals
if(gears_zwift_ratio && (gears > 24 || gears < 1)) {
qDebug() << "new gear value ignored because of gears_zwift_ratio setting!";
if(gears > 24) {
emit gearFailedUp();
} else {
emit gearFailedDown();
}
return;
}
if(gears > maxGears()) {
qDebug() << "new gear value ignored because of maxGears" << maxGears();
emit gearFailedUp();
return;
}
if(gears < minGears()) {
qDebug() << "new gear value ignored because of minGears" << minGears();
emit gearFailedDown();
return;
}
if(m_gears > gears) {
emit gearOkDown();
} else {
emit gearOkUp();
}
m_gears = gears;
settings.setValue(QZSettings::gears_current_value, m_gears);
if(homeform::singleton()) {
homeform::singleton()->updateGearsValue();
}
if (settings.value(QZSettings::gears_restore_value, QZSettings::default_gears_restore_value).toBool())
settings.setValue(QZSettings::gears_current_value, m_gears);
if (lastRawRequestedResistanceValue != -1) {
changeResistance(lastRawRequestedResistanceValue);
}
@@ -142,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);
@@ -169,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);
@@ -194,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

@@ -23,6 +23,9 @@ class bike : public bluetoothdevice {
double currentCrankRevolutions() override;
uint16_t lastCrankEventTime() override;
bool connected() override;
double defaultMaxGears() { return 9999.0; }
virtual double maxGears() { return defaultMaxGears(); }
virtual double minGears() { return -9999.0; }
virtual uint16_t watts();
virtual resistance_t pelotonToBikeResistance(int pelotonResistance);
virtual resistance_t resistanceFromPowerRequest(uint16_t power);
@@ -60,16 +63,28 @@ class bike : public bluetoothdevice {
void changeInclination(double grade, double percentage) override;
virtual void changeSteeringAngle(double angle) { m_steeringAngle = angle; }
virtual void resistanceFromFTMSAccessory(resistance_t res) { Q_UNUSED(res); }
void gearUp() {QSettings settings; setGears(gears() +
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble());}
void gearDown() {QSettings settings; setGears(gears() -
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble());}
void gearUp() {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() + (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}
void gearDown() {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() - (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}
Q_SIGNALS:
void bikeStarted();
void resistanceChanged(resistance_t resistance);
void resistanceRead(resistance_t resistance);
void steeringAngleChanged(double angle);
void gearOkUp(); // Signal when gear up succeeds
void gearOkDown(); // Signal when gear down succeeds
void gearFailedUp(); // Signal when gear up hits max
void gearFailedDown(); // Signal when gear down hits min
protected:
metric RequestedResistance;

File diff suppressed because it is too large Load Diff

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,13 +31,17 @@
#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"
#include "devices/csafeelliptical/csafeelliptical.h"
#endif
#include "devices/concept2skierg/concept2skierg.h"
#include "devices/crossrope/crossrope.h"
#include "devices/cscbike/cscbike.h"
#include "devices/cycleopsphantombike/cycleopsphantombike.h"
#include "devices/deeruntreadmill/deerruntreadmill.h"
#include "devices/domyosbike/domyosbike.h"
#include "devices/domyoselliptical/domyoselliptical.h"
#include "devices/domyosrower/domyosrower.h"
@@ -43,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"
@@ -66,12 +74,15 @@
#include "devices/iconceptelliptical/iconceptelliptical.h"
#include "devices/inspirebike/inspirebike.h"
#include "devices/keepbike/keepbike.h"
#include "devices/kineticinroadbike/kineticinroadbike.h"
#include "devices/kingsmithr1protreadmill/kingsmithr1protreadmill.h"
#include "devices/kingsmithr2treadmill/kingsmithr2treadmill.h"
#include "devices/lifefitnesstreadmill/lifefitnesstreadmill.h"
#include "devices/lifespantreadmill/lifespantreadmill.h"
#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"
@@ -85,6 +96,7 @@
#include "devices/pafersbike/pafersbike.h"
#include "devices/paferstreadmill/paferstreadmill.h"
#include "devices/pelotonbike/pelotonbike.h"
#include "devices/pitpatbike/pitpatbike.h"
#include "devices/proformbike/proformbike.h"
#include "devices/proformelliptical/proformelliptical.h"
#include "devices/proformellipticaltrainer/proformellipticaltrainer.h"
@@ -109,8 +121,10 @@
#include "devices/spirittreadmill/spirittreadmill.h"
#include "devices/sportsplusbike/sportsplusbike.h"
#include "devices/sportsplusrower/sportsplusrower.h"
#include "devices/sportstechbike/sportstechbike.h"
#include "devices/sportstechelliptical/sportstechelliptical.h"
#include "devices/sramAXSController/sramAXSController.h"
#include "devices/stagesbike/stagesbike.h"
#include "devices/renphobike/renphobike.h"
@@ -121,11 +135,13 @@
#include "devices/echelonstride/echelonstride.h"
#include "templateinfosenderbuilder.h"
#include "technogymbike/technogymbike.h"
#include "devices/toorxtreadmill/toorxtreadmill.h"
#include "devices/treadmill.h"
#include "devices/truetreadmill/truetreadmill.h"
#include "devices/trxappgateusbbike/trxappgateusbbike.h"
#include "devices/trxappgateusbelliptical/trxappgateusbelliptical.h"
#include "devices/trxappgateusbrower/trxappgateusbrower.h"
#include "devices/trxappgateusbtreadmill/trxappgateusbtreadmill.h"
#include "devices/ultrasportbike/ultrasportbike.h"
#include "devices/wahookickrheadwind/wahookickrheadwind.h"
@@ -163,19 +179,24 @@ 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;
#ifndef Q_OS_IOS
computrainerbike *computrainerBike = nullptr;
csaferower *csafeRower = nullptr;
csafeelliptical *csafeElliptical = nullptr;
#endif
concept2skierg *concept2Skierg = nullptr;
cycleopsphantombike *cycleopsphantomBike = nullptr;
deerruntreadmill *deerrunTreadmill = nullptr;
domyostreadmill *domyos = nullptr;
domyosbike *domyosBike = nullptr;
domyosrower *domyosRower = nullptr;
@@ -186,14 +207,17 @@ 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;
trxappgateusbbike *trxappgateusbBike = nullptr;
trxappgateusbrower *trxappgateusbRower = nullptr;
trxappgateusbelliptical *trxappgateusbElliptical = nullptr;
echelonconnectsport *echelonConnectSport = nullptr;
yesoulbike *yesoulBike = nullptr;
flywheelbike *flywheelBike = nullptr;
moxy5sensor *moxy5Sensor = nullptr;
nordictrackelliptical *nordictrackElliptical = nullptr;
nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr;
nordictrackifitadbbike *nordictrackifitadbBike = nullptr;
@@ -217,9 +241,11 @@ class bluetooth : public QObject, public SignalHandler {
truetreadmill *trueTreadmill = nullptr;
horizongr7bike *horizonGr7Bike = nullptr;
schwinnic4bike *schwinnIC4Bike = nullptr;
technogymbike* technogymBike = nullptr;
sportstechbike *sportsTechBike = nullptr;
sportstechelliptical *sportsTechElliptical = nullptr;
sportsplusbike *sportsPlusBike = nullptr;
sportsplusrower *sportsPlusRower = nullptr;
inspirebike *inspireBike = nullptr;
snodebike *snodeBike = nullptr;
eslinkertreadmill *eslinkerTreadmill = nullptr;
@@ -240,7 +266,9 @@ 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;
kingsmithr1protreadmill *kingsmithR1ProTreadmill = nullptr;
kingsmithr2treadmill *kingsmithR2Treadmill = nullptr;
@@ -248,6 +276,7 @@ class bluetooth : public QObject, public SignalHandler {
pafersbike *pafersBike = nullptr;
paferstreadmill *pafersTreadmill = nullptr;
tacxneo2 *tacxneo2Bike = nullptr;
pitpatbike *pitpatBike = nullptr;
renphobike *renphoBike = nullptr;
shuaa5treadmill *shuaA5Treadmill = nullptr;
heartratebelt *heartRateBelt = nullptr;
@@ -260,6 +289,7 @@ class bluetooth : public QObject, public SignalHandler {
wahookickrsnapbike *wahooKickrSnapBike = nullptr;
ypooelliptical *ypooElliptical = nullptr;
ziprotreadmill *ziproTreadmill = nullptr;
kineticinroadbike *kineticInroadBike = nullptr;
strydrunpowersensor *powerTreadmill = nullptr;
eliterizer *eliteRizer = nullptr;
elitesterzosmart *eliteSterzoSmart = nullptr;
@@ -272,6 +302,8 @@ class bluetooth : public QObject, public SignalHandler {
QList<eliteariafan *> eliteAriaFan;
QList<zwiftclickremote* > zwiftPlayDevice;
zwiftclickremote* zwiftClickRemote = nullptr;
sramaxscontroller* sramAXSController = nullptr;
elitesquarecontroller* eliteSquareController = nullptr;
QString filterDevice = QLatin1String("");
bool testResistance = false;
@@ -306,6 +338,7 @@ class bluetooth : public QObject, public SignalHandler {
bool eliteSterzoSmartAvaiable();
bool fitmetriaFanfitAvaiable();
bool zwiftDeviceAvaiable();
bool sramDeviceAvaiable();
bool fitmetria_fanfit_isconnected(QString name);
#ifdef Q_OS_WIN
@@ -344,6 +377,10 @@ class bluetooth : public QObject, public SignalHandler {
void speedChanged(double);
void inclinationChanged(double, double);
void connectedAndDiscovered();
void gearDown();
void gearUp();
void gearFailedDown();
void gearFailedUp();
signals:
};

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();
@@ -149,6 +153,7 @@ double bluetoothdevice::inclinationDifficultOffset() { return m_inclination_diff
void bluetoothdevice::cadenceSensor(uint8_t cadence) { Q_UNUSED(cadence) }
void bluetoothdevice::powerSensor(uint16_t power) { Q_UNUSED(power) }
void bluetoothdevice::speedSensor(double speed) { Q_UNUSED(speed) }
void bluetoothdevice::inclinationSensor(double grade, double inclination) { Q_UNUSED(grade); Q_UNUSED(inclination) }
void bluetoothdevice::instantaneousStrideLengthSensor(double length) { Q_UNUSED(length); }
void bluetoothdevice::groundContactSensor(double groundContact) { Q_UNUSED(groundContact); }
void bluetoothdevice::verticalOscillationSensor(double verticalOscillation) { Q_UNUSED(verticalOscillation); }
@@ -254,6 +259,7 @@ void bluetoothdevice::update_hr_from_external() {
h.setSpeed(Speed.value());
h.setPower(m_watt.value());
h.setCadence(Cadence.value());
h.setSteps(StepCount.value());
Heart = appleWatchHeartRate;
qDebug() << "Current Heart from Apple Watch: " << QString::number(appleWatchHeartRate);
#endif
@@ -279,6 +285,7 @@ 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);
@@ -299,6 +306,7 @@ 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);
@@ -318,6 +326,7 @@ 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);
@@ -473,9 +482,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);

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.
*/
@@ -216,6 +221,18 @@ class bluetoothdevice : public QObject {
*/
metric wattsMetric();
/**
* @brief wattsMetricforUi Show the wattage applying averaging in case the user requested this. Units: watts
*/
double wattsMetricforUI() {
QSettings settings;
bool power5s = settings.value(QZSettings::power_avg_5s, QZSettings::default_power_avg_5s).toBool();
if (power5s)
return wattsMetric().average5s();
else
return wattsMetric().value();
}
/**
* @brief changeFanSpeed Tries to change the fan speed.
* @param speed The requested fan speed. Units: depends on device
@@ -401,7 +418,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 };
/**
@@ -426,6 +443,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);
@@ -433,6 +455,10 @@ 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);
virtual void changeInclination(double grade, double percentage);
@@ -568,9 +594,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
@@ -664,6 +695,11 @@ class bluetoothdevice : public QObject {
* @brief _ergTable The current erg table
*/
ergTable _ergTable;
/**
* @brief StepCount A metric to get and set the step count. Unit: step
*/
metric StepCount;
/**
* @brief Collect the number of seconds in each zone for the current heart rate

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(12)) + ((uint16_t)((uint8_t)packet.at(13)) << 8);
double data = (double)convertedData / 100.0f;
return data;
} 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

@@ -64,8 +64,24 @@ csafe::csafe() {
cmds["CSAFE_PM_GET_WORKTIME"] = populateCmd(0xa0, QList<int>(), 0x1a);
cmds["CSAFE_PM_GET_WORKDISTANCE"] = populateCmd(0xa3, QList<int>(), 0x1a);
// LIFE FITNESS specific commands
cmds["CSAFE_LF_GET_DETAIL"] = populateCmd(0xd0, QList<int>());
// Generic long commands
cmds["CSAFE_SETPROGRAM_CMD"] = populateCmd(0x24, QList<int>() << 1 << 1);
cmds["CSAFE_SETUSERINFO_CMD"] = populateCmd(0x2B, QList<int>() << 2 << 1 << 1 << 1);
cmds["CSAFE_SETLEVEL_CMD"] = populateCmd(0x2D, QList<int>() << 1);
// Generic Short Commands
cmds["CSAFE_GETSTATUS_CMD"] = populateCmd(0x80, QList<int>());
cmds["CSAFE_GOINUSE_CMD"] = populateCmd(0x85, QList<int>());
cmds["CSAFE_GETCALORIES_CMD"] = populateCmd(0xa3, QList<int>());
cmds["CSAFE_GETPROGRAM_CMD"] = populateCmd(0xA4, QList<int>());
cmds["CSAFE_GETPACE_CMD"] = populateCmd(0xa6, QList<int>());
cmds["CSAFE_GETCADENCE_CMD"] = populateCmd(0xa7, QList<int>());
cmds["CSAFE_GETHORIZONTAL_CMD"] = populateCmd(0xA1, QList<int>());
cmds["CSAFE_GETSPEED_CMD"] = populateCmd(0xA5, QList<int>());
cmds["CSAFE_GETUSERINFO_CMD"] = populateCmd(0xAB, QList<int>());
cmds["CSAFE_GETHRCUR_CMD"] = populateCmd(0xb0, QList<int>());
cmds["CSAFE_GETPOWER_CMD"] = populateCmd(0xb4, QList<int>());
@@ -87,7 +103,8 @@ csafe::csafe() {
resp[0xA0] = qMakePair(QString("CSAFE_GETTWORK_CMD"), QList<int>() << 1 << 1 << 1);
resp[0xA1] = qMakePair(QString("CSAFE_GETHORIZONTAL_CMD"), QList<int>() << 2 << 1);
resp[0xA3] = qMakePair(QString("CSAFE_GETCALORIES_CMD"), QList<int>() << 2);
resp[0xA4] = qMakePair(QString("CSAFE_GETPROGRAM_CMD"), QList<int>() << 1);
resp[0xA4] = qMakePair(QString("CSAFE_GETPROGRAM_CMD"), QList<int>() << 1 << 1);
resp[0xA5] = qMakePair(QString("CSAFE_GETSPEED_CMD"), QList<int>() << 2 << 1);
resp[0xA6] = qMakePair(QString("CSAFE_GETPACE_CMD"), QList<int>() << 2 << 1);
resp[0xA7] = qMakePair(QString("CSAFE_GETCADENCE_CMD"), QList<int>() << 2 << 1);
resp[0xAB] = qMakePair(QString("CSAFE_GETUSERINFO_CMD"), QList<int>() << 2 << 1 << 1 << 1);
@@ -105,6 +122,8 @@ csafe::csafe() {
resp[0x21] = qMakePair(QString("CSAFE_SETHORIZONTAL_CMD"), QList<int>() << 0);
resp[0x23] = qMakePair(QString("CSAFE_SETCALORIES_CMD"), QList<int>() << 0);
resp[0x24] = qMakePair(QString("CSAFE_SETPROGRAM_CMD"), QList<int>() << 0);
resp[0x2B] = qMakePair(QString("CSAFE_SETUSERINFO_CMD"), QList<int>() << 0);
resp[0x2D] = qMakePair(QString("CSAFE_SETLEVEL_CMD"), QList<int>() << 0);
resp[0x34] = qMakePair(QString("CSAFE_SETPOWER_CMD"), QList<int>() << 0);
resp[0x70] = qMakePair(QString("CSAFE_GETCAPS_CMD"), QList<int>() << 11);
@@ -129,6 +148,9 @@ csafe::csafe() {
resp[0x1A6C] =
qMakePair(QString("CSAFE_PM_GET_HEARTBEATDATA"),
QList<int>() << 1 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2 << 2);
// LIFE FITNESS specific response
resp[0xD0] = qMakePair(QString("CSAFE_LF_GET_DETAIL"), QList<int>() << 0x1c);
}
QList<QList<int>> csafe::populateCmd(int First, QList<int> Second, int Third) {
@@ -140,7 +162,9 @@ QList<QList<int>> csafe::populateCmd(int First, QList<int> Second, int Third) {
second.clear();
third.clear();
first.append(First);
foreach (int a, Second) { second.append(a); }
foreach (int a, Second) {
second.append(a);
}
third.append(Third);
ret.append(first);
ret.append(second);
@@ -155,7 +179,9 @@ QList<QList<int>> csafe::populateCmd(int First, QList<int> Second) {
first.clear();
second.clear();
first.append(First);
foreach (int a, Second) { second.append(a); }
foreach (int a, Second) {
second.append(a);
}
ret.append(first);
ret.append(second);
return ret;
@@ -195,7 +221,7 @@ QString csafe::bytes2ascii(const QVector<quint8> &raw_bytes) {
return word;
}
QByteArray csafe::write(const QStringList &arguments) {
QByteArray csafe::write(const QStringList &arguments, bool surround_msg) {
int i = 0;
QVector<quint8> message;
int wrapper = 0;
@@ -204,6 +230,10 @@ QByteArray csafe::write(const QStringList &arguments) {
while (i < arguments.size()) {
QString arg = arguments[i];
if (!cmds.contains(arg)) {
qWarning("CSAFE Command not implemented: %s", qPrintable(arg));
return QByteArray();
}
const auto &cmdprop = cmds[arg];
QVector<quint8> command;
@@ -290,27 +320,31 @@ QByteArray csafe::write(const QStringList &arguments) {
qWarning("Message is too long: %d", message.size());
}
int maxmessage = qMax(message.size() + 1, maxresponse); // report IDs
if (surround_msg) { // apply non-standard wrapping for PM3 rower
int maxmessage = qMax(message.size() + 1, maxresponse); // report IDs
if (maxmessage <= 21) {
message.prepend(0x01);
message.append(QVector<quint8>(21 - message.size()));
} else if (maxmessage <= 63) {
message.prepend(0x04);
message.append(QVector<quint8>(63 - message.size()));
} else if ((message.size() + 1) <= 121) {
message.prepend(0x02);
message.append(QVector<quint8>(121 - message.size()));
if (maxresponse > 121) {
qWarning("Response may be too long to receive. Max possible length: %d", maxresponse);
if (maxmessage <= 21) {
message.prepend(0x01);
message.append(QVector<quint8>(21 - message.size()));
} else if (maxmessage <= 63) {
message.prepend(0x04);
message.append(QVector<quint8>(63 - message.size()));
} else if ((message.size() + 1) <= 121) {
message.prepend(0x02);
message.append(QVector<quint8>(121 - message.size()));
if (maxresponse > 121) {
qWarning("Response may be too long to receive. Max possible length: %d", maxresponse);
}
} else {
qWarning("Message too long. Message length: %d", message.size());
message.clear();
}
} else {
qWarning("Message too long. Message length: %d", message.size());
message.clear();
}
QByteArray ret;
foreach (int a, message) { ret.append(a); }
foreach (int a, message) {
ret.append(a);
}
return ret;
}
@@ -325,9 +359,7 @@ QVector<quint8> csafe::check_message(QVector<quint8> message) {
quint8 stuffvalue = message.takeAt(i + 1);
message[i] = 0xF0 | stuffvalue;
}
checksum ^= message[i]; // calculate checksum
++i;
}
@@ -345,15 +377,21 @@ QVector<quint8> csafe::check_message(QVector<quint8> message) {
QVariantMap csafe::read(const QVector<quint8> &transmission) {
QVector<quint8> message;
bool stopfound = false;
int startflag = transmission[1];
int j = 0;
if (startflag == Extended_Frame_Start_Flag) {
j = 4;
} else if (startflag == Standard_Frame_Start_Flag) {
j = 2;
} else {
while (j < transmission.size()) {
int startflag = transmission[j];
if (startflag == Extended_Frame_Start_Flag) {
j = j + 3;
break;
} else if (startflag == Standard_Frame_Start_Flag) {
++j;
break;
} else {
++j;
}
}
if (j >= transmission.size()) {
qWarning("No Start Flag found.");
return QVariantMap();
}
@@ -371,7 +409,6 @@ QVariantMap csafe::read(const QVector<quint8> &transmission) {
qWarning("No Stop Flag found.");
return QVariantMap();
}
message = check_message(message);
int status = message.takeFirst();

View File

@@ -34,7 +34,7 @@ class csafe {
public:
csafe();
QByteArray write(const QStringList &arguments);
QByteArray write(const QStringList &arguments , bool surround_msg = false); //surround_msg is for wrapping the communication in CSAFE non-standard way for some devices like PM3
QVector<quint8> check_message(QVector<quint8> message);
QVariantMap read(const QVector<quint8> &transmission);
};

View File

@@ -0,0 +1,128 @@
#include "csaferunner.h"
CsafeRunnerThread::CsafeRunnerThread() {}
CsafeRunnerThread::CsafeRunnerThread(QString deviceFileName, int sleepTime) {
setDevice(deviceFileName);
setSleepTime(sleepTime);
}
void CsafeRunnerThread::setDevice(const QString &device) { deviceName = device; }
void CsafeRunnerThread::setBaudRate(uint32_t _baudRate) { baudRate = _baudRate; }
void CsafeRunnerThread::setSleepTime(int time) { sleepTime = time; }
void CsafeRunnerThread::addRefreshCommand(const QStringList &commands) {
mutex.lock();
refreshCommands.append(commands);
mutex.unlock();
}
void CsafeRunnerThread::sendCommand(const QStringList &commands) {
mutex.lock();
if (commandQueue.size() < MAX_QUEUE_SIZE) {
commandQueue.enqueue(commands);
mutex.unlock();
} else {
qDebug() << "CSAFE port commands QUEUE FULL. Dropping commands" << commands;
}
}
void CsafeRunnerThread::run() {
int rc = 0;
SerialHandler *serial = SerialHandler::create(deviceName, baudRate);
serial->setEndChar(0xf2); // end of frame for CSAFE
serial->setTimeout(1200); // CSAFE spec specifies 1s timeout
csafe *csafeInstance = new csafe();
int connectioncounter = 20; // counts timeouts. If 10 timeouts in a row, then the port is closed and reopened
int refresh_nr = -1;
QStringList refreshCommand = {};
while (1) {
if (connectioncounter > 10 || !serial->isOpen()) {
serial->closePort();
rc = serial->openPort();
if (rc != 0) {
emit portAvailable(false);
connectioncounter++;
qDebug() << "Error opening serial port " << deviceName << "rc=" << rc << " sleeping for "
<< "5s";
QThread::msleep(5000);
continue;
} else {
emit portAvailable(true);
connectioncounter = 0;
}
}
int elapsed = 0;
while (elapsed < sleepTime || sleepTime == -1) {
QThread::msleep(50);
elapsed += 50;
// TODO: does not seem to work with netsocket as intended. (no data available)
// Needs further testing, maybe because the port is already closed and needs to remain open.
// No issue for current implementations as they do not use unsolicited slave data / cmdAutoUpload .
if (serial->dataAvailable() > 0 || !commandQueue.isEmpty()) {
qDebug() << "CSAFE port data available. " << serial->dataAvailable() << " bytes"
<< "commands in queue: " << commandQueue.size();
break;
}
}
QByteArray ret;
mutex.lock();
if (!commandQueue.isEmpty()) {
ret = csafeInstance->write(commandQueue.dequeue());
qDebug() << "CSAFE port commands processed from queue. Remaining commands in queue: "
<< commandQueue.size();
} else {
if (!(elapsed < sleepTime) || !refreshCommands.isEmpty()) {
if (refreshCommands.length() > 0) {
refresh_nr++;
if (refresh_nr >= refreshCommands.length()) {
refresh_nr = 0;
}
QStringList refreshCommand = refreshCommands[refresh_nr];
ret = csafeInstance->write(refreshCommand);
}
}
}
mutex.unlock();
if (!ret.isEmpty()) { // we have commands to send
qDebug() << "CSAFE >> " << ret.toHex(' ');
rc = serial->rawWrite((uint8_t *)ret.data(), ret.length());
if (rc < 0) {
qDebug() << "Error writing serial port " << deviceName << "rc=" << rc;
connectioncounter++;
continue;
}
} else {
qDebug() << "CSAFE Slave unsolicited data present.";
}
static uint8_t rx[120];
rc = serial->rawRead(rx, 120, true);
if (rc > 0) {
qDebug() << "CSAFE << " << QByteArray::fromRawData((const char *)rx, rc).toHex(' ') << " (" << rc << ")";
connectioncounter = 0;
} else {
qDebug() << "Error reading serial port " << deviceName << " rc=" << rc;
connectioncounter++;
continue;
}
QVector<quint8> v;
for (int i = 0; i < rc; i++)
v.append(rx[i]);
QVariantMap frame = csafeInstance->read(v);
emit onCsafeFrame(frame);
memset(rx, 0x00, sizeof(rx));
}
serial->closePort();
}

View File

@@ -0,0 +1,44 @@
#include "csafe.h"
#include "qzsettings.h"
#include "serialhandler.h"
#include <QDebug>
#include <QMutex>
#include <QQueue>
#include <QSettings>
#include <QThread>
#include <QVariantMap>
#include <QVector>
#define MAX_QUEUE_SIZE 100
/**
* @brief The CsafeRunnerThread class is a thread that runs the CSAFE protocol interaction.
* It periodically sends the refresh commands processes the responses.
* It also allows sending additional commands to the device.
*/
class CsafeRunnerThread : public QThread {
Q_OBJECT
public:
explicit CsafeRunnerThread();
explicit CsafeRunnerThread(QString deviceFileName, int sleepTime = 200);
void setDevice(const QString &device);
void setBaudRate(uint32_t baudRate = 9600);
void setSleepTime(int time);
void addRefreshCommand(const QStringList &commands);
void run();
public slots:
void sendCommand(const QStringList &commands);
signals:
void onCsafeFrame(const QVariantMap &frame);
void portAvailable(bool available);
private:
QString deviceName;
uint32_t baudRate = 9600;
int sleepTime = 200;
QList<QStringList> refreshCommands;
QQueue<QStringList> commandQueue;
QMutex mutex;
};

View File

@@ -0,0 +1,101 @@
#include "csafeutility.h"
// Static data map initialization
const QMap<int, QPair<QString, double>> CSafeUtility::unitData = {{1, {"mile", 1609.34}},
{2, {"0.1 mile", 160.934}},
{3, {"0.01 mile", 16.0934}},
{4, {"0.001 mile", 1.60934}},
{5, {"ft", 0.3048}},
{6, {"inch", 0.0254}},
{7, {"lbs.", 0.453592}},
{8, {"0.1 lbs.", 0.0453592}},
{10, {"10 ft", 3.048}},
{16, {"mile/hour", 0.44704}},
{17, {"0.1 mile/hour", 0.044704}},
{18, {"0.01 mile/hour", 0.0044704}},
{19, {"ft/minute", 0.00508}},
{33, {"Km", 1000}},
{34, {"0.1 km", 100}},
{35, {"0.01 km", 10}},
{36, {"Meter", 1}},
{37, {"0.1 meter", 0.1}},
{38, {"Cm", 0.01}},
{39, {"Kg", 1}},
{40, {"0.1 kg", 0.1}},
{48, {"Km/hour", 0.277778}},
{49, {"0.1 Km/hour", 0.0277778}},
{50, {"0.01 Km/hour", 0.00277778}},
{51, {"Meter/minute", 0.0166667}},
{55, {"Minutes/mile", 1}},
{56, {"Minutes/km", 1}},
{57, {"Seconds/km", 1}},
{58, {"Seconds/mile", 1}},
{65, {"floors", 1}},
{66, {"0.1 floors", 0.1}},
{67, {"steps", 1}},
{68, {"revolutions", 1}},
{69, {"strides", 1}},
{70, {"strokes", 1}},
{71, {"beats", 1}},
{72, {"calories", 1}},
{73, {"Kp", 1}},
{74, {"% grade", 1}},
{75, {"0.01 % grade", 0.01}},
{76, {"0.1 % grade", 0.1}},
{79, {"0.1 floors/minute", 0.1}},
{80, {"floors/minute", 1}},
{81, {"steps/minute", 1}},
{82, {"revs/minute", 1}},
{83, {"strides/minute", 1}},
{84, {"stokes/minute", 1}},
{85, {"beats/minute", 1}},
{86, {"calories/minute", 1}},
{87, {"calories/hour", 1}},
{88, {"Watts", 1}},
{89, {"Kpm", 1}},
{90, {"Inch-Lb", 1}},
{91, {"Foot-Lb", 1}},
{92, {"Newton-Meters", 1}},
{97, {"Amperes", 1}},
{98, {"0.001 Amperes", 0.001}},
{99, {"Volts", 1}},
{100, {"0.001 Volts", 0.001}}};
QString CSafeUtility::getUnitName(int unitCode) {
if (unitData.contains(unitCode)) {
return unitData[unitCode].first; // Return the description
}
return "Unknown unit";
}
double CSafeUtility::convertToStandard(int unitCode, double value) {
if (unitData.contains(unitCode)) {
return value * unitData[unitCode].second; // Apply the conversion factor
}
return value; // Return the original value if no conversion factor is available
}
QString CSafeUtility::statusByteToText(int status) {
switch (status) {
case 0x00:
return "Error";
case 0x01:
return "Ready";
case 0x02:
return "Idle";
case 0x03:
return "Have ID";
case 0x05:
return "In Use";
case 0x06:
return "Pause";
case 0x07:
return "Finish";
case 0x08:
return "Manual";
case 0x09:
return "Off line";
default:
return "Unknown";
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2024 Marcel Verpaalen (marcel@verpaalen.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* This emulates a serial port over a network connection.
* e.g. as created by ser2net or hardware serial to ethernet converters
*
*/
#ifndef CSAFEUTILITY_H
#define CSAFEUTILITY_H
#include <cstdint> // For uint8_t
#include <QString>
#include <QMap>
#include <QPair>
/**
* @brief This class contains some utility functions supporting the CSAFE protocol
*/
class CSafeUtility {
public:
static QString getUnitName(int unitCode);
static double convertToStandard(int unitCode, double value);
static QString statusByteToText(int statusByte);
private:
// Static map to hold unit data
static const QMap<int, QPair<QString, double>> unitData;
};
#endif // CSAFEUTILITY_H

View File

@@ -0,0 +1,28 @@
#include "kalmanfilter.h"
KalmanFilter::KalmanFilter(double measure_error, double estimate_error, double process_noise_q, double initial_Value) {
setMeasurementError(measure_error);
setEstimateError(estimate_error);
setProcessNoise(process_noise_q);
estimate = initial_Value;
}
KalmanFilter::~KalmanFilter() {};
double KalmanFilter::updateEstimate(double measurement) {
estimate_error += process_noise_q;
gain = estimate_error / (estimate_error + measure_error);
estimate += gain * (measurement - estimate);
estimate_error *= (1 - gain);
return estimate;
}
void KalmanFilter::setMeasurementError(double measure_err) { measure_error = measure_err; }
void KalmanFilter::setEstimateError(double estimate_err) { estimate_error = estimate_err; }
void KalmanFilter::setProcessNoise(double noise_q) { process_noise_q = noise_q; }
double KalmanFilter::getGain() { return gain; }
double KalmanFilter::getEstimateError() { return estimate_error; }

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 Marcel Verpaalen (marcel@verpaalen.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* This emulates a serial port over a network connection.
* e.g. as created by ser2net or hardware serial to ethernet converters
*
*/
#ifndef KALMAN_FILTER_H
#define KALMAN_FILTER_H
#include <math.h>
/**
* @brief A simple Kalman filter for smoothing noisy data.
*/
class KalmanFilter {
public:
KalmanFilter(double measure_error, double estimate_error, double process_noise_q = 0, double initial_Value = 0);
~KalmanFilter();
double updateEstimate(double measurement);
void setMeasurementError(double measure_err);
void setEstimateError(double estimate_err);
void setProcessNoise(double noise_q);
double getGain();
double getEstimateError();
private:
double measure_error;
double estimate_error;
double process_noise_q;
double estimate = 0;
double gain = 0;
};
#endif

View File

@@ -0,0 +1,118 @@
#include "netserial.h"
NetSerial::NetSerial(QString deviceFilename) : socket(new QTcpSocket()), _timeout(1000), endChar('\n') {
setDevice(deviceFilename);
}
NetSerial::~NetSerial() {
closePort();
delete socket;
}
void NetSerial::setTimeout(int timeout) { this->_timeout = timeout; }
void NetSerial::setDevice(const QString &devname) {
if (!devname.isEmpty()) {
deviceFilename = devname;
parseDeviceFilename(devname);
}
}
void NetSerial::setEndChar(uint8_t endChar) { this->endChar = endChar; }
bool NetSerial::isOpen() const { return socket->state() == QAbstractSocket::ConnectedState; }
int NetSerial::openPort() {
if (serverAddress.isEmpty() || serverPort == 0) {
qDebug() << "Invalid server address or port";
return -1;
}
socket->connectToHost(serverAddress, serverPort);
if (!socket->waitForConnected(_timeout)) {
qDebug() << "Failed to connect to server:" << socket->errorString();
return -1;
}
return 0;
}
int NetSerial::closePort() {
if (isOpen()) {
socket->disconnectFromHost();
if (socket->state() != QAbstractSocket::UnconnectedState) {
socket->waitForDisconnected(_timeout);
}
}
return 0;
}
int NetSerial::dataAvailable() {
if (!isOpen()) {
qDebug() << "Socket not connected.";
return -1;
}
if (socket->bytesAvailable() > 0) {
qDebug() << "Socket data is available!!!!!!!!!!!!!!.";
}
return socket->bytesAvailable();
}
int NetSerial::rawWrite(uint8_t *bytes, int size) {
if (!isOpen()) {
qDebug() << "Socket not connected.";
return -1;
}
QByteArray data(reinterpret_cast<const char *>(bytes), size);
qint64 bytesWritten = socket->write(data);
if (bytesWritten == -1) {
qDebug() << "Failed to write to socket:" << socket->errorString();
return -1;
}
if (!socket->waitForBytesWritten(_timeout)) {
qDebug() << "Write operation timed out.";
return -1;
}
return static_cast<int>(bytesWritten);
}
int NetSerial::rawRead(uint8_t bytes[], int size, bool line) {
if (!isOpen()) {
qDebug() << "Socket not connected.";
return -1;
}
QByteArray buffer;
while (buffer.size() < size) {
if (!socket->waitForReadyRead(_timeout)) {
qDebug() << "Read operation timed out.";
return buffer.size() > 0 ? buffer.size() : -1;
}
buffer.append(socket->read(size - buffer.size()));
if (line && buffer.contains(static_cast<char>(endChar))) {
int index = buffer.indexOf(static_cast<char>(endChar)) + 1;
memcpy(bytes, buffer.data(), index);
return index;
}
}
memcpy(bytes, buffer.data(), buffer.size());
return buffer.size();
}
bool NetSerial::parseDeviceFilename(const QString &filename) {
// Format: "server:port", e.g., "127.0.0.1:12345"
QStringList parts = filename.split(':');
if (parts.size() == 2) {
serverAddress = parts[0];
serverPort = parts[1].toUShort();
return true;
} else {
qDebug() << "Invalid device filename format. Expected 'server:port'.";
return false;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2024 Marcel Verpaalen (marcel@verpaalen.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*
*/
#ifndef NETSERIAL_H
#define NETSERIAL_H
#include "serialhandler.h"
#include <QString>
#include <QHostAddress>
#include <QTcpSocket>
#include <QDebug>
/**
* @brief This is a simple implementation of serial port emulation over TCP
* It emulates a serial port over a network connection.
* e.g. as created by ser2net or hardware serial to ethernet converters
*/
class NetSerial : public SerialHandler {
public:
NetSerial(QString deviceFilename);
~NetSerial() ;
int openPort() override;
int closePort() override;
int dataAvailable() override;
int rawWrite(uint8_t *bytes, int size) override;
int rawRead(uint8_t bytes[], int size, bool line = false) override;
bool isOpen() const override;
void setTimeout(int timeout) override;
void setEndChar(uint8_t endChar) override;
void setDevice(const QString &devname) override;
private:
QString deviceFilename;
QString serverAddress;
quint16 serverPort;
QTcpSocket *socket;
int _timeout = 1000; // Timeout in milliseconds
uint8_t endChar = '\n';
bool parseDeviceFilename(const QString &filename);
};
#endif // NETSERIAL_H

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