mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
850 Commits
build-883
...
Core-Senso
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf2f6406ed | ||
|
|
5fea12e6bc | ||
|
|
dcdd4fdbb6 | ||
|
|
aac9f7a20e | ||
|
|
5146352494 | ||
|
|
f666c4cc55 | ||
|
|
882778b9ff | ||
|
|
783b832805 | ||
|
|
defb177f53 | ||
|
|
d511df4186 | ||
|
|
ffe79b434a | ||
|
|
9050be6063 | ||
|
|
744d2d138f | ||
|
|
612acf3610 | ||
|
|
a84e4ca84d | ||
|
|
e4b4dd943e | ||
|
|
121a046bb8 | ||
|
|
21e0a7edc8 | ||
|
|
83e1c136e4 | ||
|
|
9a19956b1c | ||
|
|
18571131ca | ||
|
|
eba9a2c8d5 | ||
|
|
dbf5b7005d | ||
|
|
64476f7e61 | ||
|
|
f111e9c4e4 | ||
|
|
0f7b240f4a | ||
|
|
63ee0f6d71 | ||
|
|
e38b00b735 | ||
|
|
97a5de93d8 | ||
|
|
9aabaddf6e | ||
|
|
2fb9d6043a | ||
|
|
d1afc5c6b3 | ||
|
|
2270d93419 | ||
|
|
4e3e6de6c1 | ||
|
|
201877f214 | ||
|
|
0941d3f218 | ||
|
|
7f8876d021 | ||
|
|
62c7e2125a | ||
|
|
13b2d6a18b | ||
|
|
2e854d8f1f | ||
|
|
093f5cbe33 | ||
|
|
b9da86d1ce | ||
|
|
5ac449a178 | ||
|
|
179e60b40b | ||
|
|
5603dc4259 | ||
|
|
b8d703d94f | ||
|
|
8ce49beefa | ||
|
|
2437c4c30c | ||
|
|
0029258fa5 | ||
|
|
8541ec0242 | ||
|
|
0c138d2c07 | ||
|
|
4f70b5d160 | ||
|
|
e0bcafa804 | ||
|
|
330e0b3725 | ||
|
|
46c12af44d | ||
|
|
74db47ba16 | ||
|
|
3bc848cdf4 | ||
|
|
a8d58a733e | ||
|
|
37c6c03ada | ||
|
|
f258b9e0ae | ||
|
|
6ec3936bf1 | ||
|
|
0f0991f956 | ||
|
|
4649744cb4 | ||
|
|
755c1765b4 | ||
|
|
6df9fe6acb | ||
|
|
32180bf1bd | ||
|
|
a373f41ee0 | ||
|
|
bb63e0c3cf | ||
|
|
e84f6d0468 | ||
|
|
b08cf90bdb | ||
|
|
9939056115 | ||
|
|
9ea09aca32 | ||
|
|
65e15ab29b | ||
|
|
1d1e63c40d | ||
|
|
7e854f5bb1 | ||
|
|
fa42eadf43 | ||
|
|
7ee4f85f67 | ||
|
|
afa02fab3f | ||
|
|
c10703316c | ||
|
|
93b09d64b0 | ||
|
|
12663907d2 | ||
|
|
f72717d440 | ||
|
|
c94174a994 | ||
|
|
2789c2bf0f | ||
|
|
0c69226747 | ||
|
|
9dc65861cc | ||
|
|
b14d917f3d | ||
|
|
23fb91b4d2 | ||
|
|
110eea144b | ||
|
|
f413068074 | ||
|
|
917d559ebf | ||
|
|
8d52455574 | ||
|
|
3602fa566c | ||
|
|
a53239df97 | ||
|
|
6154158254 | ||
|
|
f91b25a177 | ||
|
|
650a74dd8c | ||
|
|
d44f57285f | ||
|
|
33b1dcf0f8 | ||
|
|
e914be7ee0 | ||
|
|
b2ea3b0525 | ||
|
|
3c5c0518b4 | ||
|
|
2b1e11d2e0 | ||
|
|
b3eab44f50 | ||
|
|
bec8776c63 | ||
|
|
519e39d54b | ||
|
|
8e4ab441c0 | ||
|
|
6522baccd6 | ||
|
|
be851955b6 | ||
|
|
4083059155 | ||
|
|
9ace6dd570 | ||
|
|
b0677a768d | ||
|
|
dad99fe6bf | ||
|
|
b5c48bd9f7 | ||
|
|
8e73dd7a7c | ||
|
|
905c06771b | ||
|
|
255dbde832 | ||
|
|
eaaaee2b4b | ||
|
|
7227d32c3b | ||
|
|
988419b7e2 | ||
|
|
1d1401b1d6 | ||
|
|
282667c6e3 | ||
|
|
fa13ba1d72 | ||
|
|
a2ad67fbac | ||
|
|
5e42345889 | ||
|
|
684ed7d7e9 | ||
|
|
2ff3c1d3b4 | ||
|
|
e0ea3b2fe0 | ||
|
|
35fe2c3fd3 | ||
|
|
0ec6f99429 | ||
|
|
dbe407e784 | ||
|
|
93f0c714fc | ||
|
|
a1b57654c6 | ||
|
|
624234dc92 | ||
|
|
cdda64575a | ||
|
|
483a31d984 | ||
|
|
6d88e7e831 | ||
|
|
bb97e49982 | ||
|
|
4f8090e5bf | ||
|
|
8c18d6f179 | ||
|
|
7d615b4e65 | ||
|
|
e264a4c887 | ||
|
|
0991495f51 | ||
|
|
94c19d70eb | ||
|
|
805639bbfa | ||
|
|
a900ab939d | ||
|
|
34881df30a | ||
|
|
b35995a925 | ||
|
|
ea9a7170ca | ||
|
|
4e41a8aaac | ||
|
|
b718eb3f4c | ||
|
|
8b03718422 | ||
|
|
e0bb6f3c95 | ||
|
|
4b1649b26a | ||
|
|
20bd6dbdc4 | ||
|
|
2c6088e96c | ||
|
|
db3961443c | ||
|
|
83c3cea88f | ||
|
|
850f680e32 | ||
|
|
dcc0b3cbd8 | ||
|
|
fb372cee89 | ||
|
|
916af395b6 | ||
|
|
27b9ef9216 | ||
|
|
c85463b728 | ||
|
|
9561cc0269 | ||
|
|
e69c1689ec | ||
|
|
18497e92fd | ||
|
|
f692621565 | ||
|
|
934a1089db | ||
|
|
b0dfeb6e5f | ||
|
|
448702b081 | ||
|
|
8ed9117bee | ||
|
|
439bb30bbd | ||
|
|
fc29cd7051 | ||
|
|
51aca98536 | ||
|
|
2ab3f0b442 | ||
|
|
cb9105682e | ||
|
|
84d46e3aee | ||
|
|
9a97dc5221 | ||
|
|
ee810b9e0c | ||
|
|
516a96a4a8 | ||
|
|
506a9c0896 | ||
|
|
71196983ca | ||
|
|
fc600873b4 | ||
|
|
49e588e60e | ||
|
|
dc5beebb24 | ||
|
|
58d4e8b456 | ||
|
|
ca89072273 | ||
|
|
ff5d8baa1a | ||
|
|
9ac09db4c3 | ||
|
|
18e845f99f | ||
|
|
e6307dec97 | ||
|
|
d2941d94bc | ||
|
|
ba132a0546 | ||
|
|
d3bbd836f6 | ||
|
|
8777c2df64 | ||
|
|
e123c94e8a | ||
|
|
7b3af6ac90 | ||
|
|
ce522a75b5 | ||
|
|
24720e02f0 | ||
|
|
f34430348a | ||
|
|
667cdb1520 | ||
|
|
966bcfadab | ||
|
|
3c13f211f2 | ||
|
|
0033d261d3 | ||
|
|
e1ed32af92 | ||
|
|
0d51f4c1d4 | ||
|
|
4c0ffd483e | ||
|
|
3e4751070f | ||
|
|
4b6611ae2b | ||
|
|
1a58636cd4 | ||
|
|
11dc65bdf1 | ||
|
|
387a8f0efe | ||
|
|
0847f9237f | ||
|
|
2f5818ba5a | ||
|
|
d502f983c0 | ||
|
|
af79605268 | ||
|
|
0192da5a92 | ||
|
|
e563a9dc21 | ||
|
|
8f5d969f61 | ||
|
|
3a8fb33dd6 | ||
|
|
0bc2d74dcc | ||
|
|
13da5056be | ||
|
|
ecb2d98ad7 | ||
|
|
e88811c1fd | ||
|
|
56c3ab74cb | ||
|
|
ae280e170a | ||
|
|
d2dfb16033 | ||
|
|
64d99748c7 | ||
|
|
16d90a010b | ||
|
|
32baab9072 | ||
|
|
31dd125263 | ||
|
|
483fd87ee5 | ||
|
|
254786ea5d | ||
|
|
c06a439c0c | ||
|
|
7b76999c0d | ||
|
|
5c0190dffe | ||
|
|
aabd2824d3 | ||
|
|
2f7033cd6d | ||
|
|
756fe823f8 | ||
|
|
ed0d163944 | ||
|
|
b7ee025a6f | ||
|
|
23dca8ec93 | ||
|
|
fe05cb613f | ||
|
|
c56c6fe5e4 | ||
|
|
8f7fafa4f2 | ||
|
|
ef9ca0bfc8 | ||
|
|
34635114df | ||
|
|
3e29dd63df | ||
|
|
c575159616 | ||
|
|
ddebfc7e75 | ||
|
|
2b51e5982a | ||
|
|
00b616f4f8 | ||
|
|
38274e1056 | ||
|
|
cf1397cb81 | ||
|
|
b23c1b46ab | ||
|
|
4750ee9214 | ||
|
|
05b39acb3e | ||
|
|
26d2a59ad5 | ||
|
|
1ed382faef | ||
|
|
ebbbd4febb | ||
|
|
95014c3863 | ||
|
|
b61f5752d2 | ||
|
|
4b7533d721 | ||
|
|
6519e9ae86 | ||
|
|
cbc3b9d292 | ||
|
|
1dde627a4c | ||
|
|
06727f23e4 | ||
|
|
1c8279d2fc | ||
|
|
aa0193b41e | ||
|
|
fcf6a8b586 | ||
|
|
574c51bcec | ||
|
|
01fa8602a0 | ||
|
|
b006e8cc2b | ||
|
|
e8486364a3 | ||
|
|
f72b6b04ce | ||
|
|
1152b4d9b2 | ||
|
|
219c4e2491 | ||
|
|
47d78f4464 | ||
|
|
bebfd03ae9 | ||
|
|
0bf98491cb | ||
|
|
63e4c627b3 | ||
|
|
a37e3c8287 | ||
|
|
45ab560d08 | ||
|
|
3d1846cbe8 | ||
|
|
936bbe2372 | ||
|
|
963a3fbb97 | ||
|
|
b4161da81a | ||
|
|
be65d915e3 | ||
|
|
22fb9df723 | ||
|
|
8709f81b16 | ||
|
|
347ede62b2 | ||
|
|
c4a913d317 | ||
|
|
1706fde7ab | ||
|
|
60e8d37624 | ||
|
|
519bc38fb6 | ||
|
|
375d223f66 | ||
|
|
f16e0649cb | ||
|
|
9e685cde37 | ||
|
|
d32d7cd802 | ||
|
|
8961ca860a | ||
|
|
9a3fa7c82f | ||
|
|
183f36bf40 | ||
|
|
bac91d14c6 | ||
|
|
7455729225 | ||
|
|
223b6b7a0e | ||
|
|
825555a34f | ||
|
|
aeead83510 | ||
|
|
78a8981006 | ||
|
|
e9bb6bc73b | ||
|
|
d2354074f8 | ||
|
|
78ee43cb7d | ||
|
|
952fc914fb | ||
|
|
bc54203cdf | ||
|
|
854846585a | ||
|
|
309bfb623b | ||
|
|
9a99740701 | ||
|
|
1f299a1ff1 | ||
|
|
8bce3d0541 | ||
|
|
06d033d13c | ||
|
|
654b070c7e | ||
|
|
a159c8f072 | ||
|
|
3846d974af | ||
|
|
7e5fcfb881 | ||
|
|
4e9cafcd5e | ||
|
|
15f4fc51ce | ||
|
|
d343c1c98c | ||
|
|
4a17a8474a | ||
|
|
2bb1ff0b09 | ||
|
|
dec5bd6603 | ||
|
|
282f01b55d | ||
|
|
349be00771 | ||
|
|
adc47fd19c | ||
|
|
e876ef97cd | ||
|
|
903409d962 | ||
|
|
9295554195 | ||
|
|
8d6cfe03ac | ||
|
|
70d5051a6f | ||
|
|
124e4ec561 | ||
|
|
036db83321 | ||
|
|
23dfa67fe5 | ||
|
|
79d7a09203 | ||
|
|
96d9fb485b | ||
|
|
68c4d954ef | ||
|
|
ef7bedacb8 | ||
|
|
e7a1373305 | ||
|
|
cfd06df25e | ||
|
|
89bc6d0529 | ||
|
|
0446000270 | ||
|
|
9908e8ca98 | ||
|
|
326f09c903 | ||
|
|
2b52206795 | ||
|
|
4cadcddac1 | ||
|
|
8910b8bf28 | ||
|
|
fc3287758e | ||
|
|
c94a03bb23 | ||
|
|
2c5ba21b99 | ||
|
|
4f00550400 | ||
|
|
dfd622c948 | ||
|
|
a7d66727f3 | ||
|
|
06a5c412bd | ||
|
|
0a3616ec0e | ||
|
|
b4226306b0 | ||
|
|
4f3353303a | ||
|
|
81b832071a | ||
|
|
d1966df73c | ||
|
|
e194291efb | ||
|
|
af88f6cd0d | ||
|
|
62838da761 | ||
|
|
7236608f59 | ||
|
|
2570f2843c | ||
|
|
a9fe9bebaf | ||
|
|
15a7c3abd0 | ||
|
|
7872950f65 | ||
|
|
4a711368e3 | ||
|
|
fbcc7e4478 | ||
|
|
e23af2e5f5 | ||
|
|
9c6fed4d48 | ||
|
|
26ac25d3ba | ||
|
|
19beae66bb | ||
|
|
037f660825 | ||
|
|
47e719bff0 | ||
|
|
020f30d8df | ||
|
|
7078508ba9 | ||
|
|
eb002332ed | ||
|
|
ea1da07e71 | ||
|
|
e1d32cd747 | ||
|
|
1bb3450512 | ||
|
|
ce1a78156e | ||
|
|
9d95e52d12 | ||
|
|
73c072583a | ||
|
|
253e2b7eab | ||
|
|
650c6de692 | ||
|
|
1ac4e20efb | ||
|
|
f08ea4346e | ||
|
|
c206886639 | ||
|
|
61bf953b1a | ||
|
|
1dcd35e825 | ||
|
|
5a7bb8b103 | ||
|
|
c4be4f068f | ||
|
|
4534c334bc | ||
|
|
9fa6d6d8b1 | ||
|
|
a5b34161c1 | ||
|
|
bf2c6929e1 | ||
|
|
2a451c3120 | ||
|
|
1169714908 | ||
|
|
14de4e4760 | ||
|
|
712f527ce0 | ||
|
|
0631c64ba5 | ||
|
|
85c43db53e | ||
|
|
8394bf3f19 | ||
|
|
bd1f25f016 | ||
|
|
95f340063a | ||
|
|
2be1d82e8d | ||
|
|
501af18298 | ||
|
|
724292bd34 | ||
|
|
cbbdebdf84 | ||
|
|
02c17dcf55 | ||
|
|
23d1f9d8c0 | ||
|
|
f4e0d3596d | ||
|
|
3b012bc946 | ||
|
|
33a5a2c80f | ||
|
|
e8b481d517 | ||
|
|
dcfa58b3a9 | ||
|
|
fd4106cf00 | ||
|
|
87dddac5f4 | ||
|
|
5488af7e35 | ||
|
|
0a3bd56f15 | ||
|
|
a5ae8f994b | ||
|
|
6a0b3e7fc4 | ||
|
|
a7620c38d0 | ||
|
|
0060e316dc | ||
|
|
b71321f301 | ||
|
|
c99ef80d78 | ||
|
|
2adf3fe27b | ||
|
|
ade033eb59 | ||
|
|
42666cf1e9 | ||
|
|
530f11f67c | ||
|
|
0391db60aa | ||
|
|
486c90a112 | ||
|
|
d1767797d7 | ||
|
|
fbe03d23f3 | ||
|
|
361280c131 | ||
|
|
04e0fc6e7c | ||
|
|
ab52eee127 | ||
|
|
94825252f7 | ||
|
|
93f13817be | ||
|
|
739ea4e841 | ||
|
|
fe3ad9ffb4 | ||
|
|
8fce809ee9 | ||
|
|
c156cbff99 | ||
|
|
268be8e0f5 | ||
|
|
5581e1c0e1 | ||
|
|
7fea2d442f | ||
|
|
74276764a6 | ||
|
|
a3e54782bb | ||
|
|
b7bc80b2a3 | ||
|
|
b869a41f3d | ||
|
|
9c7954945f | ||
|
|
13cd666718 | ||
|
|
c3e627e85b | ||
|
|
f23c24ae9b | ||
|
|
d27da35beb | ||
|
|
6457b205e4 | ||
|
|
bea7b61dcc | ||
|
|
2cc8d51a6c | ||
|
|
5410b806bb | ||
|
|
b937d8bd71 | ||
|
|
cd25cfab8e | ||
|
|
229e6ad461 | ||
|
|
977cae1cbd | ||
|
|
c8a9be2ca6 | ||
|
|
c3acf82a9b | ||
|
|
ddfc60bbf5 | ||
|
|
445646fe02 | ||
|
|
3dd3c8fb40 | ||
|
|
fb390b3618 | ||
|
|
ca5fb75f3a | ||
|
|
e881ce5f0f | ||
|
|
8002e47551 | ||
|
|
5b66b5705d | ||
|
|
d1c5521d2a | ||
|
|
74151edfb3 | ||
|
|
00f6747d7d | ||
|
|
0101955ad3 | ||
|
|
f6f9a95f06 | ||
|
|
3d82b89db0 | ||
|
|
8c7b549a45 | ||
|
|
3ad4dc1cfe | ||
|
|
7524314f74 | ||
|
|
94545e8958 | ||
|
|
2c74b2d2e2 | ||
|
|
108c190254 | ||
|
|
466209307e | ||
|
|
acccba59dc | ||
|
|
9325e2f9d1 | ||
|
|
36ebff2667 | ||
|
|
6d0d08b5fb | ||
|
|
e695a1e291 | ||
|
|
133488221b | ||
|
|
b186b672ea | ||
|
|
2badef3daf | ||
|
|
f8700296fb | ||
|
|
0f79fb56c7 | ||
|
|
d8412c95d4 | ||
|
|
469c239eed | ||
|
|
7fad542553 | ||
|
|
d0c0aeab84 | ||
|
|
9fd7123649 | ||
|
|
5b922043ec | ||
|
|
2953589ece | ||
|
|
5836990903 | ||
|
|
acd7e24382 | ||
|
|
71827e0546 | ||
|
|
7e8139e5a5 | ||
|
|
20d2b6ec9e | ||
|
|
be7d0e58a7 | ||
|
|
f20c449279 | ||
|
|
bf059715ec | ||
|
|
98cd3f22a2 | ||
|
|
bad290d104 | ||
|
|
3c55d025ce | ||
|
|
5c775ac5b4 | ||
|
|
9295aa58a7 | ||
|
|
96d68bbd39 | ||
|
|
7ddb6bc1ca | ||
|
|
a0145793a2 | ||
|
|
ecb37d67cc | ||
|
|
969476f368 | ||
|
|
3ae203d7ad | ||
|
|
e979b5aebe | ||
|
|
d568bccc28 | ||
|
|
301429182d | ||
|
|
8df78b9387 | ||
|
|
ba57309bcd | ||
|
|
8d573b3ee6 | ||
|
|
ba43ba8c21 | ||
|
|
47a3c24b03 | ||
|
|
40579fd376 | ||
|
|
bb17c1cc1a | ||
|
|
1cc8862a04 | ||
|
|
ff4606caa4 | ||
|
|
aff12a0462 | ||
|
|
3c5054acbd | ||
|
|
9a854f7810 | ||
|
|
1e731f7cbe | ||
|
|
c0cd6234f3 | ||
|
|
9cc79ab33a | ||
|
|
181de73a13 | ||
|
|
57e03c39f1 | ||
|
|
ff8a89d688 | ||
|
|
36f6fa7feb | ||
|
|
7d7e9cc79d | ||
|
|
2616ebe229 | ||
|
|
2b568c6260 | ||
|
|
f49f539e71 | ||
|
|
1845f3a5ae | ||
|
|
9aa337cd47 | ||
|
|
9b12c5c4bf | ||
|
|
1d12f7e475 | ||
|
|
e463ab9aae | ||
|
|
baa9de9059 | ||
|
|
e3b706f537 | ||
|
|
438bd2c195 | ||
|
|
5b546911ff | ||
|
|
b544e325ce | ||
|
|
c01fad2e29 | ||
|
|
a4d2f53207 | ||
|
|
a8b3fc3129 | ||
|
|
b7dec6d223 | ||
|
|
2d76ea554d | ||
|
|
b0f03dbe0a | ||
|
|
46429e04b4 | ||
|
|
7d479b7d88 | ||
|
|
c86651cdf6 | ||
|
|
63bfdba992 | ||
|
|
6767c42b14 | ||
|
|
70f2f2ecb5 | ||
|
|
691ec420b0 | ||
|
|
278f130906 | ||
|
|
531a88e326 | ||
|
|
d8e3e193b8 | ||
|
|
cda99d6f21 | ||
|
|
3cf3ae9135 | ||
|
|
7333361190 | ||
|
|
f75ebbb47f | ||
|
|
ce35fad608 | ||
|
|
8160d711a1 | ||
|
|
2a595ced57 | ||
|
|
9cb14b91ba | ||
|
|
92e008b3c8 | ||
|
|
f6d8924e18 | ||
|
|
ef955a03bc | ||
|
|
67c12f7342 | ||
|
|
5cfbc6855b | ||
|
|
aa9e9e20fe | ||
|
|
417f9c370d | ||
|
|
3f3bdbb83e | ||
|
|
c1c58a7a4d | ||
|
|
a6a3dd4c28 | ||
|
|
55c3dbe3b6 | ||
|
|
50e0c5aab9 | ||
|
|
fde0566abf | ||
|
|
be83fcbddb | ||
|
|
a7ccc1997a | ||
|
|
f6339a9f70 | ||
|
|
6d15d61f68 | ||
|
|
7b0b81694c | ||
|
|
e9ab643aab | ||
|
|
2e1ef17861 | ||
|
|
2527a3d303 | ||
|
|
62973d0564 | ||
|
|
59c428a14b | ||
|
|
064a4de214 | ||
|
|
998d95f6b2 | ||
|
|
e609d9ea93 | ||
|
|
0d191e6f02 | ||
|
|
3cb3aa4d1c | ||
|
|
f580e98d26 | ||
|
|
e1ad8aab73 | ||
|
|
340cdd9323 | ||
|
|
82ba5debcd | ||
|
|
d9a677b4ca | ||
|
|
cd56463286 | ||
|
|
3faa726fcd | ||
|
|
ef938df79f | ||
|
|
470ca0a98e | ||
|
|
fe9bb7e26a | ||
|
|
a30b1f5298 | ||
|
|
e7196d3033 | ||
|
|
09977ac703 | ||
|
|
ea9ed85bf7 | ||
|
|
1f3d819b24 | ||
|
|
096a025b79 | ||
|
|
0926f0b484 | ||
|
|
b6f6641204 | ||
|
|
65418c6e9a | ||
|
|
6abd0e677b | ||
|
|
ecfae5f416 | ||
|
|
e2f4d4e376 | ||
|
|
fb8de4606a | ||
|
|
09af8f98b3 | ||
|
|
70abcee27d | ||
|
|
d0deb6bee5 | ||
|
|
3494349961 | ||
|
|
5abc9ac9c0 | ||
|
|
d0360ea87b | ||
|
|
d31c5c4d53 | ||
|
|
f2f49464fc | ||
|
|
f6d0e068f6 | ||
|
|
17f699234c | ||
|
|
bf4c07aba4 | ||
|
|
a7fadf55aa | ||
|
|
4912807ae4 | ||
|
|
4a61c34d58 | ||
|
|
109dc901e9 | ||
|
|
9b8b6643f8 | ||
|
|
a26e79dac7 | ||
|
|
291c5d68f2 | ||
|
|
c85e838e33 | ||
|
|
94975e0117 | ||
|
|
eebb9359a6 | ||
|
|
40e6609297 | ||
|
|
af028504eb | ||
|
|
bfe4564659 | ||
|
|
85cdcaa457 | ||
|
|
cd5c10835c | ||
|
|
ce97c92645 | ||
|
|
d27b3b9ba0 | ||
|
|
0f96923006 | ||
|
|
bc7ac73de9 | ||
|
|
8d2fe9dd25 | ||
|
|
2d99106254 | ||
|
|
04d01efd0f | ||
|
|
da10fff966 | ||
|
|
991ee8674a | ||
|
|
7f24950498 | ||
|
|
405fb415d4 | ||
|
|
0aa4c27e02 | ||
|
|
add12366a0 | ||
|
|
2c496f6fe2 | ||
|
|
1381c19723 | ||
|
|
a8ddfc8b70 | ||
|
|
6680ff1cd9 | ||
|
|
863dc6378c | ||
|
|
be0465d094 | ||
|
|
42d2dcef7e | ||
|
|
c70fd8608d | ||
|
|
e217f929e8 | ||
|
|
b95c51a2be | ||
|
|
e3b635d107 | ||
|
|
ae1642d052 | ||
|
|
13e9f49a8b | ||
|
|
13f174fb1e | ||
|
|
3ef9e6304d | ||
|
|
398909b809 | ||
|
|
dc140a3dd5 | ||
|
|
b2fe23c32e | ||
|
|
46f0761f50 | ||
|
|
63258f2029 | ||
|
|
eeaf8fbe19 | ||
|
|
934e6dfa57 | ||
|
|
a9dd04f4fa | ||
|
|
34df54b96c | ||
|
|
e5ebd3c925 | ||
|
|
e99660ce40 | ||
|
|
d5357ed1c3 | ||
|
|
5a357c43e0 | ||
|
|
287ef5bdc7 | ||
|
|
89db56ae58 | ||
|
|
8f8aa888ca | ||
|
|
f9321a7bde | ||
|
|
4c59d2e2cb | ||
|
|
26d346bdf1 | ||
|
|
ba741ada31 | ||
|
|
2e8c4ebf9a | ||
|
|
579e30683a | ||
|
|
b1f893e944 | ||
|
|
415e305415 | ||
|
|
adb6928772 | ||
|
|
dc70bd1513 | ||
|
|
57f6a7d1a5 | ||
|
|
deafbd45d0 | ||
|
|
b2fa338e03 | ||
|
|
4e6a98e789 | ||
|
|
72ca19e3e7 | ||
|
|
2c7dac1837 | ||
|
|
de22d58e75 | ||
|
|
2f6d5415cc | ||
|
|
b8101ffa76 | ||
|
|
281590cf63 | ||
|
|
6759fb9ec0 | ||
|
|
c7dad4f1ad | ||
|
|
d29726632b | ||
|
|
95e5c58a92 | ||
|
|
8c5a3693c8 | ||
|
|
7aa1061b06 | ||
|
|
46bd172d59 | ||
|
|
d4dbaf5c57 | ||
|
|
6b4d47c79d | ||
|
|
1bd32ade9f | ||
|
|
5e1f3abd14 | ||
|
|
5bf7ecab64 | ||
|
|
dd75df0af8 | ||
|
|
72de08f9a3 | ||
|
|
13213edb4f | ||
|
|
872b618ea1 | ||
|
|
78e7fe76c6 | ||
|
|
980245bbfc | ||
|
|
2fd98a0be0 | ||
|
|
700f5debe5 | ||
|
|
06b4604e59 | ||
|
|
a6fd6b71d6 | ||
|
|
da194caf7c | ||
|
|
fa45e1040f | ||
|
|
09f0357763 | ||
|
|
f82e106fc1 | ||
|
|
906431b3a6 | ||
|
|
e82a76492a | ||
|
|
8c75c01017 | ||
|
|
dfabd2b414 | ||
|
|
8199dea809 | ||
|
|
1cb20088b2 | ||
|
|
203a9e5ca5 | ||
|
|
5a70586756 | ||
|
|
478beca96d | ||
|
|
637b57158a | ||
|
|
a2009fa91f | ||
|
|
4ee5fa3b00 | ||
|
|
36dde79dac | ||
|
|
9071d8a000 | ||
|
|
1c815e9d47 | ||
|
|
9b16779293 | ||
|
|
c702477dc8 | ||
|
|
b54df1d299 | ||
|
|
548a254262 | ||
|
|
d7330ad654 | ||
|
|
cfe02d3489 | ||
|
|
973bc4309d | ||
|
|
2112ed111f | ||
|
|
95d714ea0c | ||
|
|
cad60e3343 | ||
|
|
c1f0640eda | ||
|
|
d511f0ea95 | ||
|
|
96ad01f78c | ||
|
|
bdb5ed9ec1 | ||
|
|
49b054330c | ||
|
|
40feaa010d | ||
|
|
fbae0a48dc | ||
|
|
84a0f93cc1 | ||
|
|
642a89548c | ||
|
|
6ac19bd6b5 | ||
|
|
5eaf54ccf1 | ||
|
|
78f64180c7 | ||
|
|
d921c426e4 | ||
|
|
efb09e7a81 | ||
|
|
cb8939849b | ||
|
|
60e990a6c4 | ||
|
|
7c258dc4a4 | ||
|
|
bae7abb765 | ||
|
|
9b5eee64d8 | ||
|
|
edfdc0ae6c | ||
|
|
ff7bc5dbec | ||
|
|
cd918f3664 | ||
|
|
88ba9563ad | ||
|
|
b12a3d39a7 | ||
|
|
0bc0885439 | ||
|
|
9b38e93cf4 | ||
|
|
e87687f175 | ||
|
|
1e681de8a3 | ||
|
|
27bf0667fa | ||
|
|
732cfce4a0 | ||
|
|
516f301822 | ||
|
|
21a1a7b765 | ||
|
|
f1e57967d3 | ||
|
|
c6bf70b3e1 | ||
|
|
f2e9f5b28a | ||
|
|
02737c8b41 | ||
|
|
2455298bb1 | ||
|
|
779afb5b17 | ||
|
|
969843dde4 | ||
|
|
f371a5337d | ||
|
|
ef9e97a588 | ||
|
|
41de930b49 | ||
|
|
4e1adee102 | ||
|
|
c66f623173 | ||
|
|
625f62b057 | ||
|
|
a996cb32b9 | ||
|
|
d1fd8f6a70 | ||
|
|
4a33008c61 | ||
|
|
9d808b28a4 | ||
|
|
f69aee817c | ||
|
|
b07a75df90 | ||
|
|
1e2af212ca | ||
|
|
c36afc3173 | ||
|
|
1d5d29bf1d | ||
|
|
deb5eab79e | ||
|
|
e767e964ab | ||
|
|
eb540dc579 | ||
|
|
01cd02ef94 | ||
|
|
77b2ec46d1 | ||
|
|
cf6b1953e0 | ||
|
|
533fba4c6e | ||
|
|
60068dea5b | ||
|
|
1b597c16dd | ||
|
|
5d4b2a1fe1 | ||
|
|
c39f80bdeb | ||
|
|
a220efa9a4 | ||
|
|
23c803add1 | ||
|
|
e38d8f24b6 |
491
.github/workflows/main.yml
vendored
491
.github/workflows/main.yml
vendored
@@ -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
2
.gitmodules
vendored
@@ -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
16
.vscode/launch.json
vendored
Normal 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/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
22
README.md
22
README.md
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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))")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
96
docker/linux_gui_vnc/Dockerfile
Normal file
96
docker/linux_gui_vnc/Dockerfile
Normal 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
2
docker/linux_gui_vnc/build.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker build -t qdomyos-zwift-vnc .
|
||||
10
docker/linux_gui_vnc/docker-compose.yml
Normal file
10
docker/linux_gui_vnc/docker-compose.yml
Normal 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
|
||||
95
docker/linux_webgl/Dockerfile
Normal file
95
docker/linux_webgl/Dockerfile
Normal 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
2
docker/linux_webgl/build.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker build -t qdomyos-zwift-webgl .
|
||||
19
docker/linux_webgl/docker-compose.yml
Normal file
19
docker/linux_webgl/docker-compose.yml
Normal 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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
188
helpers/dircon-parser.py
Normal 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
331
helpers/wahoo-simulator.py
Normal 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)
|
||||
37
qdomyos-zwift.code-workspace
Normal file
37
qdomyos-zwift.code-workspace
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
src/Home.qml
24
src/Home.qml
@@ -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?"
|
||||
|
||||
20
src/IndicatorOnlySwitch.qml
Normal file
20
src/IndicatorOnlySwitch.qml
Normal 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
26
src/OAuth2.h
Normal 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
|
||||
@@ -22,7 +22,7 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
AccordionElement {
|
||||
StaticAccordionElement {
|
||||
title: qsTr("Settings folder")
|
||||
indicatRectColor: Material.color(Material.Grey)
|
||||
textColor: Material.color(Material.Grey)
|
||||
|
||||
68
src/StaticAccordionElement.qml
Normal file
68
src/StaticAccordionElement.qml
Normal 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
80
src/WebPelotonAuth.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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:+'
|
||||
|
||||
|
||||
BIN
src/android/libs/android-antplus-plugin-lib-release_3.9.0.aar
Normal file
BIN
src/android/libs/android-antplus-plugin-lib-release_3.9.0.aar
Normal file
Binary file not shown.
BIN
src/android/libs/usb-serial-for-android-3.8.1.aar
Normal file
BIN
src/android/libs/usb-serial-for-android-3.8.1.aar
Normal file
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
239
src/android/src/BikeChannelController.java
Normal file
239
src/android/src/BikeChannelController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
651
src/android/src/BikeTransmitterController.java
Normal file
651
src/android/src/BikeTransmitterController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
121
src/android/src/QLog.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
74
src/android/src/ZwiftHubBike.java
Normal file
74
src/android/src/ZwiftHubBike.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
162
src/android/src/main/proto/zwift_hub.proto
Normal file
162
src/android/src/main/proto/zwift_hub.proto
Normal 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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
42
src/androidqlog.cpp
Normal 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
1
src/build-qrc-qml.sh
Executable 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
|
||||
23
src/characteristics/characteristicnotifier0002.cpp
Normal file
23
src/characteristics/characteristicnotifier0002.cpp
Normal 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;
|
||||
}
|
||||
19
src/characteristics/characteristicnotifier0002.h
Normal file
19
src/characteristics/characteristicnotifier0002.h
Normal 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
|
||||
23
src/characteristics/characteristicnotifier0004.cpp
Normal file
23
src/characteristics/characteristicnotifier0004.cpp
Normal 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;
|
||||
}
|
||||
19
src/characteristics/characteristicnotifier0004.h
Normal file
19
src/characteristics/characteristicnotifier0004.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
316
src/characteristics/characteristicwriteprocessor0003.cpp
Normal file
316
src/characteristics/characteristicwriteprocessor0003.cpp
Normal 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;
|
||||
}
|
||||
45
src/characteristics/characteristicwriteprocessor0003.h
Normal file
45
src/characteristics/characteristicwriteprocessor0003.h
Normal 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
|
||||
187
src/devices/android_antbike/android_antbike.cpp
Normal file
187
src/devices/android_antbike/android_antbike.cpp
Normal 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; }
|
||||
81
src/devices/android_antbike/android_antbike.h
Normal file
81
src/devices/android_antbike/android_antbike.h
Normal 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
|
||||
@@ -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(' ') +
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
456
src/devices/coresensor/coresensor.cpp
Normal file
456
src/devices/coresensor/coresensor.cpp
Normal 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
|
||||
}
|
||||
196
src/devices/coresensor/coresensor.h
Normal file
196
src/devices/coresensor/coresensor.h
Normal 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
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
128
src/devices/csafe/csaferunner.cpp
Normal file
128
src/devices/csafe/csaferunner.cpp
Normal 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();
|
||||
}
|
||||
44
src/devices/csafe/csaferunner.h
Normal file
44
src/devices/csafe/csaferunner.h
Normal 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;
|
||||
};
|
||||
101
src/devices/csafe/csafeutility.cpp
Normal file
101
src/devices/csafe/csafeutility.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
44
src/devices/csafe/csafeutility.h
Normal file
44
src/devices/csafe/csafeutility.h
Normal 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
|
||||
28
src/devices/csafe/kalmanfilter.cpp
Normal file
28
src/devices/csafe/kalmanfilter.cpp
Normal 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; }
|
||||
52
src/devices/csafe/kalmanfilter.h
Normal file
52
src/devices/csafe/kalmanfilter.h
Normal 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
|
||||
118
src/devices/csafe/netserial.cpp
Normal file
118
src/devices/csafe/netserial.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/devices/csafe/netserial.h
Normal file
61
src/devices/csafe/netserial.h
Normal 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
Reference in New Issue
Block a user