mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
655 Commits
build-1225
...
Mobi-Rower
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba867a1b5 | ||
|
|
72f57053a7 | ||
|
|
13ea5313b1 | ||
|
|
7f694733b2 | ||
|
|
b1755c004a | ||
|
|
360ab66431 | ||
|
|
04b659a91f | ||
|
|
9487fa3cb4 | ||
|
|
1ac2149424 | ||
|
|
28558697b2 | ||
|
|
6918fb9eba | ||
|
|
365abbb7cb | ||
|
|
d3f52682cc | ||
|
|
da4f360f63 | ||
|
|
b1c6cf70f5 | ||
|
|
50e18b1db4 | ||
|
|
47ea3c2176 | ||
|
|
c83c272ed4 | ||
|
|
de3ea61ecf | ||
|
|
2d53ebf190 | ||
|
|
93feea6c16 | ||
|
|
baaf689b4c | ||
|
|
4dea13d78e | ||
|
|
914b02f8e0 | ||
|
|
0e01889ed3 | ||
|
|
19ef4bb230 | ||
|
|
51c8d060de | ||
|
|
d1afe0ebb2 | ||
|
|
e5532ca04e | ||
|
|
bf37d681de | ||
|
|
03bf9d8fd1 | ||
|
|
161362f11f | ||
|
|
2bb0e212db | ||
|
|
67344ea130 | ||
|
|
727fb99572 | ||
|
|
89ec6eef1f | ||
|
|
a7ca3f329a | ||
|
|
695c05c284 | ||
|
|
b38712851d | ||
|
|
73241765d1 | ||
|
|
ad79beb44b | ||
|
|
39288f1343 | ||
|
|
ff093a126e | ||
|
|
489ef0a665 | ||
|
|
73771f42c2 | ||
|
|
dbdd58c398 | ||
|
|
5c0aa59bed | ||
|
|
7ec3b601b4 | ||
|
|
9d2bf0821c | ||
|
|
5d85cdd8e5 | ||
|
|
56bbc2439c | ||
|
|
31e1bb8a9f | ||
|
|
88a8b138ca | ||
|
|
10cfac1e40 | ||
|
|
79952ad73c | ||
|
|
3a3755d18c | ||
|
|
9a45b28f8c | ||
|
|
9b70d6c144 | ||
|
|
137036efd7 | ||
|
|
e5e2851d45 | ||
|
|
cadfaa8be1 | ||
|
|
6df4acf35c | ||
|
|
2ccbbfb07b | ||
|
|
7d4775c0ea | ||
|
|
9baa9a0f23 | ||
|
|
f3fc0a5212 | ||
|
|
eb29ec5dfd | ||
|
|
d29b312554 | ||
|
|
56f3916387 | ||
|
|
bbb2d6fe90 | ||
|
|
34be7d451f | ||
|
|
83bbb56ad8 | ||
|
|
87846d9dd2 | ||
|
|
94a70a56db | ||
|
|
563c7a1445 | ||
|
|
6e23d0c743 | ||
|
|
60d5880081 | ||
|
|
fb56c58046 | ||
|
|
27759c14ee | ||
|
|
19bee8ee9b | ||
|
|
d25ecb176c | ||
|
|
da92d8711f | ||
|
|
e6cbc43e3b | ||
|
|
ad628b58d6 | ||
|
|
7ddbc984dc | ||
|
|
bb16ecc80d | ||
|
|
77d18da7a1 | ||
|
|
5c008dba20 | ||
|
|
1fae044590 | ||
|
|
fc09ea3de2 | ||
|
|
332b3efe59 | ||
|
|
aa163d9ec0 | ||
|
|
e037faf020 | ||
|
|
b84543d97f | ||
|
|
979114b115 | ||
|
|
35abc2cd1f | ||
|
|
c98411ac88 | ||
|
|
af94abae37 | ||
|
|
10ce431568 | ||
|
|
681eaffb05 | ||
|
|
d087453b2c | ||
|
|
b88a6d8cff | ||
|
|
e6e4953bfd | ||
|
|
39df00f460 | ||
|
|
2fd87e86ac | ||
|
|
eb4b73b5f8 | ||
|
|
e94002fc11 | ||
|
|
ca6ff11275 | ||
|
|
3d88324e93 | ||
|
|
27c77d6619 | ||
|
|
25b00c6730 | ||
|
|
4c01124dee | ||
|
|
b2d7821f68 | ||
|
|
e4875c7767 | ||
|
|
f36c191442 | ||
|
|
cbb3b20fcd | ||
|
|
7fc6245ee5 | ||
|
|
1a66877679 | ||
|
|
d3eb58e862 | ||
|
|
6bf403907d | ||
|
|
d26f2b6d25 | ||
|
|
61145e973c | ||
|
|
336f94a812 | ||
|
|
58f96ac932 | ||
|
|
5b5586eb29 | ||
|
|
e18b207de5 | ||
|
|
f1c9fa5b73 | ||
|
|
d3b05e03e6 | ||
|
|
32fac1b5fa | ||
|
|
27b7f969fc | ||
|
|
b736ff2162 | ||
|
|
33533f930b | ||
|
|
a27b95d623 | ||
|
|
7c3d24f37f | ||
|
|
372eaaf7e8 | ||
|
|
220a2dd7ea | ||
|
|
cdb45fe011 | ||
|
|
a32ed12a3b | ||
|
|
ab76e3b007 | ||
|
|
08566ae75c | ||
|
|
b9c6f53a9d | ||
|
|
99c2222118 | ||
|
|
4b3d7310a6 | ||
|
|
8389c9e4bb | ||
|
|
2a2307f400 | ||
|
|
16d5a4067c | ||
|
|
d98ca719d0 | ||
|
|
2e2dc59f2a | ||
|
|
31bfb06416 | ||
|
|
268b948a7f | ||
|
|
64a4831091 | ||
|
|
d66c3deeca | ||
|
|
6d5f38d0c6 | ||
|
|
41bb13df4b | ||
|
|
972c07cdcd | ||
|
|
40aedaec71 | ||
|
|
8ce5ec8468 | ||
|
|
8477731f89 | ||
|
|
9f2a58c81f | ||
|
|
b0e991b472 | ||
|
|
ce14d95af1 | ||
|
|
38e76d88a5 | ||
|
|
5321105136 | ||
|
|
19f2d17d83 | ||
|
|
0f58538e80 | ||
|
|
70ee7cfa44 | ||
|
|
e29eba3a71 | ||
|
|
76e836f69c | ||
|
|
eccd85b84b | ||
|
|
5bc187a748 | ||
|
|
f6ff45b449 | ||
|
|
967fe63652 | ||
|
|
50adea9d5b | ||
|
|
8c9f680a90 | ||
|
|
c4de251dc7 | ||
|
|
2048debf3a | ||
|
|
924635c047 | ||
|
|
d68bddcf57 | ||
|
|
7059e680b3 | ||
|
|
64272d508a | ||
|
|
eb9cc1b34c | ||
|
|
a298af10f0 | ||
|
|
ca140a20b4 | ||
|
|
5623df2869 | ||
|
|
6c91436abb | ||
|
|
6b5b1b5c0e | ||
|
|
d2a883e380 | ||
|
|
3fa9939fa1 | ||
|
|
374ea0ffc2 | ||
|
|
4f32f9b520 | ||
|
|
f5769fd7bc | ||
|
|
01a09a0e36 | ||
|
|
a70317453b | ||
|
|
7bc91094ba | ||
|
|
ef152b1edd | ||
|
|
ca7ea7e7d5 | ||
|
|
3adbb96a4e | ||
|
|
eb26c19964 | ||
|
|
27a7cf1471 | ||
|
|
7c865da169 | ||
|
|
2cbe92e525 | ||
|
|
2deb37ae83 | ||
|
|
a8f4cc899e | ||
|
|
44cab34f38 | ||
|
|
9dab1cb357 | ||
|
|
98e58f5f17 | ||
|
|
5c8835cd38 | ||
|
|
3b3dd9dbe7 | ||
|
|
a449fdd09d | ||
|
|
6d226dd592 | ||
|
|
7f7aac4cd5 | ||
|
|
dc3f3f5d21 | ||
|
|
6ec3c71ac4 | ||
|
|
f9f940b0a5 | ||
|
|
8cef05fb2d | ||
|
|
b45ca3e596 | ||
|
|
8eca1d6fd6 | ||
|
|
b1bce39c4a | ||
|
|
4ea5152a63 | ||
|
|
4826f75788 | ||
|
|
dc3f5baf23 | ||
|
|
9fb19bb5e3 | ||
|
|
94189368d0 | ||
|
|
3c10869db2 | ||
|
|
ee46a9b9be | ||
|
|
2ffa08848e | ||
|
|
4151c500be | ||
|
|
326bba8106 | ||
|
|
bf512f3841 | ||
|
|
a1dd201bee | ||
|
|
d522dcb61b | ||
|
|
044a06f3cf | ||
|
|
c90093046c | ||
|
|
ddc01d1ae0 | ||
|
|
6f54194e43 | ||
|
|
b4478812dc | ||
|
|
3a4d01f886 | ||
|
|
3e50bf1f92 | ||
|
|
b08fb0687c | ||
|
|
477804da82 | ||
|
|
e4d536ea2d | ||
|
|
464b126db8 | ||
|
|
c8ca7af1dc | ||
|
|
7919319955 | ||
|
|
a6fd4cf4cb | ||
|
|
2900c5f4fa | ||
|
|
fd611c1bea | ||
|
|
7f1a702021 | ||
|
|
0cc33e0c2b | ||
|
|
b8fc355ea7 | ||
|
|
4922019a32 | ||
|
|
2eefcab2c8 | ||
|
|
3da7906a8b | ||
|
|
83c6b2ceb9 | ||
|
|
42c139b881 | ||
|
|
377c6df085 | ||
|
|
c462124128 | ||
|
|
9f85ea84aa | ||
|
|
b9d65081d5 | ||
|
|
fdb359a89d | ||
|
|
6f166d2760 | ||
|
|
2e077e9268 | ||
|
|
28c7de4608 | ||
|
|
19b204ff2d | ||
|
|
76d6ebceeb | ||
|
|
a945fa6314 | ||
|
|
6eed563655 | ||
|
|
f0ac2da4f9 | ||
|
|
6863ebcbfe | ||
|
|
9a4c368492 | ||
|
|
4af83bd51b | ||
|
|
a8136f2cbc | ||
|
|
49fbf8acec | ||
|
|
9fa6eb2a48 | ||
|
|
be12057c51 | ||
|
|
390471abb7 | ||
|
|
f0188aa9b1 | ||
|
|
9b784d935b | ||
|
|
cd07e46ff0 | ||
|
|
0393488c69 | ||
|
|
29613b97fa | ||
|
|
c3ff3c2e06 | ||
|
|
60e23c731b | ||
|
|
7d33d87f04 | ||
|
|
b15055e914 | ||
|
|
5ddb5f08cd | ||
|
|
13161cd894 | ||
|
|
3089dc8a1c | ||
|
|
f38652f7b2 | ||
|
|
fdbc6e94e1 | ||
|
|
025815fe99 | ||
|
|
e2a93cde72 | ||
|
|
a44002c924 | ||
|
|
3bcd4d0ee4 | ||
|
|
e15e8ebf9e | ||
|
|
fba48cb7da | ||
|
|
daacf806bf | ||
|
|
4c21b01903 | ||
|
|
59228197ac | ||
|
|
f7b514c623 | ||
|
|
088208ff57 | ||
|
|
a5a4b93407 | ||
|
|
47696c24ad | ||
|
|
ba9da36087 | ||
|
|
8fcc9b6725 | ||
|
|
d065dd5bd1 | ||
|
|
6f42a0d2cc | ||
|
|
14e2e16595 | ||
|
|
025a757c35 | ||
|
|
292a5600c9 | ||
|
|
468bc8f87b | ||
|
|
b0e011fd34 | ||
|
|
2ef0a3c5a7 | ||
|
|
019b3c8abb | ||
|
|
317116f2d5 | ||
|
|
fe005a2f00 | ||
|
|
08c1e26d3b | ||
|
|
e98820601a | ||
|
|
c499092460 | ||
|
|
e3d50bda7c | ||
|
|
c060e8b24a | ||
|
|
f15f841860 | ||
|
|
15010b27dd | ||
|
|
88c6091e21 | ||
|
|
4a6df1c020 | ||
|
|
3d24e7c1a0 | ||
|
|
54a8b2619a | ||
|
|
038c4a6165 | ||
|
|
0831a4ed20 | ||
|
|
74fc5f660c | ||
|
|
ae5dd54738 | ||
|
|
c0299b16ac | ||
|
|
6401a66f4c | ||
|
|
ba064c2acd | ||
|
|
9375f15207 | ||
|
|
24183a4968 | ||
|
|
9e1537caad | ||
|
|
9fe72d13c0 | ||
|
|
df5e80a5be | ||
|
|
3b751d44e6 | ||
|
|
3815e45107 | ||
|
|
580eb3f092 | ||
|
|
aba59cd136 | ||
|
|
369fbc4bc0 | ||
|
|
871e704852 | ||
|
|
b574e86804 | ||
|
|
91735a714b | ||
|
|
da3b5b168e | ||
|
|
d339cd461d | ||
|
|
f5ac438905 | ||
|
|
073b331535 | ||
|
|
184c99ff6c | ||
|
|
b25f7acf20 | ||
|
|
cb0df3ae27 | ||
|
|
7f9ffb7e0e | ||
|
|
dd104da06d | ||
|
|
9440089d05 | ||
|
|
39b6ad7463 | ||
|
|
fdc548fda7 | ||
|
|
cc85cbba4f | ||
|
|
7576a77cd8 | ||
|
|
61c633474a | ||
|
|
69aefc0b30 | ||
|
|
19ca844968 | ||
|
|
5bb3a808a1 | ||
|
|
63cedd457d | ||
|
|
d9925ac780 | ||
|
|
e4a71e2940 | ||
|
|
d66d2fd915 | ||
|
|
d20f651672 | ||
|
|
19a564d832 | ||
|
|
5e100f8857 | ||
|
|
5491007a2a | ||
|
|
028b5b4c4a | ||
|
|
8eb0083897 | ||
|
|
89ed87ecb1 | ||
|
|
eac18d7a51 | ||
|
|
d9a50973cd | ||
|
|
39c33f3ebc | ||
|
|
2858468a04 | ||
|
|
1bbbda4efb | ||
|
|
49fc672538 | ||
|
|
d93107e286 | ||
|
|
08af7d61a5 | ||
|
|
e25dfc2354 | ||
|
|
a4a4d1b9c5 | ||
|
|
5761865916 | ||
|
|
5be7b8530e | ||
|
|
fe44490ad9 | ||
|
|
97138d8492 | ||
|
|
76845d5507 | ||
|
|
28bc5670c4 | ||
|
|
e0b84cb4a3 | ||
|
|
4cdf23d544 | ||
|
|
7beb1aed6f | ||
|
|
91841a1ff7 | ||
|
|
886310497c | ||
|
|
5e96d3cff3 | ||
|
|
d66af32eed | ||
|
|
982318326f | ||
|
|
2d680b9c4c | ||
|
|
17e2d211c1 | ||
|
|
e1020be250 | ||
|
|
ce219a790a | ||
|
|
7e55ded95d | ||
|
|
f70c6e8feb | ||
|
|
bdfaaecc83 | ||
|
|
4622fd6df2 | ||
|
|
fa5691fa2a | ||
|
|
2b995f8396 | ||
|
|
e044dc69bc | ||
|
|
26bf095f19 | ||
|
|
2ba66d9625 | ||
|
|
621440f981 | ||
|
|
a0c1efce9c | ||
|
|
861f916eb4 | ||
|
|
8ae1c59b41 | ||
|
|
d213b5dffe | ||
|
|
099531be72 | ||
|
|
b7e92ab33c | ||
|
|
0720d431db | ||
|
|
62c7b7b9df | ||
|
|
4eed4be958 | ||
|
|
eb0dc48d24 | ||
|
|
4ff8e8335a | ||
|
|
ec263a402d | ||
|
|
0ffb06cc79 | ||
|
|
302526000f | ||
|
|
76891d41e2 | ||
|
|
bb3f9fe216 | ||
|
|
dd0ce73260 | ||
|
|
206fa06049 | ||
|
|
3985eecfe6 | ||
|
|
97a7b5c27c | ||
|
|
02c7063655 | ||
|
|
0067a728a4 | ||
|
|
3ec15253d0 | ||
|
|
09772d0968 | ||
|
|
6496e0cf7c | ||
|
|
b6ef01c59a | ||
|
|
522ea54ff2 | ||
|
|
ba4cc11196 | ||
|
|
95622182c9 | ||
|
|
0349dfcbaf | ||
|
|
9fc9d9cfe9 | ||
|
|
5ea16d0869 | ||
|
|
57b259fcba | ||
|
|
b78cf1fca5 | ||
|
|
77b71e56de | ||
|
|
c8e3d370a1 | ||
|
|
ab8a325da2 | ||
|
|
a3158514af | ||
|
|
2804d4686a | ||
|
|
cf0dc2d00d | ||
|
|
164aa38cb1 | ||
|
|
14998d0f25 | ||
|
|
ca74fe7ccd | ||
|
|
facba11bae | ||
|
|
8b8302fb53 | ||
|
|
eaea4bf8b8 | ||
|
|
3d9c3e4103 | ||
|
|
e840d7b3e9 | ||
|
|
3a248ad2c5 | ||
|
|
5912d7df2d | ||
|
|
94842114e6 | ||
|
|
d83df0ba5a | ||
|
|
0764fb50b2 | ||
|
|
bb5de868ab | ||
|
|
2b8fe6c28d | ||
|
|
0153e09f0d | ||
|
|
dc44433d7c | ||
|
|
8bdefdb331 | ||
|
|
c7f5e320fc | ||
|
|
c1582cc763 | ||
|
|
f2f0f7a793 | ||
|
|
3d665e397e | ||
|
|
194f8686f3 | ||
|
|
fb79d0ddd6 | ||
|
|
d7e0a4e441 | ||
|
|
465123a156 | ||
|
|
88d01562b1 | ||
|
|
85421f41b8 | ||
|
|
a67cb10633 | ||
|
|
f00a161fc1 | ||
|
|
c071c56eb7 | ||
|
|
b39f769423 | ||
|
|
dde526c059 | ||
|
|
c223d6e81d | ||
|
|
d531a1d313 | ||
|
|
b0722cc827 | ||
|
|
2e534abfbb | ||
|
|
6d3ca9877a | ||
|
|
f477cb32ab | ||
|
|
51b79ed413 | ||
|
|
fa78f03f0a | ||
|
|
a40fec4082 | ||
|
|
f6a9d8ca4e | ||
|
|
dd2bfc4e1b | ||
|
|
06fd78378e | ||
|
|
f28574245c | ||
|
|
b964c523dd | ||
|
|
0721bc3ec5 | ||
|
|
3f783305b2 | ||
|
|
be29180e48 | ||
|
|
19c65d7d90 | ||
|
|
704c7f1f80 | ||
|
|
678ac9d466 | ||
|
|
a8a6c5d736 | ||
|
|
e8408710df | ||
|
|
47825f0783 | ||
|
|
f7ce518812 | ||
|
|
f887a068b9 | ||
|
|
6ecbce4b87 | ||
|
|
9454d75f55 | ||
|
|
4063321418 | ||
|
|
bb88d58e47 | ||
|
|
7bc2f065c0 | ||
|
|
c773b45ddf | ||
|
|
eaf7db7813 | ||
|
|
a29f6350d0 | ||
|
|
65ad925d37 | ||
|
|
8fd486d582 | ||
|
|
8fa5dcadcb | ||
|
|
6abf6c9cfd | ||
|
|
b4603da714 | ||
|
|
b27e84de69 | ||
|
|
49337cbbc6 | ||
|
|
fe2f5e923c | ||
|
|
69f54dbd54 | ||
|
|
bc20ec0d8f | ||
|
|
278add7a11 | ||
|
|
6e90091883 | ||
|
|
ebda22d7b4 | ||
|
|
625ffb3932 | ||
|
|
fe6868911e | ||
|
|
1c73d15377 | ||
|
|
c33ee55efb | ||
|
|
56979a2122 | ||
|
|
3e1db8bfdf | ||
|
|
10fdc52446 | ||
|
|
23d23c40a5 | ||
|
|
90e8eeb983 | ||
|
|
dcf395ec46 | ||
|
|
d55cb553d3 | ||
|
|
b862d26bc3 | ||
|
|
d5e4f11849 | ||
|
|
5e9679f6c3 | ||
|
|
8799c447fb | ||
|
|
bcdb767b7e | ||
|
|
15e208d34c | ||
|
|
f16c41e6dd | ||
|
|
9110c55cb1 | ||
|
|
881e155cbc | ||
|
|
e2d187a7bd | ||
|
|
66821d884a | ||
|
|
73ad1dc46c | ||
|
|
c91a2d3ee5 | ||
|
|
87c0e95b01 | ||
|
|
174da2ac14 | ||
|
|
b61ba37b8f | ||
|
|
27333e7836 | ||
|
|
58a9e81bd8 | ||
|
|
d78e92f42f | ||
|
|
2a5eb7b057 | ||
|
|
ae5f70645a | ||
|
|
d26b14276e | ||
|
|
9166ce7218 | ||
|
|
5f0ec98b0c | ||
|
|
1bc7af0a88 | ||
|
|
df75d33ca6 | ||
|
|
34f7df6bfb | ||
|
|
1208b439fa | ||
|
|
14a9faa2ee | ||
|
|
ca4fb0b35e | ||
|
|
6ea6e6d9b2 | ||
|
|
2e17aa40ec | ||
|
|
098392684f | ||
|
|
6678e225c5 | ||
|
|
ca0bd15e69 | ||
|
|
1675240f13 | ||
|
|
b21c6325bb | ||
|
|
b2f9e3d754 | ||
|
|
6dc5d74de3 | ||
|
|
be560aae89 | ||
|
|
37858ca972 | ||
|
|
d3a1a2aafb | ||
|
|
49b890715e | ||
|
|
f19449107b | ||
|
|
8bbed4fa76 | ||
|
|
efc4950f92 | ||
|
|
23fd13ad0c | ||
|
|
64cd90dfaa | ||
|
|
ec5919d67f | ||
|
|
0fc9d7fb40 | ||
|
|
4f03554fbb | ||
|
|
4a16605f43 | ||
|
|
3ae60e1c41 | ||
|
|
edab888e31 | ||
|
|
2eefcee9b7 | ||
|
|
c1db263dcf | ||
|
|
cdf0d34b86 | ||
|
|
eb0528215b | ||
|
|
30d0940359 | ||
|
|
9f7cdd8b42 | ||
|
|
af00334455 | ||
|
|
4e8af61539 | ||
|
|
a17b78c56b | ||
|
|
8f536f487e | ||
|
|
82cad601bf | ||
|
|
a3579c42fa | ||
|
|
9af0046554 | ||
|
|
d59eabc9b3 | ||
|
|
d8d55cfbf8 | ||
|
|
bce3f3cef3 | ||
|
|
e2d5e602e1 | ||
|
|
054087a3bf | ||
|
|
123d1f9634 | ||
|
|
9130cabc65 | ||
|
|
3c893444e6 | ||
|
|
24935046e9 | ||
|
|
ecf596623e | ||
|
|
620be36635 | ||
|
|
3c98edfb6d | ||
|
|
b0c4690489 | ||
|
|
64f1fce8c8 | ||
|
|
f2df53b94b | ||
|
|
ae4aec68c6 | ||
|
|
68696a585a | ||
|
|
ee0279186a | ||
|
|
60c4747a0e | ||
|
|
23e2202bc0 | ||
|
|
e9c2ed8a76 | ||
|
|
9b9174b45a | ||
|
|
e9451c1c76 | ||
|
|
28bd6423b7 | ||
|
|
083fe13ce3 | ||
|
|
574a78ba0b | ||
|
|
54177f927e | ||
|
|
a9c0a23f0a | ||
|
|
5f92401c98 | ||
|
|
2d959a580f | ||
|
|
cc046278fd | ||
|
|
af82f731cf | ||
|
|
a9ff106e54 | ||
|
|
8e2cf858b9 | ||
|
|
d19eee81b3 | ||
|
|
9f5a2ae120 | ||
|
|
6bb520a0a9 | ||
|
|
5031e01e00 | ||
|
|
4ae0c5c638 | ||
|
|
fb45b52341 | ||
|
|
156ea9e7ae | ||
|
|
1b7e86481b | ||
|
|
e11d6d7f6a | ||
|
|
c72759c70a |
375
.github/workflows/main.yml
vendored
375
.github/workflows/main.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
window-build:
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
@@ -37,14 +37,6 @@ jobs:
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -117,7 +109,7 @@ jobs:
|
||||
|
||||
- name: Build qthttpserver
|
||||
run: |
|
||||
cd src\qthttpserver
|
||||
cd src\qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
@@ -132,6 +124,8 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
|
||||
@@ -270,13 +264,6 @@ jobs:
|
||||
# path: "src/smtpclient/"
|
||||
# ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
#
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Checkout submodule repo
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# repository: cagnulein/qmdnsengine
|
||||
# path: "src/qmdnsengine/"
|
||||
# ref: "zwift"
|
||||
#
|
||||
# - uses: msys2/setup-msys2@v2
|
||||
# with:
|
||||
@@ -385,14 +372,6 @@ jobs:
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -409,7 +388,7 @@ jobs:
|
||||
path: "src/qthttpserver"
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -449,7 +428,16 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: test_results_xml
|
||||
path: tst/test-results/**/*.xml
|
||||
path: tst/test-results/**/*.xml
|
||||
|
||||
- name: Upload test FIT files and database
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test_fit_files_and_db
|
||||
path: |
|
||||
tst/test-artifacts/*.fit
|
||||
tst/test-artifacts/*.sqlite
|
||||
|
||||
# - name: Test Peloton API
|
||||
# if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
@@ -544,12 +532,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -622,9 +604,11 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
@@ -655,7 +639,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api-level: [24, 26, 28, 29, 30, 31, 33, 34, 35, 36]
|
||||
api-level: [24, 26, 28, 29, 30, 31, 33, 35, 36]
|
||||
include:
|
||||
- api-level: 24
|
||||
target: default
|
||||
@@ -685,10 +669,6 @@ jobs:
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 13"
|
||||
- api-level: 34
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
android-version: "Android 14"
|
||||
- api-level: 35
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
@@ -781,7 +761,7 @@ jobs:
|
||||
adb shell am start -n org.cagnulen.qdomyoszwift/org.cagnulen.qdomyoszwift.CustomQtActivity
|
||||
|
||||
# Wait for app to start
|
||||
sleep 60
|
||||
sleep 90
|
||||
|
||||
# Verify the app is running
|
||||
echo "Checking if app is running..."
|
||||
@@ -842,7 +822,7 @@ jobs:
|
||||
|
||||
ios-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -856,14 +836,6 @@ jobs:
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -903,9 +875,11 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
qmake CONFIG+=debug && make -j4
|
||||
qmake CONFIG+=debug CONFIG+=iphonesimulator && make -j4
|
||||
|
||||
# causes iOS build on Mac to fail
|
||||
# - name: Commit moc files
|
||||
@@ -932,14 +906,6 @@ jobs:
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -1014,6 +980,8 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
|
||||
@@ -1135,14 +1103,6 @@ jobs:
|
||||
path: "src/smtpclient/"
|
||||
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: cagnulein/qmdnsengine
|
||||
path: "src/qmdnsengine/"
|
||||
ref: "zwift"
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodule repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -1201,9 +1161,11 @@ jobs:
|
||||
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 "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
cd ..
|
||||
|
||||
- name: Clone vcpkg
|
||||
run: git clone https://github.com/microsoft/vcpkg.git
|
||||
@@ -1297,6 +1259,8 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
@@ -1307,8 +1271,8 @@ jobs:
|
||||
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 &&
|
||||
for i in 1 2 3; do apt-get update && break || sleep 5; done &&
|
||||
for i in 1 2 3; do apt-get install -y --fix-missing build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql && break || sleep 5; done &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
@@ -1356,6 +1320,8 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
@@ -1366,8 +1332,8 @@ jobs:
|
||||
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 &&
|
||||
for i in 1 2 3; do apt-get update && break || sleep 5; done &&
|
||||
for i in 1 2 3; do apt-get install -y --fix-missing build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql && break || sleep 5; done &&
|
||||
export QT_SELECT=qt5 &&
|
||||
export PATH=/usr/lib/qt5/bin:$PATH &&
|
||||
cd /github/workspace &&
|
||||
@@ -1463,6 +1429,8 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
cd ..
|
||||
|
||||
@@ -1577,7 +1545,20 @@ jobs:
|
||||
nordictrack-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- name: treadmill
|
||||
setting_key: nordictrack_2950_ip
|
||||
setting_value: localhost
|
||||
- name: bike
|
||||
setting_key: tdf_10_ip
|
||||
setting_value: localhost
|
||||
- name: rower
|
||||
setting_key: proform_rower_ip
|
||||
setting_value: localhost
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
@@ -1598,12 +1579,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -1647,8 +1622,12 @@ jobs:
|
||||
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 "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
# Set variant-specific IP setting
|
||||
sed -i 's/property string ${{ matrix.variant.setting_key }}: ""/property string ${{ matrix.variant.setting_key }}: "${{ matrix.variant.setting_value }}"/' settings.qml
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
@@ -1662,30 +1641,232 @@ jobs:
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-nordictrack"/g' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-nordictrack.apk/g' src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-nordictrack-deployment-settings.json
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-nordictrack-${{ matrix.variant.name }}"/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-nordictrack-${{ matrix.variant.name }}.apk/g' src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-nordictrack-${{ matrix.variant.name }}-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
|
||||
|
||||
- name: Archive nordictrack binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nordictrack-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack.apk
|
||||
name: nordictrack-${{ matrix.variant.name }}-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-nordictrack-${{ matrix.variant.name }}.apk
|
||||
|
||||
peloton-bike-plus-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3632/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-peloton-bike-plus"/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-peloton-bike-plus.apk/g' src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-plus-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
|
||||
|
||||
- name: Archive peloton-bike-plus binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: peloton-bike-plus-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike-plus.apk
|
||||
|
||||
peloton-bike-build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- name: Xvfb install and run
|
||||
run: |
|
||||
sudo apt-get install -y xvfb
|
||||
Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 &
|
||||
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: refs/pull/3639/head
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
submodules: 'false' # Prima disattiva il checkout automatico dei submodule
|
||||
|
||||
- name: Checkout submodules with specific branches
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Install packages required to run QZ inside workflow
|
||||
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
|
||||
|
||||
- name: Install Qt Android
|
||||
uses: jdpurcell/install-qt-action@v5
|
||||
with:
|
||||
version: '5.15.0'
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android'
|
||||
modules: 'qtcharts qtnetworkauth'
|
||||
dir: '${{ github.workspace }}/output/android/'
|
||||
cache: 'true'
|
||||
cache-key-prefix: 'install-qt-action-android'
|
||||
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '11.0.23+9'
|
||||
|
||||
- name: patching qt for bluetooth
|
||||
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
|
||||
|
||||
- name: download 3rd party files for qthttpserver
|
||||
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
|
||||
|
||||
- name: Set Android NDK 21 && build
|
||||
run: |
|
||||
# Install NDK 21 after GitHub update
|
||||
# https://github.com/actions/virtual-environments/issues/5595
|
||||
ANDROID_ROOT="/usr/local/lib/android"
|
||||
ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk"
|
||||
SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager"
|
||||
echo "y" | $SDKMANAGER "ndk;21.4.7075529"
|
||||
export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_NDK}"
|
||||
cd src
|
||||
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
|
||||
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
|
||||
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
|
||||
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
|
||||
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
|
||||
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
|
||||
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
|
||||
echo "#define LICENSE" >> secret.h
|
||||
cd ..
|
||||
|
||||
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
|
||||
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
|
||||
|
||||
# QTHTTPSERVER must use the same NDK
|
||||
cd src/qthttpserver
|
||||
qmake
|
||||
make -j8
|
||||
make install
|
||||
cd ../..
|
||||
|
||||
qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install
|
||||
cp src/android-qdomyos-zwift-deployment-settings.json src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i 's/"android-debug"/"android-peloton-bike"/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
sed -i 's/android-debug\.apk/android-debug-peloton-bike.apk/g' src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
cat src/android-qdomyos-zwift-peloton-bike-deployment-settings.json
|
||||
|
||||
- name: Build APK (not usable for production due to unpatched QT library)
|
||||
run: cd src; androiddeployqt --input android-qdomyos-zwift-peloton-bike-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab; mv ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
|
||||
|
||||
- name: Archive peloton-bike binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: peloton-bike-android-trial
|
||||
path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug-peloton-bike.apk
|
||||
|
||||
upload_to_release:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-22.04
|
||||
#if: github.event_name == 'schedule'
|
||||
needs: [linux-x86-build, window-msvc2019-build, window-msvc2022-build, ios-build, window-build, android-build, raspberry-pi-build]
|
||||
if: github.event_name == 'schedule'
|
||||
needs: [window-msvc2019-build, window-msvc2022-build, window-build, android-build, raspberry-pi-build, nordictrack-build, peloton-bike-plus-build, peloton-bike-build, raspberry-pi-build-and-image-64bit]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
@@ -1694,9 +1875,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: nightly
|
||||
tag_name: nightly-${{ steps.date.outputs.date }}
|
||||
prerelease: false
|
||||
name: 'QZ nightly build $$'
|
||||
name: 'QZ nightly build - ${{ steps.date.outputs.date }}'
|
||||
body: |
|
||||
This is a nightly build of QZ.
|
||||
|
||||
@@ -1713,7 +1894,11 @@ jobs:
|
||||
|
||||
## Other Platforms:
|
||||
- **fdroid-android-trial**: Android build
|
||||
- **nordictrack-android-trial**: Nordictrack build for iFIT2 Tablets
|
||||
- **nordictrack-treadmill-android-trial**: Nordictrack Treadmill build for iFIT2 Tablets
|
||||
- **nordictrack-bike-android-trial**: Nordictrack Bike build for iFIT2 Tablets
|
||||
- **nordictrack-rower-android-trial**: Nordictrack Rower build for iFIT2 Tablets
|
||||
- **peloton-bike-plus-android-trial**: Peloton Bike Plus build with Grupetto backend
|
||||
- **peloton-bike-android-trial**: Peloton Bike build with Grupetto backend
|
||||
- **raspberry-pi-binary**: Raspberry Pi build
|
||||
|
||||
__Please help us improve QZ by reporting any issues you encounter!__ :wink:
|
||||
@@ -1726,6 +1911,10 @@ jobs:
|
||||
windows-binary-no-python/*
|
||||
windows-binary/*
|
||||
fdroid-android-trial/android-debug.apk
|
||||
nordictrack-android-trial/android-debug-nordictrack.apk
|
||||
nordictrack-treadmill-android-trial/android-debug-nordictrack-treadmill.apk
|
||||
nordictrack-bike-android-trial/android-debug-nordictrack-bike.apk
|
||||
nordictrack-rower-android-trial/android-debug-nordictrack-rower.apk
|
||||
peloton-bike-plus-android-trial/android-debug-peloton-bike-plus.apk
|
||||
peloton-bike-android-trial/android-debug-peloton-bike.apk
|
||||
raspberry-pi-binary/qdomyos-zwift-32bit
|
||||
raspberry-pi-binary/qdomyos-zwift-64bit
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
src/qdomyos-zwift.pro.user
|
||||
|
||||
.idea/
|
||||
|
||||
src/Makefile
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -5,10 +5,6 @@
|
||||
path = src/smtpclient
|
||||
url = https://github.com/cagnulein/SmtpClient-for-Qt.git
|
||||
branch = cagnulein-patch-2
|
||||
[submodule "src/qmdnsengine"]
|
||||
path = src/qmdnsengine
|
||||
url = https://github.com/cagnulein/qmdnsengine.git
|
||||
branch = zwift
|
||||
[submodule "tst/googletest"]
|
||||
path = tst/googletest
|
||||
url = https://github.com/google/googletest.git
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// QZWidget.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), emoji: "😀")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
// func relevances() async -> WidgetRelevances<Void> {
|
||||
// // Generate a list containing the contexts this widget is relevant in.
|
||||
// }
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let emoji: String
|
||||
}
|
||||
|
||||
struct QZWidgetEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Emoji:")
|
||||
Text(entry.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QZWidget: Widget {
|
||||
let kind: String = "QZWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
QZWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
QZWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("My Widget")
|
||||
.description("This is an example widget.")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
QZWidget()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, emoji: "😀")
|
||||
SimpleEntry(date: .now, emoji: "🤩")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// QZWidgetBundle.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct QZWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
QZWidget()
|
||||
QZWidgetControl()
|
||||
QZWidgetLiveActivity()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// QZWidgetControl.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct QZWidgetControl: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "org.cagnulein.qdomyoszwift.QZWidget",
|
||||
provider: Provider()
|
||||
) { value in
|
||||
ControlWidgetToggle(
|
||||
"Start Timer",
|
||||
isOn: value,
|
||||
action: StartTimerIntent()
|
||||
) { isRunning in
|
||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
||||
}
|
||||
}
|
||||
.displayName("Timer")
|
||||
.description("A an example control that runs a timer.")
|
||||
}
|
||||
}
|
||||
|
||||
extension QZWidgetControl {
|
||||
struct Provider: ControlValueProvider {
|
||||
var previewValue: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func currentValue() async throws -> Bool {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return isRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StartTimerIntent: SetValueIntent {
|
||||
static let title: LocalizedStringResource = "Start a timer"
|
||||
|
||||
@Parameter(title: "Timer is running")
|
||||
var value: Bool
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Start / stop the timer based on `value`.
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// QZWidgetLiveActivity.swift
|
||||
// QDomyos-Zwift Live Activity Widget
|
||||
//
|
||||
// Displays workout metrics on Dynamic Island and Lock Screen
|
||||
//
|
||||
|
||||
import ActivityKit
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// QZWorkoutAttributes is defined in QZWorkoutAttributes.swift (shared file)
|
||||
|
||||
// MARK: - Live Activity Widget
|
||||
@available(iOS 16.1, *)
|
||||
struct QZWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: QZWorkoutAttributes.self) { context in
|
||||
// Lock screen/banner UI
|
||||
LockScreenLiveActivityView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
// Expanded UI
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
|
||||
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
|
||||
Label("\(Int(speed)) \(speedUnit)", systemImage: "speedometer")
|
||||
.font(.caption)
|
||||
Label("\(context.state.heartRate) bpm", systemImage: "heart.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Label("\(Int(context.state.power)) W", systemImage: "bolt.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.yellow)
|
||||
Label("\(Int(context.state.cadence)) rpm", systemImage: "arrow.clockwise")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
// Empty or can add more info
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
HStack {
|
||||
let distanceKm = context.state.distance / 1000.0
|
||||
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
|
||||
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
|
||||
Label(String(format: "%.2f \(distanceUnit)", distance), systemImage: "map")
|
||||
Spacer()
|
||||
Label("\(Int(context.state.kcal)) kcal", systemImage: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
} compactLeading: {
|
||||
// Compact leading (left side of Dynamic Island)
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("\(context.state.heartRate)")
|
||||
.font(.caption2)
|
||||
}
|
||||
} compactTrailing: {
|
||||
// Compact trailing (right side of Dynamic Island)
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundColor(.yellow)
|
||||
Text("\(Int(context.state.power))")
|
||||
.font(.caption2)
|
||||
}
|
||||
} minimal: {
|
||||
// Minimal view (when multiple activities)
|
||||
Image(systemName: "figure.run")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lock Screen View
|
||||
@available(iOS 16.1, *)
|
||||
struct LockScreenLiveActivityView: View {
|
||||
let context: ActivityViewContext<QZWorkoutAttributes>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "figure.indoor.cycle")
|
||||
.foregroundColor(.blue)
|
||||
Text(context.attributes.deviceName)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
|
||||
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
|
||||
MetricView(icon: "speedometer", value: String(format: "%.1f", speed), unit: speedUnit)
|
||||
MetricView(icon: "heart.fill", value: "\(context.state.heartRate)", unit: "bpm", color: .red)
|
||||
MetricView(icon: "bolt.fill", value: "\(Int(context.state.power))", unit: "W", color: .yellow)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
let distanceKm = context.state.distance / 1000.0
|
||||
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
|
||||
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
|
||||
MetricView(icon: "arrow.clockwise", value: "\(Int(context.state.cadence))", unit: "rpm")
|
||||
MetricView(icon: "map", value: String(format: "%.2f", distance), unit: distanceUnit)
|
||||
MetricView(icon: "flame.fill", value: "\(Int(context.state.kcal))", unit: "kcal", color: .orange)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metric View Component
|
||||
struct MetricView: View {
|
||||
let icon: String
|
||||
let value: String
|
||||
let unit: String
|
||||
var color: Color = .primary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color)
|
||||
.font(.caption)
|
||||
Text(value)
|
||||
.font(.system(.body, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
Text(unit)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
@available(iOS 16.1, *)
|
||||
struct QZWidgetLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = QZWorkoutAttributes(deviceName: "QZ Bike", useMiles: false)
|
||||
static let contentState = QZWorkoutAttributes.ContentState(
|
||||
speed: 25.5,
|
||||
cadence: 85,
|
||||
power: 200,
|
||||
heartRate: 145,
|
||||
distance: 12500, // meters (will be displayed as 12.50 km or 7.77 mi)
|
||||
kcal: 320,
|
||||
useMiles: false
|
||||
)
|
||||
|
||||
static var previews: some View {
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
|
||||
.previewDisplayName("Island Compact")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
|
||||
.previewDisplayName("Island Expanded")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
|
||||
.previewDisplayName("Minimal")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .content)
|
||||
.previewDisplayName("Lock Screen")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// QZWorkoutAttributes.swift
|
||||
// QDomyos-Zwift
|
||||
//
|
||||
// Shared attributes for Live Activities
|
||||
// MUST be included in both qdomyoszwift and QZWidget targets
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
public struct QZWorkoutAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
public var speed: Double
|
||||
public var cadence: Double
|
||||
public var power: Double
|
||||
public var heartRate: Int
|
||||
public var distance: Double
|
||||
public var kcal: Double
|
||||
public var useMiles: Bool
|
||||
|
||||
public init(speed: Double, cadence: Double, power: Double, heartRate: Int, distance: Double, kcal: Double, useMiles: Bool) {
|
||||
self.speed = speed
|
||||
self.cadence = cadence
|
||||
self.power = power
|
||||
self.heartRate = heartRate
|
||||
self.distance = distance
|
||||
self.kcal = kcal
|
||||
self.useMiles = useMiles
|
||||
}
|
||||
}
|
||||
|
||||
public var deviceName: String
|
||||
public var useMiles: Bool
|
||||
|
||||
public init(deviceName: String, useMiles: Bool) {
|
||||
self.deviceName = deviceName
|
||||
self.useMiles = useMiles
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -130,6 +130,7 @@
|
||||
87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */; };
|
||||
870A5DB32CEF8FB100839641 /* moc_technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */; };
|
||||
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB42CEF8FD200839641 /* technogymbike.cpp */; };
|
||||
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870C72632E91565E00DC8A84 /* ios_liveactivity.mm */; };
|
||||
8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706B29C48AEA0094D0F3 /* handleurl.cpp */; };
|
||||
8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */; };
|
||||
8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710707229C4A5E70094D0F3 /* GarminConnect.swift */; };
|
||||
@@ -286,6 +287,8 @@
|
||||
8752C0E92B15D85600C3D1A5 /* ios_eliteariafan.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8752C0E62B15D85600C3D1A5 /* ios_eliteariafan.mm */; };
|
||||
87540FAD2848FD70005E0D44 /* libqtexttospeech_speechios.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */; };
|
||||
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8754D24B27F786F0003D7054 /* virtualrower.swift */; };
|
||||
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */; };
|
||||
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D22E4E260B006A12E4 /* fontmanager.cpp */; };
|
||||
87586A4125B8340E00A243C4 /* proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4025B8340E00A243C4 /* proformbike.cpp */; };
|
||||
87586A4325B8341B00A243C4 /* moc_proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4225B8341B00A243C4 /* moc_proformbike.cpp */; };
|
||||
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */; };
|
||||
@@ -301,7 +304,6 @@
|
||||
87646C2227B5065100F82131 /* moc_bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */; };
|
||||
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */; };
|
||||
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */; };
|
||||
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */; };
|
||||
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */; };
|
||||
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */; };
|
||||
8768C8BA2BBC11C80099DBE1 /* file_sync_client.c in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8768C89C2BBC11C70099DBE1 /* file_sync_client.c */; };
|
||||
@@ -340,6 +342,8 @@
|
||||
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9927BE35C4001D7645 /* bowflext216treadmill.cpp */; };
|
||||
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */; };
|
||||
876BFCA127BE35D8001D7645 /* moc_bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */; };
|
||||
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */; };
|
||||
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */; };
|
||||
876E4E142594748000BD5714 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 876E4E132594748000BD5714 /* Assets.xcassets */; };
|
||||
876E4E1B2594748000BD5714 /* watchkit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
876E4E202594748000BD5714 /* qdomyoszwiftApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876E4E1F2594748000BD5714 /* qdomyoszwiftApp.swift */; };
|
||||
@@ -370,7 +374,6 @@
|
||||
8772A0E625E43ADB0080718C /* trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */; };
|
||||
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */; };
|
||||
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */; };
|
||||
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */; };
|
||||
877350F72D1C08E60070CBD8 /* SmartControl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877350F62D1C08E50070CBD8 /* SmartControl.cpp */; };
|
||||
8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */; };
|
||||
8775008529E87713008E48B7 /* moc_iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */; };
|
||||
@@ -388,6 +391,9 @@
|
||||
8781908526150C8E0085E656 /* libqtlabsplatformplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 8781908126150B490085E656 /* libqtlabsplatformplugin.a */; };
|
||||
8783153B25E8D81E0007817C /* moc_sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */; };
|
||||
8783153C25E8DAFD0007817C /* sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3EBBA25D2CFED0040EB4C /* sportstechbike.cpp */; };
|
||||
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */; };
|
||||
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */; };
|
||||
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */; };
|
||||
878531642711A3E1004B153D /* fakebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531602711A3E0004B153D /* fakebike.cpp */; };
|
||||
878531652711A3E1004B153D /* activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531612711A3E1004B153D /* activiotreadmill.cpp */; };
|
||||
878531682711A3EC004B153D /* moc_activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531662711A3EB004B153D /* moc_activiotreadmill.cpp */; };
|
||||
@@ -395,7 +401,6 @@
|
||||
8785D5432B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5412B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp */; };
|
||||
8785D5442B3DD105005A2EB7 /* moc_zwift_client_auth.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */; };
|
||||
87873AEE2D09A8AA005F86B4 /* moc_sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */; };
|
||||
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */; };
|
||||
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */; };
|
||||
878A331A25AB4FF800BD13E1 /* yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331725AB4FF800BD13E1 /* yesoulbike.cpp */; };
|
||||
878A331D25AB50C300BD13E1 /* moc_yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */; };
|
||||
@@ -454,6 +459,8 @@
|
||||
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A4B75F25AF27CB0027EF3C /* metric.cpp */; };
|
||||
87A6825A2CE3AB3100586A2A /* moc_sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */; };
|
||||
87A6825D2CE3AB4000586A2A /* sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */; };
|
||||
87A892562F0C12EB00811D95 /* deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */; };
|
||||
87A892582F0C173600811D95 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A892572F0C173600811D95 /* sportsplusrower.cpp */; };
|
||||
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */; };
|
||||
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */; };
|
||||
87ADD2BB27634C1500B7A0AB /* technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */; };
|
||||
@@ -515,6 +522,8 @@
|
||||
87C5F0D926285E7E0067A1B5 /* moc_mimeattachment.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C5F0CE26285E7E0067A1B5 /* moc_mimeattachment.cpp */; };
|
||||
87C7074227E4CF5300E79C46 /* moc_keepbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C7074127E4CF5300E79C46 /* moc_keepbike.cpp */; };
|
||||
87C7074327E4CF5900E79C46 /* keepbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C7073F27E4CF4500E79C46 /* keepbike.cpp */; };
|
||||
87CBCF122EFAA2F8004F5ECE /* garminconnect.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */; };
|
||||
87CBCF132EFAA2F8004F5ECE /* moc_garminconnect.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */; };
|
||||
87CC3B9D25A08812001EC5A8 /* moc_domyoselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9B25A08812001EC5A8 /* moc_domyoselliptical.cpp */; };
|
||||
87CC3B9E25A08812001EC5A8 /* moc_elliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9C25A08812001EC5A8 /* moc_elliptical.cpp */; };
|
||||
87CC3BA325A0885F001EC5A8 /* domyoselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9F25A0885D001EC5A8 /* domyoselliptical.cpp */; };
|
||||
@@ -582,8 +591,20 @@
|
||||
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
|
||||
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
|
||||
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
|
||||
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87EFC5652E918D35005BB573 /* WidgetKit.framework */; };
|
||||
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */; };
|
||||
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */; };
|
||||
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
|
||||
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
|
||||
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
|
||||
87EFE45B27A51901006EA1C3 /* moc_nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */; };
|
||||
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
|
||||
@@ -691,6 +712,13 @@
|
||||
remoteGlobalIDString = 876E4E102594747F00BD5714;
|
||||
remoteInfo = watchkit;
|
||||
};
|
||||
87EFC5742E918D38005BB573 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 6DB9C3763D02B1415CD9D565 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 87EFC5632E918D35005BB573;
|
||||
remoteInfo = QZWidgetExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -716,6 +744,17 @@
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
99542592E9780B9225F24AA8 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -961,6 +1000,8 @@
|
||||
87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusbike.cpp; sourceTree = "<group>"; };
|
||||
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymbike.cpp; sourceTree = "<group>"; };
|
||||
870A5DB42CEF8FD200839641 /* technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = technogymbike.cpp; path = ../src/devices/technogymbike/technogymbike.cpp; sourceTree = SOURCE_ROOT; };
|
||||
870C72622E91565E00DC8A84 /* ios_liveactivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_liveactivity.h; path = ../src/ios/ios_liveactivity.h; sourceTree = SOURCE_ROOT; };
|
||||
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_liveactivity.mm; path = ../src/ios/ios_liveactivity.mm; sourceTree = SOURCE_ROOT; };
|
||||
8710706A29C48AE90094D0F3 /* handleurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = handleurl.h; path = ../src/handleurl.h; sourceTree = "<group>"; };
|
||||
8710706B29C48AEA0094D0F3 /* handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = handleurl.cpp; path = ../src/handleurl.cpp; sourceTree = "<group>"; };
|
||||
8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_handleurl.cpp; sourceTree = "<group>"; };
|
||||
@@ -1198,6 +1239,9 @@
|
||||
8752C0E72B15D85600C3D1A5 /* ios_eliteariafan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ios_eliteariafan.h; path = ../src/ios/ios_eliteariafan.h; sourceTree = "<group>"; };
|
||||
87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtexttospeech_speechios.a; path = ../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios.a; sourceTree = "<group>"; };
|
||||
8754D24B27F786F0003D7054 /* virtualrower.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = virtualrower.swift; path = ../src/ios/virtualrower.swift; sourceTree = "<group>"; };
|
||||
8755E5D12E4E260B006A12E4 /* fontmanager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fontmanager.h; path = ../src/fontmanager.h; sourceTree = SOURCE_ROOT; };
|
||||
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fontmanager.cpp; path = ../src/fontmanager.cpp; sourceTree = SOURCE_ROOT; };
|
||||
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fontmanager.cpp; sourceTree = "<group>"; };
|
||||
87586A3F25B8340D00A243C4 /* proformbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformbike.h; path = ../src/devices/proformbike/proformbike.h; sourceTree = "<group>"; };
|
||||
87586A4025B8340E00A243C4 /* proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformbike.cpp; path = ../src/devices/proformbike/proformbike.cpp; sourceTree = "<group>"; };
|
||||
87586A4225B8341B00A243C4 /* moc_proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformbike.cpp; sourceTree = "<group>"; };
|
||||
@@ -1228,8 +1272,6 @@
|
||||
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = elitesquarecontroller.h; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.h; sourceTree = SOURCE_ROOT; };
|
||||
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = elitesquarecontroller.cpp; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.cpp; sourceTree = SOURCE_ROOT; };
|
||||
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elitesquarecontroller.cpp; sourceTree = "<group>"; };
|
||||
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_wahookickrsnapbike.h; path = ../src/ios/ios_wahookickrsnapbike.h; sourceTree = SOURCE_ROOT; };
|
||||
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_wahookickrsnapbike.mm; path = ../src/ios/ios_wahookickrsnapbike.mm; sourceTree = SOURCE_ROOT; };
|
||||
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_zwiftclickremote.h; path = ../src/ios/ios_zwiftclickremote.h; sourceTree = SOURCE_ROOT; };
|
||||
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_zwiftclickremote.mm; path = ../src/ios/ios_zwiftclickremote.mm; sourceTree = SOURCE_ROOT; };
|
||||
8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor.cpp; path = ../src/characteristics/characteristicwriteprocessor.cpp; sourceTree = "<group>"; };
|
||||
@@ -1284,6 +1326,9 @@
|
||||
876BFC9B27BE35C5001D7645 /* proformelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformelliptical.h; path = ../src/devices/proformelliptical/proformelliptical.h; sourceTree = "<group>"; };
|
||||
876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformelliptical.cpp; sourceTree = "<group>"; };
|
||||
876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bowflext216treadmill.cpp; sourceTree = "<group>"; };
|
||||
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitbackupwriter.h; path = ../src/fitbackupwriter.h; sourceTree = SOURCE_ROOT; };
|
||||
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitbackupwriter.cpp; path = ../src/fitbackupwriter.cpp; sourceTree = SOURCE_ROOT; };
|
||||
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitbackupwriter.cpp; sourceTree = "<group>"; };
|
||||
876E4E112594747F00BD5714 /* watchkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watchkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
876E4E132594748000BD5714 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
876E4E152594748000BD5714 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -1340,7 +1385,6 @@
|
||||
8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = trxappgateusbbike.cpp; path = ../src/devices/trxappgateusbbike/trxappgateusbbike.cpp; sourceTree = "<group>"; };
|
||||
8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_trxappgateusbbike.cpp; sourceTree = "<group>"; };
|
||||
8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_deerruntreadmill.cpp; sourceTree = "<group>"; };
|
||||
8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = deerruntreadmill.cpp; sourceTree = "<group>"; };
|
||||
8772B7F92CB5603A004AB8E9 /* deerruntreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = deerruntreadmill.h; path = ../src/devices/deeruntreadmill/deerruntreadmill.h; sourceTree = SOURCE_ROOT; };
|
||||
877350F52D1C08E50070CBD8 /* SmartControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SmartControl.h; path = ../src/devices/kineticinroadbike/SmartControl.h; sourceTree = SOURCE_ROOT; };
|
||||
877350F62D1C08E50070CBD8 /* SmartControl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = SmartControl.cpp; path = ../src/devices/kineticinroadbike/SmartControl.cpp; sourceTree = SOURCE_ROOT; };
|
||||
@@ -1367,6 +1411,10 @@
|
||||
878225C234983ACB863D2D29 /* fit_nmea_sentence_mesg.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_nmea_sentence_mesg.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_nmea_sentence_mesg.hpp"; sourceTree = "<absolute>"; };
|
||||
8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechbike.cpp; sourceTree = "<group>"; };
|
||||
87842E7E25AF88FB00321E69 /* secret.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = secret.h; path = ../src/secret.h; sourceTree = "<group>"; };
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtlabscalendarplugin.a; path = ../../Qt/5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a; sourceTree = "<group>"; };
|
||||
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbrower.cpp; sourceTree = "<group>"; };
|
||||
878521D22E44B26600922796 /* nordictrackifitadbrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbrower.h; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h; sourceTree = SOURCE_ROOT; };
|
||||
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbrower.cpp; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp; sourceTree = SOURCE_ROOT; };
|
||||
878531602711A3E0004B153D /* fakebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakebike.cpp; path = ../src/devices/fakebike/fakebike.cpp; sourceTree = "<group>"; };
|
||||
878531612711A3E1004B153D /* activiotreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = activiotreadmill.cpp; path = ../src/devices/activiotreadmill/activiotreadmill.cpp; sourceTree = "<group>"; };
|
||||
878531622711A3E1004B153D /* activiotreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = activiotreadmill.h; path = ../src/devices/activiotreadmill/activiotreadmill.h; sourceTree = "<group>"; };
|
||||
@@ -1378,7 +1426,6 @@
|
||||
8785D5412B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_PlayerStateWrapper.cpp; sourceTree = "<group>"; };
|
||||
8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_zwift_client_auth.cpp; sourceTree = "<group>"; };
|
||||
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusrower.cpp; sourceTree = "<group>"; };
|
||||
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sportsplusrower.cpp; sourceTree = "<group>"; };
|
||||
87873AF22D09AADF005F86B4 /* sportsplusrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportsplusrower.h; path = ../src/devices/sportsplusrower/sportsplusrower.h; sourceTree = SOURCE_ROOT; };
|
||||
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = inclinationresistancetable.h; path = ../src/inclinationresistancetable.h; sourceTree = SOURCE_ROOT; };
|
||||
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inclinationresistancetable.cpp; sourceTree = "<group>"; };
|
||||
@@ -1473,6 +1520,8 @@
|
||||
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sramAXSController.cpp; sourceTree = "<group>"; };
|
||||
87A6825B2CE3AB4000586A2A /* sramAXSController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sramAXSController.h; path = ../src/devices/sramAXSController/sramAXSController.h; sourceTree = SOURCE_ROOT; };
|
||||
87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sramAXSController.cpp; path = ../src/devices/sramAXSController/sramAXSController.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = deerruntreadmill.cpp; path = ../src/devices/deeruntreadmill/deerruntreadmill.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87A892572F0C173600811D95 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sportsplusrower.cpp; path = ../src/devices/sportsplusrower/sportsplusrower.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = androidstatusbar.h; path = ../src/androidstatusbar.h; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = androidstatusbar.cpp; path = ../src/androidstatusbar.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_androidstatusbar.cpp; sourceTree = "<group>"; };
|
||||
@@ -1563,6 +1612,9 @@
|
||||
87C7073F27E4CF4500E79C46 /* keepbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = keepbike.cpp; path = ../src/devices/keepbike/keepbike.cpp; sourceTree = "<group>"; };
|
||||
87C7074027E4CF4500E79C46 /* keepbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = keepbike.h; path = ../src/devices/keepbike/keepbike.h; sourceTree = "<group>"; };
|
||||
87C7074127E4CF5300E79C46 /* moc_keepbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_keepbike.cpp; sourceTree = "<group>"; };
|
||||
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = garminconnect.h; path = ../src/garminconnect.h; sourceTree = SOURCE_ROOT; };
|
||||
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = garminconnect.cpp; path = ../src/garminconnect.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_garminconnect.cpp; sourceTree = "<group>"; };
|
||||
87CC3B9B25A08812001EC5A8 /* moc_domyoselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_domyoselliptical.cpp; sourceTree = "<group>"; };
|
||||
87CC3B9C25A08812001EC5A8 /* moc_elliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elliptical.cpp; sourceTree = "<group>"; };
|
||||
87CC3B9F25A0885D001EC5A8 /* domyoselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = domyoselliptical.cpp; path = ../src/devices/domyoselliptical/domyoselliptical.cpp; sourceTree = "<group>"; };
|
||||
@@ -1658,9 +1710,23 @@
|
||||
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
|
||||
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
|
||||
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
|
||||
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
|
||||
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
|
||||
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QZWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
87EFC5652E918D35005BB573 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveActivityBridge.swift; path = ../src/ios/LiveActivityBridge.swift; sourceTree = SOURCE_ROOT; };
|
||||
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QZWidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QZWorkoutAttributes.swift; path = QZWidget/QZWorkoutAttributes.swift; sourceTree = "<group>"; };
|
||||
87EFE45727A518F5006EA1C3 /* nautiluselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nautiluselliptical.h; path = ../src/devices/nautiluselliptical/nautiluselliptical.h; sourceTree = "<group>"; };
|
||||
87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nautiluselliptical.cpp; path = ../src/devices/nautiluselliptical/nautiluselliptical.cpp; sourceTree = "<group>"; };
|
||||
87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nautiluselliptical.cpp; sourceTree = "<group>"; };
|
||||
@@ -1897,6 +1963,20 @@
|
||||
FF5BDAB0076F3391B219EA52 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = "<absolute>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
87EFC5682E918D35005BB573 /* QZWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = QZWidget; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
876E4E172594748000BD5714 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
@@ -1905,10 +1985,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC5612E918D35005BB573 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */,
|
||||
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D1C883685E82D5676953459A /* Link Binary With Libraries */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */,
|
||||
8768C9282BBC13220099DBE1 /* libcrypto.a in Link Binary With Libraries */,
|
||||
87FA94672B6B89FD00B6AB9A /* SwiftUI.framework in Link Binary With Libraries */,
|
||||
879F74112893D5B8009A64C8 /* libqavfcamera.a in Link Binary With Libraries */,
|
||||
@@ -2245,9 +2335,36 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87A892572F0C173600811D95 /* sportsplusrower.cpp */,
|
||||
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */,
|
||||
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
|
||||
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */,
|
||||
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */,
|
||||
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */,
|
||||
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
|
||||
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
|
||||
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */,
|
||||
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
|
||||
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
|
||||
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
|
||||
8755E5D12E4E260B006A12E4 /* fontmanager.h */,
|
||||
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */,
|
||||
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */,
|
||||
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */,
|
||||
878521D22E44B26600922796 /* nordictrackifitadbrower.h */,
|
||||
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */,
|
||||
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */,
|
||||
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */,
|
||||
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */,
|
||||
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */,
|
||||
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */,
|
||||
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */,
|
||||
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */,
|
||||
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */,
|
||||
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */,
|
||||
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */,
|
||||
87EBB2A42D39214E00348B15 /* workoutmodel.h */,
|
||||
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */,
|
||||
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */,
|
||||
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */,
|
||||
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */,
|
||||
@@ -2256,8 +2373,6 @@
|
||||
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */,
|
||||
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */,
|
||||
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */,
|
||||
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */,
|
||||
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */,
|
||||
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */,
|
||||
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */,
|
||||
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */,
|
||||
@@ -2300,7 +2415,6 @@
|
||||
875CA9482D0C742500667EE6 /* kineticinroadbike.cpp */,
|
||||
875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */,
|
||||
87873AF22D09AADF005F86B4 /* sportsplusrower.h */,
|
||||
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */,
|
||||
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */,
|
||||
870A5DB42CEF8FD200839641 /* technogymbike.cpp */,
|
||||
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */,
|
||||
@@ -2346,7 +2460,6 @@
|
||||
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */,
|
||||
87A083062C73361C00567A4E /* characteristicnotifier2ad9.h */,
|
||||
8772B7F92CB5603A004AB8E9 /* deerruntreadmill.h */,
|
||||
8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */,
|
||||
8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */,
|
||||
877758B52C98629B00BB1697 /* sportstechelliptical.cpp */,
|
||||
877758B42C98629B00BB1697 /* sportstechelliptical.h */,
|
||||
@@ -2948,6 +3061,7 @@
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */,
|
||||
8768C9262BBC12D10099DBE1 /* libcrypto.a */,
|
||||
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */,
|
||||
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */,
|
||||
@@ -3079,6 +3193,7 @@
|
||||
4D765E1B1EA6C757220C63E7 /* CoreFoundation.framework */,
|
||||
FCC237CA5AD60B9BA4447615 /* Foundation.framework */,
|
||||
344F66310C19536DB4886D8F /* qtpcre2 */,
|
||||
87EFC5652E918D35005BB573 /* WidgetKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -3341,6 +3456,7 @@
|
||||
E8C543AB96796ECAA2E65C57 /* qdomyoszwift */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */,
|
||||
8768C8CE2BBC12170099DBE1 /* adb */,
|
||||
87BAC3BE2BA497160003E925 /* PrivacyInfo.xcprivacy */,
|
||||
8745B2752AFCB4A300991A39 /* android */,
|
||||
@@ -3351,6 +3467,7 @@
|
||||
74B182DB50CB5611B5C1C297 /* Supporting Files */,
|
||||
876E4E122594747F00BD5714 /* watchkit */,
|
||||
876E4E1E2594748000BD5714 /* watchkit Extension */,
|
||||
87EFC5682E918D35005BB573 /* QZWidget */,
|
||||
AF39DD055C3EF8226FBE929D /* Frameworks */,
|
||||
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
|
||||
FE0A091FDBFB3E9C31B7A1BD /* Products */,
|
||||
@@ -3365,6 +3482,7 @@
|
||||
040B10E2EF2CEF79F2205FE2 /* qdomyoszwift.app */,
|
||||
876E4E112594747F00BD5714 /* watchkit.app */,
|
||||
876E4E1A2594748000BD5714 /* watchkit Extension.appex */,
|
||||
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -3381,11 +3499,13 @@
|
||||
30414803F31797EB689AE508 /* Copy Bundle Resources */,
|
||||
99542592E9780B9225F24AA8 /* Embed Frameworks */,
|
||||
876E4E332594748100BD5714 /* Embed Watch Content */,
|
||||
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
876E4E312594748100BD5714 /* PBXTargetDependency */,
|
||||
87EFC5752E918D38005BB573 /* PBXTargetDependency */,
|
||||
);
|
||||
name = qdomyoszwift;
|
||||
packageProductDependencies = (
|
||||
@@ -3431,6 +3551,28 @@
|
||||
productReference = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */;
|
||||
productType = "com.apple.product-type.watchkit2-extension";
|
||||
};
|
||||
87EFC5632E918D35005BB573 /* QZWidgetExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */;
|
||||
buildPhases = (
|
||||
87EFC5602E918D35005BB573 /* Sources */,
|
||||
87EFC5612E918D35005BB573 /* Frameworks */,
|
||||
87EFC5622E918D35005BB573 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
87EFC5682E918D35005BB573 /* QZWidget */,
|
||||
);
|
||||
name = QZWidgetExtension;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = QZWidgetExtension;
|
||||
productReference = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -3438,7 +3580,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
DefaultBuildSystemTypeForWorkspace = Original;
|
||||
LastSwiftUpdateCheck = 1220;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
TargetAttributes = {
|
||||
799833E5566DEFFC37E4BF1E = {
|
||||
DevelopmentTeam = 6335M7T29D;
|
||||
@@ -3454,6 +3596,9 @@
|
||||
DevelopmentTeam = 6335M7T29D;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
87EFC5632E918D35005BB573 = {
|
||||
CreatedOnToolsVersion = 26.0.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */;
|
||||
@@ -3475,6 +3620,7 @@
|
||||
799833E5566DEFFC37E4BF1E /* qdomyoszwift */,
|
||||
876E4E102594747F00BD5714 /* watchkit */,
|
||||
876E4E192594748000BD5714 /* watchkit Extension */,
|
||||
87EFC5632E918D35005BB573 /* QZWidgetExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -3521,6 +3667,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC5622E918D35005BB573 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -3538,10 +3691,19 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
87EFC5602E918D35005BB573 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F7E50F631C51CD5B5DC0BC43 /* Compile Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */,
|
||||
8738249627E646E3004F1B46 /* characteristicnotifier2acd.cpp in Compile Sources */,
|
||||
8738249127E646E3004F1B46 /* dirconpacket.cpp in Compile Sources */,
|
||||
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
|
||||
@@ -3575,7 +3737,6 @@
|
||||
871235BF26B297670012D0F2 /* kingsmithr1protreadmill.cpp in Compile Sources */,
|
||||
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */,
|
||||
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */,
|
||||
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */,
|
||||
20A50533946A39CBD2C89104 /* bluetoothdevice.cpp in Compile Sources */,
|
||||
87C5F0D126285E7E0067A1B5 /* moc_stagesbike.cpp in Compile Sources */,
|
||||
873824E927E647A8004F1B46 /* mdns.cpp in Compile Sources */,
|
||||
@@ -3685,9 +3846,10 @@
|
||||
87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */,
|
||||
A4BD6DF51CFFF867B7B5AED4 /* fit_developer_field_definition.cpp in Compile Sources */,
|
||||
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */,
|
||||
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */,
|
||||
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */,
|
||||
3015F9B9FF4CA6D653D46CCA /* fit_developer_field_description.cpp in Compile Sources */,
|
||||
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */,
|
||||
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */,
|
||||
87310B22266FBB78008BA0D6 /* moc_homefitnessbuddy.cpp in Compile Sources */,
|
||||
87958F1B27628D5400124B24 /* moc_elitesterzosmart.cpp in Compile Sources */,
|
||||
8768C8D82BBC12890099DBE1 /* centraldir.c in Compile Sources */,
|
||||
@@ -3699,6 +3861,7 @@
|
||||
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
|
||||
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
|
||||
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
|
||||
87A892582F0C173600811D95 /* sportsplusrower.cpp in Compile Sources */,
|
||||
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
|
||||
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
|
||||
873CD22327EF8E18000131BC /* inappstoreqmltype.cpp in Compile Sources */,
|
||||
@@ -3726,7 +3889,6 @@
|
||||
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */,
|
||||
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */,
|
||||
876E50F52B701C050080FAAF /* moc_zwiftclickremote.cpp in Compile Sources */,
|
||||
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */,
|
||||
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
|
||||
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
|
||||
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
|
||||
@@ -3788,6 +3950,7 @@
|
||||
87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */,
|
||||
87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */,
|
||||
ACB47DC464A2BC9D39C544AD /* gpx.cpp in Compile Sources */,
|
||||
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */,
|
||||
6361329E515248BB41640C07 /* homeform.cpp in Compile Sources */,
|
||||
87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */,
|
||||
87C5F0D026285E7E0067A1B5 /* moc_smtpclient.cpp in Compile Sources */,
|
||||
@@ -3815,6 +3978,12 @@
|
||||
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
|
||||
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
|
||||
27E452D452B62D0948DF0755 /* sessionline.cpp in Compile Sources */,
|
||||
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */,
|
||||
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */,
|
||||
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */,
|
||||
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */,
|
||||
E40895A73216AC52D35083D9 /* signalhandler.cpp in Compile Sources */,
|
||||
873CD22427EF8E18000131BC /* inappproductqmltype.cpp in Compile Sources */,
|
||||
87DF68BF25E2675100FCDA46 /* moc_schwinnic4bike.cpp in Compile Sources */,
|
||||
@@ -3864,6 +4033,8 @@
|
||||
872088EB2CE6543C008C2C17 /* moc_mqttpublisher.cpp in Compile Sources */,
|
||||
872088EC2CE6543C008C2C17 /* moc_qmqttclient.cpp in Compile Sources */,
|
||||
875CA94C2D130F8100667EE6 /* moc_osc.cpp in Compile Sources */,
|
||||
87CBCF122EFAA2F8004F5ECE /* garminconnect.cpp in Compile Sources */,
|
||||
87CBCF132EFAA2F8004F5ECE /* moc_garminconnect.cpp in Compile Sources */,
|
||||
872088ED2CE6543C008C2C17 /* moc_qmqttmessage.cpp in Compile Sources */,
|
||||
872088EE2CE6543C008C2C17 /* moc_qmqttsubscription.cpp in Compile Sources */,
|
||||
872088EF2CE6543C008C2C17 /* moc_qmqttconnection_p.cpp in Compile Sources */,
|
||||
@@ -3874,6 +4045,8 @@
|
||||
87440FBF2640292900E4DC0B /* moc_fitplusbike.cpp in Compile Sources */,
|
||||
8768C8CA2BBC11C80099DBE1 /* sockets.c in Compile Sources */,
|
||||
87B617EC25F25FED0094A1CB /* screencapture.cpp in Compile Sources */,
|
||||
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */,
|
||||
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */,
|
||||
876F9B5F275385C9006AE6FA /* fitmetria_fanfit.cpp in Compile Sources */,
|
||||
FB2566376FE0FB17ED3DE94D /* FitDeveloperField.mm in Compile Sources */,
|
||||
43FA2D5EA73D9C89F1A333B6 /* FitEncode.mm in Compile Sources */,
|
||||
@@ -3890,6 +4063,9 @@
|
||||
2B42755BF45173E11E2110CB /* FitFieldDefinition.mm in Compile Sources */,
|
||||
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
|
||||
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
|
||||
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
|
||||
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */,
|
||||
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
|
||||
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
|
||||
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
|
||||
872261F0289EA887006A6F75 /* moc_nordictrackelliptical.cpp in Compile Sources */,
|
||||
@@ -3967,6 +4143,7 @@
|
||||
E8B499F921FB0AB55C7A8A8B /* moc_gpx.cpp in Compile Sources */,
|
||||
87E6A85825B5C88E00371D28 /* moc_flywheelbike.cpp in Compile Sources */,
|
||||
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */,
|
||||
87A892562F0C12EB00811D95 /* deerruntreadmill.cpp in Compile Sources */,
|
||||
878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */,
|
||||
873824B727E64707004F1B46 /* moc_characteristicwriteprocessor.cpp in Compile Sources */,
|
||||
87310B1F266FBB59008BA0D6 /* homefitnessbuddy.cpp in Compile Sources */,
|
||||
@@ -4068,6 +4245,11 @@
|
||||
target = 876E4E102594747F00BD5714 /* watchkit */;
|
||||
targetProxy = 876E4E302594748100BD5714 /* PBXContainerItemProxy */;
|
||||
};
|
||||
87EFC5752E918D38005BB573 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
|
||||
targetProxy = 87EFC5742E918D38005BB573 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -4391,7 +4573,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1125;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
|
||||
@@ -4427,6 +4610,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4472,6 +4656,7 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
|
||||
);
|
||||
MARKETING_VERSION = 2.20;
|
||||
OTHER_CFLAGS = (
|
||||
@@ -4566,7 +4751,11 @@
|
||||
QMAKE_PKGINFO_TYPEINFO = "????";
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = "";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_INSTALL_OBJC_HEADER = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
|
||||
@@ -4585,8 +4774,9 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1125;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
@@ -4623,6 +4813,7 @@
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
|
||||
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
|
||||
../../Qt/5.15.2/ios/include/QtSql,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
|
||||
@@ -4668,6 +4859,7 @@
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
|
||||
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
|
||||
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
|
||||
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
|
||||
);
|
||||
MARKETING_VERSION = 2.20;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -4763,7 +4955,11 @@
|
||||
QMAKE_PKGINFO_TYPEINFO = "????";
|
||||
QMAKE_SHORT_VERSION = 1.7;
|
||||
QT_LIBRARY_SUFFIX = _debug;
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_INSTALL_OBJC_HEADER = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
|
||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
|
||||
@@ -4815,7 +5011,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1125;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4911,7 +5107,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1125;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5003,7 +5199,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1125;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5119,7 +5315,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1125;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5191,6 +5387,184 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
87EFC5792E918D38005BB573 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QZWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SwiftUI,
|
||||
"-framework",
|
||||
WidgetKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
87EFC57A2E918D38005BB573 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QZWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SwiftUI,
|
||||
"-framework",
|
||||
WidgetKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -5221,6 +5595,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
87EFC5792E918D38005BB573 /* Debug */,
|
||||
87EFC57A2E918D38005BB573 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -23,11 +23,13 @@ class WatchKitConnection: NSObject {
|
||||
static let shared = WatchKitConnection()
|
||||
public static var distance = 0.0
|
||||
public static var kcal = 0.0
|
||||
public static var totalKcal = 0.0
|
||||
public static var stepCadence = 0
|
||||
public static var speed = 0.0
|
||||
public static var cadence = 0.0
|
||||
public static var power = 0.0
|
||||
public static var steps = 0
|
||||
public static var elevationGain = 0.0
|
||||
weak var delegate: WatchKitConnectionDelegate?
|
||||
|
||||
private override init() {
|
||||
@@ -70,6 +72,9 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
WatchKitConnection.distance = dDistance
|
||||
let dKcal = Double(result["kcal"] as! Double)
|
||||
WatchKitConnection.kcal = dKcal
|
||||
if let totalKcalDouble = result["totalKcal"] as? Double {
|
||||
WatchKitConnection.totalKcal = totalKcalDouble
|
||||
}
|
||||
|
||||
let dSpeed = Double(result["speed"] as! Double)
|
||||
WatchKitConnection.speed = dSpeed
|
||||
@@ -81,6 +86,13 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
let iSteps = Int(stepsDouble)
|
||||
WatchKitConnection.steps = iSteps
|
||||
}
|
||||
if let elevationGainDouble = result["elevationGain"] as? Double {
|
||||
WatchKitConnection.elevationGain = elevationGainDouble
|
||||
// Calculate flights climbed and update WorkoutTracking
|
||||
let flightsClimbed = elevationGainDouble / 3.048 // One flight = 10 feet = 3.048 meters
|
||||
WorkoutTracking.flightsClimbed = flightsClimbed
|
||||
print("WatchKitConnection: Received elevation gain: \(elevationGainDouble)m, flights: \(flightsClimbed)")
|
||||
}
|
||||
}, errorHandler: { (error) in
|
||||
print(error)
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ class WorkoutTracking: NSObject {
|
||||
static let shared = WorkoutTracking()
|
||||
public static var distance = Double()
|
||||
public static var kcal = Double()
|
||||
public static var totalKcal = Double()
|
||||
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
|
||||
public static var cadenceLastSteps = Double()
|
||||
public static var cadenceSteps = 0
|
||||
@@ -36,17 +37,18 @@ class WorkoutTracking: NSObject {
|
||||
public static var steps = Int()
|
||||
public static var cadence = Double()
|
||||
public static var lastDateMetric = Date()
|
||||
public static var flightsClimbed = Double()
|
||||
var sport: Int = 0
|
||||
let healthStore = HKHealthStore()
|
||||
let configuration = HKWorkoutConfiguration()
|
||||
var workoutSession: HKWorkoutSession!
|
||||
var workoutBuilder: HKLiveWorkoutBuilder!
|
||||
|
||||
|
||||
weak var delegate: WorkoutTrackingDelegate?
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking {
|
||||
@@ -166,6 +168,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
|
||||
@@ -175,6 +178,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .runningVerticalOscillation)!,
|
||||
HKSampleType.quantityType(forIdentifier: .walkingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .walkingStepLength)!,
|
||||
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
} else {
|
||||
@@ -185,6 +189,8 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
}
|
||||
@@ -203,6 +209,8 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
func startWorkOut() {
|
||||
WorkoutTracking.lastDateMetric = Date()
|
||||
// Reset flights climbed for new workout
|
||||
WorkoutTracking.flightsClimbed = 0
|
||||
print("Start workout")
|
||||
configWorkout()
|
||||
workoutSession.startActivity(with: Date())
|
||||
@@ -223,25 +231,30 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
workoutSession.stopActivity(with: Date())
|
||||
workoutSession.end()
|
||||
|
||||
guard let quantityType = HKQuantityType.quantityType(
|
||||
// Write active calories
|
||||
guard let activeQuantityType = HKQuantityType.quantityType(
|
||||
forIdentifier: .activeEnergyBurned) else {
|
||||
return
|
||||
}
|
||||
|
||||
let unit = HKUnit.kilocalorie()
|
||||
let totalEnergyBurned = WorkoutTracking.kcal
|
||||
let quantity = HKQuantity(unit: unit,
|
||||
doubleValue: totalEnergyBurned)
|
||||
let activeEnergyBurned = WorkoutTracking.kcal
|
||||
let activeQuantity = HKQuantity(unit: unit,
|
||||
doubleValue: activeEnergyBurned)
|
||||
|
||||
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
|
||||
|
||||
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
|
||||
quantity: quantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
|
||||
quantity: activeQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sample]) {(success, error) in}
|
||||
|
||||
workoutBuilder.add([activeSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
let unitDistance = HKUnit.mile()
|
||||
let miles = WorkoutTracking.distance
|
||||
let quantityMiles = HKQuantity(unit: unitDistance,
|
||||
@@ -273,6 +286,10 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,11 +351,15 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
// Guard to check if steps quantity type is available
|
||||
guard let quantityTypeSteps = HKQuantityType.quantityType(
|
||||
forIdentifier: .stepCount) else {
|
||||
@@ -346,7 +367,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
}
|
||||
|
||||
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
|
||||
|
||||
|
||||
// Create a sample for total steps
|
||||
let sampleSteps = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeSteps,
|
||||
@@ -354,51 +375,59 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Add the steps sample to workout builder
|
||||
workoutBuilder.add([sampleSteps]) { (success, error) in
|
||||
// Guard to check if distance quantity type is available
|
||||
guard let quantityTypeDistance = HKQuantityType.quantityType(
|
||||
forIdentifier: .distanceWalkingRunning) else {
|
||||
return
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Create flights climbed sample if available
|
||||
var samplesToAdd: [HKCumulativeQuantitySeriesSample] = [sampleSteps, sampleDistance]
|
||||
|
||||
if WorkoutTracking.flightsClimbed > 0 {
|
||||
if let quantityTypeFlights = HKQuantityType.quantityType(forIdentifier: .flightsClimbed) {
|
||||
let flightsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: WorkoutTracking.flightsClimbed)
|
||||
let sampleFlights = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeFlights,
|
||||
quantity: flightsQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
samplesToAdd.append(sampleFlights)
|
||||
print("WatchWorkoutTracking: Adding flights climbed to workout: \(WorkoutTracking.flightsClimbed)")
|
||||
}
|
||||
}
|
||||
|
||||
// Add all samples to the workout builder
|
||||
workoutBuilder.add(samplesToAdd) { (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
|
||||
|
||||
// Finish the workout and save metrics
|
||||
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
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.finishWorkout{ (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
|
||||
// Reset flights climbed for next workout
|
||||
WorkoutTracking.flightsClimbed = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,7 +442,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
}
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
||||
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
|
||||
guard let weakSelf = self else {
|
||||
return
|
||||
@@ -423,7 +452,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print("Failed to fetch steps rate")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let sum = result.sumQuantity() {
|
||||
resultCount = sum.doubleValue(for: HKUnit.count())
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia sql
|
||||
QTPLUGIN += qavfmediaplayer
|
||||
QT+= charts
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ These instructions build the app itself, not the test project.
|
||||
## On a Linux System (from source)
|
||||
|
||||
```buildoutcfg
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on Raspberry Pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
$ cd qdomyos-zwift
|
||||
$ git submodule update --init src/smtpclient/
|
||||
@@ -34,16 +34,15 @@ Download and install https://download.qt.io/archive/qt/5.12/5.12.12/qt-opensourc
|
||||
|
||||

|
||||
|
||||
This guide will walk you through steps to setup an autonomous, headless raspberry bridge.
|
||||
This guide will walk you through steps to setup an autonomous, headless Raspberry Pi bridge.
|
||||
|
||||
|
||||
### Initial System Preparation
|
||||
|
||||
You can install a lightweight version of embedded OS to speed up your raspberry booting time.
|
||||
You can install a lightweight version of embedded OS to speed up your Raspberry booting time.
|
||||
|
||||
#### Prepare your SD Card
|
||||
Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and install, on a SD card, the Raspberry lite OS version.
|
||||
Boot on the raspberry (default credentials are pi/raspberry)
|
||||
Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and install, on a SD card, [`Raspberry Pi OS Lite 64bit`](https://www.raspberrypi.com/software/operating-systems/). Boot up the Raspberry Pi (default credentials are pi/raspberry)
|
||||
|
||||
#### Change default credentials
|
||||
|
||||
@@ -56,7 +55,7 @@ Boot on the raspberry (default credentials are pi/raspberry)
|
||||
`System Options` > `Wireless LAN`
|
||||
Enter an SSID and your wifi password.
|
||||
|
||||
Your raspberry will fetch a DHCP address at boot time, which can be painful :
|
||||
Your Raspberry will fetch a DHCP address at boot time, which can be painful :
|
||||
- The IP address might change at every boot
|
||||
- This process takes approximately 10 seconds at boot time.
|
||||
|
||||
@@ -77,7 +76,7 @@ Apply the changes `sudo systemctl restart dhcpcd.service` and ensure you have in
|
||||
|
||||
#### Enable SSH access
|
||||
|
||||
You might want to access your raspberry remotely while it is attached to your fitness equipment.
|
||||
You might want to access your Raspberry remotely while it is attached to your fitness equipment.
|
||||
|
||||
`sudo raspi-config` > `Interface Options` > `SSH`
|
||||
|
||||
@@ -86,15 +85,17 @@ You might want to access your raspberry remotely while it is attached to your fi
|
||||
This option allows a faster boot. `sudo raspi-config` > `System Options` > `Network at boot` > `No`
|
||||
|
||||
#### Reboot and test connectivity
|
||||
Reboot your raspberry `sudo reboot now`
|
||||
Reboot your Raspberry `sudo reboot now`
|
||||
|
||||
Congratulations !
|
||||
Your raspberry should be reachable from your local network via SSH.
|
||||
Your Raspberry should be reachable from your local network via SSH.
|
||||
|
||||
|
||||
### QDOMYOS-ZWIFT installation
|
||||
|
||||
#### Update your raspberry (mandatory !)
|
||||
Qdomyos-zwift can be compiled from source (hard), or using a binary (easy). **Only one is required**.
|
||||
|
||||
#### Update your Raspberry (mandatory !)
|
||||
|
||||
Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
|
||||
|
||||
@@ -103,10 +104,10 @@ Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
|
||||
|
||||
This operation takes a moment to complete.
|
||||
|
||||
#### Install qdomyos-zwift from sources
|
||||
#### Option 1. Install qdomyos-zwift from sources
|
||||
|
||||
```bash
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
cd qdomyos-zwift
|
||||
git submodule update --init src/smtpclient/
|
||||
@@ -126,20 +127,117 @@ Please note :
|
||||
- Don't build the application with `-j4` option (this will fail)
|
||||
- Build operation is circa 45 minutes (subsequent builds are faster)
|
||||
|
||||
#### Option 2. Install qdomyos-zwift from binary
|
||||
|
||||
Ensure you're logged in to GitHub and download `https://github.com/cagnulein/qdomyos-zwift/actions/runs/19521021942/artifacts/4622513957`. Extract the zip file and copy the QZ binary to the Raspberry Pi Zero 2 W. If you get a 404 Not Found you might have to login to GitHub first.
|
||||
|
||||
Make it executable:
|
||||
```
|
||||
chmod +x qdomyos-zwift-64bit
|
||||
```
|
||||
|
||||
Install required libraries and dependencies for headless mode:
|
||||
```
|
||||
sudo apt install libqt5charts5 libqt5multimedia5 libqt5bluetooth5 libqt5xml5t64 libqt5positioning5 libqt5networkauth5 libqt5websockets5 libqt5texttospeech5 libqt5sql5t64
|
||||
```
|
||||
|
||||
If you are running Raspberry Pi Desktop OS, and you want to run the QZ UI, additonally add the qml libraries.
|
||||
```
|
||||
sudo apt install libqt5charts5 libqt5multimedia5 libqt5bluetooth5 libqt5xml5t64 libqt5positioning5 libqt5networkauth5 libqt5websockets5 libqt5texttospeech5 libqt5sql5t64 *qml*
|
||||
```
|
||||
|
||||
|
||||
#### Unblock Bluetooth (if using Bluetooth)
|
||||
|
||||
Unblock Bluetooth:
|
||||
```
|
||||
sudo rfkill unblock bluetooth
|
||||
```
|
||||
|
||||
Troubleshooting Bluetooth not working:
|
||||
Errors:
|
||||
```
|
||||
Fri Nov 21 18:05:07 2025 1763708707500 Debug: Bluez 5 detected.
|
||||
qt.bluetooth.bluez: Aborting device discovery due to offline Bluetooth Adapter
|
||||
Fri Nov 21 18:05:07 2025 1763708707540 Debug: Aborting device discovery due to offline Bluetooth Adapter
|
||||
^C"SIGINT"
|
||||
Fri Nov 21 18:05:21 2025 1763708721033 Debug: devices/bluetooth.cpp virtual bool bluetooth::handleSignal(int) "SIGINT"
|
||||
```
|
||||
|
||||
Check if Bluetooth is blocked/down:
|
||||
```
|
||||
$ rfkill list
|
||||
0: hci0: Bluetooth
|
||||
Soft blocked: yes
|
||||
Hard blocked: no
|
||||
1: phy0: Wireless LAN
|
||||
Soft blocked: no
|
||||
Hard blocked: no
|
||||
```
|
||||
```
|
||||
$ hciconfig -a
|
||||
hci0: Type: Primary Bus: UART
|
||||
BD Address: B8:27:EB:A2:85:70 ACL MTU: 1021:8 SCO MTU: 64:1
|
||||
DOWN
|
||||
RX bytes:3629 acl:0 sco:0 events:280 errors:0
|
||||
TX bytes:48392 acl:0 sco:0 commands:280 errors:0
|
||||
Features: 0xbf 0xfe 0xcf 0xfe 0xdb 0xff 0x7b 0x87
|
||||
Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
|
||||
Link policy: RSWITCH SNIFF
|
||||
Link mode: PERIPHERAL ACCEPT
|
||||
```
|
||||
|
||||
Unblock Bluetooth:
|
||||
|
||||
```
|
||||
sudo rfkill unblock bluetooth
|
||||
```
|
||||
|
||||
|
||||
#### Test your installation
|
||||
It is now time to check everything's fine
|
||||
|
||||
`./qdomyos-zwift -no-gui -heart-service`
|
||||
`sudo ./qdomyos-zwift-64bit -no-gui -heart-service`
|
||||
|
||||

|
||||
|
||||
Test your access from your fitness device.
|
||||
|
||||
Check logs to see if it's running:
|
||||
```
|
||||
journalctl -u qz.service -f
|
||||
```
|
||||
|
||||
#### Update QZ config file
|
||||
|
||||
Running headless you need to update `/root/.config/'Roberto Viola'/qDomyos-Zwift.conf` with specific settings for your set up. If you already have it working on an iPhone/Android, follow this guide to deploy QZ with the UI, replicate the settings in the UI, check everything works, then take a copy of `/root/.config/'Roberto Viola'/qDomyos-Zwift.conf` to use with the headless deployment.
|
||||
|
||||
For my set up, I add:
|
||||
|
||||
Nordictrack C1650:
|
||||
```
|
||||
norditrack_s25_treadmill=true
|
||||
proformtreadmillip=172.31.2.36
|
||||
```
|
||||
Zwift specific options (auto inclination not there yet in the Raspberry Pi version):
|
||||
```
|
||||
zwift_api_autoinclination=true
|
||||
zwift_inclination_gain=1
|
||||
zwift_inclination_offset=0
|
||||
zwift_username=user@myemail.com
|
||||
zwift_password=Password1
|
||||
```
|
||||
|
||||
Check it works:
|
||||
```
|
||||
sudo ./qdomyos-zwift-64bit -no-gui -no-console -no-log
|
||||
```
|
||||
|
||||
#### Automate QDOMYOS-ZWIFT at startup
|
||||
|
||||
You might want to have QDOMYOS-ZWIFT to start automatically at boot time.
|
||||
|
||||
Let's create a systemd service that we'll enable at boot sequence.
|
||||
Let's create a systemd service that we'll enable at boot sequence. **Update ExecStart with the path and full name with commandline options for your qz binary. Update ExecStop with the full name of the binary.**
|
||||
|
||||
`sudo vi /lib/systemd/system/qz.service`
|
||||
|
||||
@@ -325,7 +423,7 @@ sudo tail -f /var/log/qz-treadmill-monitor.log
|
||||
|
||||
### (optional) Enable overlay FS
|
||||
|
||||
Once that everything is working as expected, and if you dedicate your Raspberry pi to this usage, you might want to enable the read-only overlay FS.
|
||||
Once that everything is working as expected, and if you dedicate your Raspberry Pi to this usage, you might want to enable the read-only overlay FS.
|
||||
|
||||
By enabling the overlay read-only system, your SD card will be read-only only and every file written will be to RAM.
|
||||
Then at each reboot the RAM is erased and you'll revert to the initial status of the overlay file-system.
|
||||
@@ -350,7 +448,19 @@ Reboot immediately.
|
||||
|
||||
## Other tricks
|
||||
|
||||
I use some [3m magic scratches](https://www.amazon.fr/Command-Languettes-Accrochage-Tableaux-Larges/dp/B00X7792IE/ref=sr_1_5?dchild=1&keywords=accroche+tableau&qid=1616515278&sr=8-5) to attach my raspberry to my bike.
|
||||
I use the USB port from the bike console (always powered as long as the bike is plugged to main), maximum power is 500mA and this is enough for the raspberry.
|
||||
I use some [3m magic scratches](https://www.amazon.fr/Command-Languettes-Accrochage-Tableaux-Larges/dp/B00X7792IE/ref=sr_1_5?dchild=1&keywords=accroche+tableau&qid=1616515278&sr=8-5) to attach my Raspberry to my bike.
|
||||
I use the USB port from the bike console (always powered as long as the bike is plugged to main), maximum power is 500mA and this is enough for the Raspberry.
|
||||
|
||||
You can easily remove the Raspberry Pi from the bike if required.
|
||||
|
||||
|
||||
## Trouobleshooting QZ on RPI
|
||||
|
||||
Run qz as root
|
||||
|
||||
For Zwift, check Zwift detects QZ. Check bluetooth
|
||||
|
||||
If Zwift isn't detecting speed from your exercise device, double check your .conf is correct. If you're not sure, Check the setup works using iPhone/Android phone, then replicate the settings by using Raspberry Pi Desktop OS and qz -qml to view the QZ UI. Change settings to match working iPhone/Android.
|
||||
|
||||
|
||||
|
||||
You can easily remove the raspberry pi from the bike if required.
|
||||
|
||||
25
docs/workout-editor.md
Normal file
25
docs/workout-editor.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Workout Editor
|
||||
|
||||
The Workout Editor lets you create multi-device training sessions without leaving QZ.
|
||||
|
||||
## Open the Editor
|
||||
- Drawer → Workout Editor
|
||||
- Select the target device profile (treadmill, bike, elliptical, rower).
|
||||
|
||||
## Build Intervals
|
||||
- Every interval exposes the parameters supported by the selected device.
|
||||
- Use **Add Interval**, **Copy**, **Up/Down**, or **Del** to manage the timeline.
|
||||
- Select a block of consecutive intervals and hit **Repeat Selection** to clone it quickly (perfect for repeat sets like work/rest pairs).
|
||||
- Toggle **Show advanced parameters** to edit cadence targets, Peloton levels, heart-rate limits, GPS metadata, etc.
|
||||
- The Chart.js preview updates automatically while you edit.
|
||||
|
||||
## Load or Save Programs
|
||||
- **Load** imports any `.xml` plan from `training/`.
|
||||
- **Save** writes the XML back into the same folder (name is sanitised automatically).
|
||||
- **Save & Start** persists the file and immediately queues it for playback.
|
||||
- Existing files trigger an overwrite confirmation.
|
||||
|
||||
## Tips
|
||||
- Durations must follow `hh:mm:ss` format.
|
||||
- Speed/incline units follow the global miles setting.
|
||||
- Saved workouts appear inside the regular “Open Train Program” list.
|
||||
@@ -25,6 +25,11 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
height: 48
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: title
|
||||
Accessible.description: expanded ? "Expanded" : "Collapsed"
|
||||
Accessible.onPressAction: toggle()
|
||||
|
||||
Rectangle {
|
||||
id: indicatRect
|
||||
x: 16; y: 20
|
||||
|
||||
@@ -9,6 +9,7 @@ ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
property int chart_display_mode: 0
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
@@ -19,6 +20,9 @@ ColumnLayout {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
} else if (loadRequest.status === WebView.LoadSucceededStatus) {
|
||||
// Send chart display mode to the web view
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
@@ -28,4 +32,22 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes in chart display mode setting
|
||||
Connections {
|
||||
target: settings
|
||||
function onChart_display_modeChanged() {
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
|
||||
function sendDisplayModeToWebView() {
|
||||
if (webView.loading === false) {
|
||||
webView.runJavaScript("
|
||||
if (window.setChartDisplayMode) {
|
||||
window.setChartDisplayMode(" + settings.chart_display_mode + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
Binary file not shown.
@@ -6,9 +6,10 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -49,9 +50,22 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -88,8 +102,11 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -99,6 +116,60 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -224,6 +295,21 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -51,6 +54,17 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
Binary file not shown.
@@ -6,9 +6,10 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -49,9 +50,22 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -88,8 +102,11 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -99,6 +116,60 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -224,6 +295,21 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -51,6 +54,17 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
@@ -6,11 +6,11 @@
|
||||
<dict>
|
||||
<key>Headers/ConnectIQ.h</key>
|
||||
<data>
|
||||
yih4e2KjbC/GqavxdCZ3xQ4mHmA=
|
||||
oktDCwqbdQQg6rdcptAN5TGhUZs=
|
||||
</data>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<data>
|
||||
NDlj8k5C84UPFmD+qEMz2WcZloY=
|
||||
CMQ9wDp2PKaw9dRd8NBYpX9xkzE=
|
||||
</data>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
<data>
|
||||
@@ -22,11 +22,11 @@
|
||||
</data>
|
||||
<key>Headers/IQDevice.h</key>
|
||||
<data>
|
||||
bl545C/cu0mw2KlRmzojKmHPom0=
|
||||
a4hkgIut7ETtkOJXPkn/nGElEYg=
|
||||
</data>
|
||||
<key>Info.plist</key>
|
||||
<data>
|
||||
YUOCJU/YBLc4CRWV1z8JHDjCx8M=
|
||||
LeO8CbXcC4FrKgyl2zDm7R7nOj0=
|
||||
</data>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<data>
|
||||
@@ -300,14 +300,14 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
kAenemss8n98vVLi54JqBUtGwaL1/i+HSejFBZgawHA=
|
||||
E2QDme6rWC+CJc/kKtxIVSpPzbE4ArUwNagnLG6Nxis=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
bSRRooQ0FKFr3BgrFolAnkU402889YFHrH+6EEca3cg=
|
||||
KhyZorkoK2Qipuzee5aE5ENCarHR+Ni21GdxCV3FQ0s=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
@@ -328,7 +328,7 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA=
|
||||
Xx+4dhu0JD6w2pd9UMvLXukYVQfKzaLJhU0paDUQyls=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Modules/module.modulemap</key>
|
||||
|
||||
@@ -92,7 +92,7 @@ class BluetoothHandler : public QObject
|
||||
void onKeyPressed(int keyCode)
|
||||
{
|
||||
qDebug() << "Key pressed:" << keyCode;
|
||||
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == BIKE) {
|
||||
if (keyCode == 115) // up
|
||||
((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() + 1);
|
||||
else if (keyCode == 114) // down
|
||||
|
||||
@@ -13,22 +13,32 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +246,7 @@ ColumnLayout {
|
||||
elevationGain = elevationGain + (pathController.geopath.coordinateAt(i).altitude - pathController.geopath.coordinateAt(i-1).altitude)
|
||||
lines[i] = pathController.geopath.coordinateAt(i)
|
||||
}
|
||||
distance.text = "Distance " + (pathController.geopath.length() / 1000.0).toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
|
||||
distance.text = "Distance " + pathController.distance.toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -263,7 +273,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'gpx')
|
||||
fileDialogTrainProgram.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
68
src/Home.qml
68
src/Home.qml
@@ -14,6 +14,10 @@ HomeForm {
|
||||
width: parent.fill
|
||||
height: parent.fill
|
||||
color: settings.theme_background_color
|
||||
|
||||
// VoiceOver accessibility - ignore decorative background
|
||||
Accessible.role: Accessible.Pane
|
||||
Accessible.ignored: true
|
||||
}
|
||||
signal start_clicked;
|
||||
signal stop_clicked;
|
||||
@@ -72,7 +76,19 @@ HomeForm {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("New lap started!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: stopConfirmationDialog
|
||||
text: qsTr("Stop Workout")
|
||||
informativeText: qsTr("Do you really want to stop the current workout?")
|
||||
buttons: (MessageDialog.Yes | MessageDialog.No)
|
||||
onYesClicked: {
|
||||
close();
|
||||
inner_stop();
|
||||
}
|
||||
onNoClicked: close()
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -141,7 +157,11 @@ HomeForm {
|
||||
|
||||
start.onClicked: { start_clicked(); }
|
||||
stop.onClicked: {
|
||||
inner_stop();
|
||||
if (rootItem.confirmStopEnabled()) {
|
||||
stopConfirmationDialog.open();
|
||||
} else {
|
||||
inner_stop();
|
||||
}
|
||||
}
|
||||
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }
|
||||
|
||||
@@ -169,6 +189,8 @@ HomeForm {
|
||||
gridView.leftMargin = (parent.width % cellWidth) / 2;
|
||||
}
|
||||
|
||||
Accessible.ignored: true
|
||||
|
||||
delegate: Item {
|
||||
id: id1
|
||||
width: 170 * settings.ui_zoom / 100
|
||||
@@ -177,6 +199,12 @@ HomeForm {
|
||||
visible: visibleItem
|
||||
Component.onCompleted: console.log("completed " + objectName)
|
||||
|
||||
// VoiceOver accessibility support
|
||||
Accessible.role: largeButton ? Accessible.Button : (writable ? Accessible.Pane : Accessible.StaticText)
|
||||
Accessible.name: name + (largeButton ? "" : (": " + value))
|
||||
Accessible.description: largeButton ? largeButtonLabel : (secondLine !== "" ? secondLine : (writable ? qsTr("Adjustable. Current value: ") + value : qsTr("Current value: ") + value))
|
||||
Accessible.focusable: true
|
||||
|
||||
Behavior on x {
|
||||
enabled: id1.state != "active"
|
||||
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
|
||||
@@ -210,6 +238,9 @@ HomeForm {
|
||||
border.color: (settings.theme_tile_shadow_enabled ? settings.theme_tile_shadow_color : settings.theme_tile_background_color)
|
||||
color: settings.theme_tile_background_color
|
||||
id: rect
|
||||
|
||||
// Ignore for VoiceOver - decorative background only
|
||||
Accessible.ignored: true
|
||||
}
|
||||
|
||||
DropShadow {
|
||||
@@ -240,6 +271,9 @@ HomeForm {
|
||||
height: 48 * settings.ui_zoom / 100
|
||||
source: icon
|
||||
visible: settings.theme_tile_icon_enabled && !largeButton
|
||||
|
||||
// Ignore for VoiceOver - decorative only
|
||||
Accessible.ignored: true
|
||||
}
|
||||
Text {
|
||||
objectName: "value"
|
||||
@@ -254,6 +288,9 @@ HomeForm {
|
||||
font.pointSize: valueFontSize * settings.ui_zoom / 100
|
||||
font.bold: true
|
||||
visible: !largeButton
|
||||
|
||||
// Ignore for VoiceOver - parent Item handles accessibility
|
||||
Accessible.ignored: true
|
||||
}
|
||||
Text {
|
||||
objectName: "secondLine"
|
||||
@@ -269,6 +306,9 @@ HomeForm {
|
||||
font.pointSize: settings.theme_tile_secondline_textsize * settings.ui_zoom / 100
|
||||
font.bold: false
|
||||
visible: !largeButton
|
||||
|
||||
// Ignore for VoiceOver - parent Item handles accessibility
|
||||
Accessible.ignored: true
|
||||
}
|
||||
Text {
|
||||
id: myText
|
||||
@@ -283,6 +323,9 @@ HomeForm {
|
||||
anchors.leftMargin: 55 * settings.ui_zoom / 100
|
||||
anchors.topMargin: 20 * settings.ui_zoom / 100
|
||||
visible: !largeButton
|
||||
|
||||
// Ignore for VoiceOver - parent Item handles accessibility
|
||||
Accessible.ignored: true
|
||||
}
|
||||
RoundButton {
|
||||
objectName: minusName
|
||||
@@ -295,6 +338,13 @@ HomeForm {
|
||||
anchors.leftMargin: 2
|
||||
width: 48 * settings.ui_zoom / 100
|
||||
height: 48 * settings.ui_zoom / 100
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Decrease ") + name
|
||||
Accessible.description: qsTr("Decrease the value of ") + name
|
||||
Accessible.focusable: true
|
||||
Accessible.onPressAction: { minus_clicked(objectName) }
|
||||
}
|
||||
RoundButton {
|
||||
autoRepeat: true
|
||||
@@ -307,6 +357,13 @@ HomeForm {
|
||||
anchors.rightMargin: 2
|
||||
width: 48 * settings.ui_zoom / 100
|
||||
height: 48 * settings.ui_zoom / 100
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Increase ") + name
|
||||
Accessible.description: qsTr("Increase the value of ") + name
|
||||
Accessible.focusable: true
|
||||
Accessible.onPressAction: { plus_clicked(objectName) }
|
||||
}
|
||||
RoundButton {
|
||||
autoRepeat: true
|
||||
@@ -320,6 +377,13 @@ HomeForm {
|
||||
radius: 20
|
||||
}
|
||||
font.pointSize: 20 * settings.ui_zoom / 100
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: largeButtonLabel
|
||||
Accessible.description: name + ": " + largeButtonLabel
|
||||
Accessible.focusable: true
|
||||
Accessible.onPressAction: { largeButton_clicked(objectName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ Page {
|
||||
title: qsTr("QZ Fitness")
|
||||
id: page
|
||||
|
||||
// VoiceOver accessibility - ignore Page itself, only children are accessible
|
||||
Accessible.ignored: true
|
||||
|
||||
property alias start: start
|
||||
property alias stop: stop
|
||||
property alias lap: lap
|
||||
@@ -39,6 +42,8 @@ Page {
|
||||
width: 50
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -47,10 +52,13 @@ Page {
|
||||
height: row.height
|
||||
spacing: 0
|
||||
padding: 0
|
||||
Accessible.ignored: true
|
||||
|
||||
Rectangle {
|
||||
width: 50
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
Image {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -60,6 +68,12 @@ Page {
|
||||
source: "icons/icons/bluetooth-icon.png"
|
||||
enabled: rootItem.device
|
||||
smooth: true
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Indicator
|
||||
Accessible.name: qsTr("Bluetooth connection")
|
||||
Accessible.description: rootItem.device ? qsTr("Device connected") : qsTr("Device not connected")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: treadmill_connection
|
||||
@@ -74,6 +88,7 @@ Page {
|
||||
height: row.height - 76
|
||||
source: rootItem.signal
|
||||
smooth: true
|
||||
Accessible.ignored: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +97,8 @@ Page {
|
||||
width: 120
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
RoundButton {
|
||||
icon.source: rootItem.startIcon
|
||||
icon.height: row.height - 54
|
||||
@@ -91,6 +108,12 @@ Page {
|
||||
id: start
|
||||
width: 120
|
||||
height: row.height - 4
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: rootItem.startText
|
||||
Accessible.description: qsTr("Start workout")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: start
|
||||
@@ -104,6 +127,7 @@ Page {
|
||||
width: 120
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
RoundButton {
|
||||
icon.source: rootItem.stopIcon
|
||||
@@ -114,6 +138,12 @@ Page {
|
||||
id: stop
|
||||
width: 120
|
||||
height: row.height - 4
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: rootItem.stopText
|
||||
Accessible.description: qsTr("Stop workout")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: stop
|
||||
@@ -128,6 +158,8 @@ Page {
|
||||
width: 50
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
RoundButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
id: lap
|
||||
@@ -138,6 +170,12 @@ Page {
|
||||
icon.height: 48
|
||||
enabled: rootItem.lap
|
||||
smooth: true
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Lap")
|
||||
Accessible.description: qsTr("Record a new lap")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: lap
|
||||
@@ -165,7 +203,7 @@ Page {
|
||||
width: parent.width
|
||||
anchors.top: row1.bottom
|
||||
anchors.topMargin: 30
|
||||
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) bluetooth and bluetooth permission MUST be on<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
|
||||
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) both Bluetooth and Bluetooth permissions MUST be enabled<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
|
||||
wrapMode: Label.WordWrap
|
||||
visible: rootItem.labelHelp
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ ColumnLayout {
|
||||
Layout.fillWidth: true;
|
||||
height: 48
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: title
|
||||
Accessible.description: expanded ? "Expanded" : "Collapsed"
|
||||
Accessible.onPressAction: toggle()
|
||||
|
||||
Rectangle{
|
||||
id:indicatRect
|
||||
x: 16; y: 20
|
||||
|
||||
@@ -42,11 +42,27 @@ class PathController : public QObject {
|
||||
|
||||
void centerChanged() W_SIGNAL(centerChanged)
|
||||
|
||||
double distance() const {
|
||||
return mDistance;
|
||||
}
|
||||
|
||||
void setDistance(double distance) {
|
||||
if (qFuzzyCompare(distance, mDistance)) {
|
||||
return;
|
||||
}
|
||||
mDistance = distance;
|
||||
emit distanceChanged();
|
||||
}
|
||||
|
||||
void distanceChanged() W_SIGNAL(distanceChanged)
|
||||
|
||||
private : QGeoPath mGeoPath;
|
||||
QGeoCoordinate mCenter;
|
||||
double mDistance = 0.0;
|
||||
|
||||
W_PROPERTY(QGeoPath, geopath READ geoPath WRITE setGeoPath NOTIFY geopathChanged)
|
||||
W_PROPERTY(QGeoCoordinate, center READ center WRITE setCenter NOTIFY centerChanged)
|
||||
W_PROPERTY(double, distance READ distance WRITE setDistance NOTIFY distanceChanged)
|
||||
};
|
||||
|
||||
#endif // APPLICATION_PATHCONTROLLER_H
|
||||
|
||||
51
src/PreviewChart.qml
Normal file
51
src/PreviewChart.qml
Normal file
@@ -0,0 +1,51 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal popupclose()
|
||||
id: column1
|
||||
spacing: 10
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/previewchart/chart.htm"
|
||||
visible: true
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: chartJscheckStartFromWeb
|
||||
interval: 200; running: true; repeat: true
|
||||
onTriggered: {if(rootItem.startRequested) {rootItem.startRequested = false; rootItem.stopRequested = false; stackView.pop(); }}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeButton
|
||||
height: 50
|
||||
width: parent.width
|
||||
text: "Close"
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
popupclose();
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
headerToolbar.visible = true;
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,28 @@ import QtQuick.Dialogs 1.0
|
||||
|
||||
ColumnLayout {
|
||||
signal loadSettings(url name)
|
||||
FileDialog {
|
||||
id: fileDialogSettings
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogSettings.fileUrl)
|
||||
loadSettings(fileDialogSettings.fileUrl)
|
||||
fileDialogSettings.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogSettings.close()
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
loadSettings(fileUrl)
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +116,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'settings')
|
||||
fileDialogSettings.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import QtQuick 2.0
|
||||
import AndroidStatusBar 1.0
|
||||
import QtQuick.Window 2.12
|
||||
|
||||
/**
|
||||
* adapted from StackOverflow:
|
||||
@@ -29,7 +31,9 @@ ListView {
|
||||
z: Infinity
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 10
|
||||
anchors.bottomMargin: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ?
|
||||
((Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.navigationBarHeight + 10 : 10) : 10
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
interactive: false
|
||||
|
||||
@@ -11,22 +11,32 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +74,12 @@ ColumnLayout {
|
||||
id: filterField
|
||||
onTextChanged: updateFilter()
|
||||
}
|
||||
Button {
|
||||
anchors.left: mainRect.right
|
||||
anchors.leftMargin: 5
|
||||
text: "←"
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
Button {
|
||||
anchors.left: mainRect.right
|
||||
anchors.leftMargin: 5
|
||||
text: "←"
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
@@ -85,10 +95,10 @@ ColumnLayout {
|
||||
id: folderModel
|
||||
nameFilters: ["*.xml", "*.zwo"]
|
||||
folder: "file://" + rootItem.getWritableAppDir() + 'training'
|
||||
showDotAndDotDot: false
|
||||
showDotAndDotDot: false
|
||||
showDirs: true
|
||||
sortField: "Name"
|
||||
showDirsFirst: true
|
||||
sortField: "Name"
|
||||
showDirsFirst: true
|
||||
}
|
||||
model: folderModel
|
||||
delegate: Component {
|
||||
@@ -96,7 +106,7 @@ ColumnLayout {
|
||||
property alias textColor: fileTextBox.color
|
||||
width: parent.width
|
||||
height: 40
|
||||
color: Material.backgroundColor
|
||||
color: Material.backgroundColor
|
||||
z: 1
|
||||
Item {
|
||||
id: root
|
||||
@@ -135,12 +145,12 @@ ColumnLayout {
|
||||
console.log('onclicked ' + index+ " count "+list.count);
|
||||
if (index == list.currentIndex) {
|
||||
let fileUrl = folderModel.get(list.currentIndex, 'fileUrl') || folderModel.get(list.currentIndex, 'fileURL');
|
||||
if (fileUrl && !folderModel.isFolder(list.currentIndex)) {
|
||||
if (fileUrl && !folderModel.isFolder(list.currentIndex)) {
|
||||
trainprogram_open_clicked(fileUrl);
|
||||
popup.open()
|
||||
} else {
|
||||
folderModel.folder = fileURL
|
||||
}
|
||||
} else {
|
||||
folderModel.folder = fileURL
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (list.currentItem)
|
||||
@@ -296,7 +306,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
|
||||
fileDialogTrainProgram.visible = true
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
349
src/TrainingProgramsListJS.qml
Normal file
349
src/TrainingProgramsListJS.qml
Normal file
@@ -0,0 +1,349 @@
|
||||
import QtQuick 2.7
|
||||
import Qt.labs.folderlistmodel 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import QtQuick.Dialogs 1.0
|
||||
import Qt.labs.settings 1.0
|
||||
import Qt.labs.platform 1.1
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
signal trainprogram_autostart_requested()
|
||||
|
||||
property url pendingWorkoutUrl: ""
|
||||
|
||||
Settings {
|
||||
id: settings
|
||||
property real ftp: 200.0
|
||||
}
|
||||
|
||||
property var selectedFileUrl: ""
|
||||
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
var chosenFile = fileDialog.fileUrl || fileDialog.file || (fileDialog.fileUrls && fileDialog.fileUrls.length > 0 ? fileDialog.fileUrls[0] : "")
|
||||
console.log("You chose: " + chosenFile)
|
||||
selectedFileUrl = chosenFile
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(chosenFile)
|
||||
} else {
|
||||
trainprogram_open_clicked(chosenFile)
|
||||
}
|
||||
close()
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
initialItem: masterView
|
||||
|
||||
// MASTER VIEW - Lista Workout
|
||||
Component {
|
||||
id: masterView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: "Filter"
|
||||
color: "white"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: filterField
|
||||
Layout.fillWidth: true
|
||||
|
||||
function updateFilter() {
|
||||
var text = filterField.text
|
||||
var filter = "*"
|
||||
for(var i = 0; i<text.length; i++)
|
||||
filter+= "[%1%2]".arg(text[i].toUpperCase()).arg(text[i].toLowerCase())
|
||||
filter+="*"
|
||||
folderModel.nameFilters = [filter + ".zwo", filter + ".xml"]
|
||||
}
|
||||
|
||||
onTextChanged: updateFilter()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "←"
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
id: list
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
nameFilters: ["*.xml", "*.zwo"]
|
||||
folder: "file://" + rootItem.getWritableAppDir() + 'training'
|
||||
showDotAndDotDot: false
|
||||
showDirs: true
|
||||
sortField: "Name"
|
||||
showDirsFirst: true
|
||||
}
|
||||
|
||||
model: folderModel
|
||||
|
||||
delegate: Component {
|
||||
Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 50
|
||||
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
id: fileIcon
|
||||
text: folderModel.isFolder(index) ? "📁" : "📄"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
|
||||
Text {
|
||||
id: fileName
|
||||
Layout.fillWidth: true
|
||||
text: !folderModel.isFolder(index) ?
|
||||
folderModel.get(index, "fileName").substring(0, folderModel.get(index, "fileName").length-4) :
|
||||
folderModel.get(index, "fileName")
|
||||
color: folderModel.isFolder(index) ? Material.color(Material.Orange) : "white"
|
||||
font.pixelSize: 16
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "›"
|
||||
font.pixelSize: 24
|
||||
color: Material.color(Material.Grey)
|
||||
visible: !ListView.isCurrentItem
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = index
|
||||
let fileUrl = folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL');
|
||||
|
||||
if (folderModel.isFolder(index)) {
|
||||
// Navigate to folder
|
||||
folderModel.folder = fileUrl
|
||||
} else if (fileUrl) {
|
||||
// Load preview and show detail view
|
||||
console.log('Loading preview for: ' + fileUrl);
|
||||
trainprogram_preview(fileUrl)
|
||||
pendingWorkoutUrl = fileUrl
|
||||
|
||||
// Wait for preview to load then push detail view
|
||||
detailViewTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
text: "Other folders"
|
||||
onClicked: {
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to push detail view after preview loads
|
||||
Timer {
|
||||
id: detailViewTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
stackView.push(detailView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DETAIL VIEW - Anteprima Workout
|
||||
Component {
|
||||
id: detailView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 10
|
||||
|
||||
// Header con pulsanti
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 5
|
||||
spacing: 10
|
||||
|
||||
Button {
|
||||
text: "← Back"
|
||||
onClicked: stackView.pop()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: "Start Workout"
|
||||
highlighted: true
|
||||
Material.background: Material.Green
|
||||
onClicked: {
|
||||
trainprogram_open_clicked(pendingWorkoutUrl)
|
||||
trainprogram_autostart_requested()
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Descrizione workout
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 10
|
||||
text: rootItem.previewWorkoutDescription
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
color: "white"
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
text: rootItem.previewWorkoutTags
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WordWrap
|
||||
color: Material.color(Material.Grey, Material.Shade400)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// WebView con grafico
|
||||
WebView {
|
||||
id: previewWebView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/workoutpreview/preview.html"
|
||||
|
||||
Component.onCompleted: {
|
||||
// Update workout after a short delay to ensure data is loaded
|
||||
updateTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: previewWebView.updateWorkout()
|
||||
}
|
||||
|
||||
function updateWorkout() {
|
||||
if (!rootItem.preview_workout_points) return;
|
||||
|
||||
// Build arrays for the workout data
|
||||
var watts = [];
|
||||
var speed = [];
|
||||
var inclination = [];
|
||||
var resistance = [];
|
||||
var cadence = [];
|
||||
|
||||
var hasWatts = false;
|
||||
var hasSpeed = false;
|
||||
var hasInclination = false;
|
||||
var hasResistance = false;
|
||||
var hasCadence = false;
|
||||
|
||||
for (var i = 0; i < rootItem.preview_workout_points; i++) {
|
||||
if (rootItem.preview_workout_watt && rootItem.preview_workout_watt[i] !== undefined && rootItem.preview_workout_watt[i] > 0) {
|
||||
watts.push({ x: i, y: rootItem.preview_workout_watt[i] });
|
||||
hasWatts = true;
|
||||
}
|
||||
if (rootItem.preview_workout_speed && rootItem.preview_workout_speed[i] !== undefined && rootItem.preview_workout_speed[i] > 0) {
|
||||
speed.push({ x: i, y: rootItem.preview_workout_speed[i] });
|
||||
hasSpeed = true;
|
||||
}
|
||||
if (rootItem.preview_workout_inclination && rootItem.preview_workout_inclination[i] !== undefined && rootItem.preview_workout_inclination[i] > -200) {
|
||||
inclination.push({ x: i, y: rootItem.preview_workout_inclination[i] });
|
||||
hasInclination = true;
|
||||
}
|
||||
if (rootItem.preview_workout_resistance && rootItem.preview_workout_resistance[i] !== undefined && rootItem.preview_workout_resistance[i] >= 0) {
|
||||
resistance.push({ x: i, y: rootItem.preview_workout_resistance[i] });
|
||||
hasResistance = true;
|
||||
}
|
||||
if (rootItem.preview_workout_cadence && rootItem.preview_workout_cadence[i] !== undefined && rootItem.preview_workout_cadence[i] > 0) {
|
||||
cadence.push({ x: i, y: rootItem.preview_workout_cadence[i] });
|
||||
hasCadence = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device type based on available data
|
||||
var deviceType = 'bike'; // default
|
||||
|
||||
// Priority 1: If has resistance, it's a bike (regardless of inclination)
|
||||
if (hasResistance) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
// Priority 2: If has speed or inclination (without resistance), it's a treadmill
|
||||
else if (hasSpeed || hasInclination) {
|
||||
deviceType = 'treadmill';
|
||||
}
|
||||
// Priority 3: If has power or cadence (bike metrics), it's a bike
|
||||
else if (hasWatts || hasCadence) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
|
||||
// Call JavaScript function in the WebView
|
||||
var data = {
|
||||
points: rootItem.preview_workout_points,
|
||||
watts: watts,
|
||||
speed: speed,
|
||||
inclination: inclination,
|
||||
resistance: resistance,
|
||||
cadence: cadence,
|
||||
deviceType: deviceType,
|
||||
miles_unit: settings.value("miles_unit", false)
|
||||
};
|
||||
|
||||
runJavaScript("if(window.setWorkoutData) window.setWorkoutData(" + JSON.stringify(data) + ");");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/WebIntervalsICUAuth.qml
Normal file
58
src/WebIntervalsICUAuth.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: true
|
||||
|
||||
WebView {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: !rootItem.generalPopupVisible
|
||||
url: rootItem.getIntervalsICUAuthUrl
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupIntervalsICUConnectedWeb
|
||||
parent: Overlay.overlay
|
||||
enabled: rootItem.generalPopupVisible
|
||||
onEnabledChanged: { if(rootItem.generalPopupVisible) popupIntervalsICUConnectedWeb.open() }
|
||||
onClosed: { rootItem.generalPopupVisible = false; }
|
||||
|
||||
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 Intervals.icu account is now connected!<br><br>When you will press STOP on QZ a file<br>will be automatically uploaded to Intervals.icu!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -845,7 +845,6 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 0.5;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
@@ -904,7 +903,6 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 1;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
|
||||
61
src/WorkoutEditor.qml
Normal file
61
src/WorkoutEditor.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.5
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property string title: qsTr("Workout Editor")
|
||||
property bool pageLoaded: false
|
||||
|
||||
signal closeRequested()
|
||||
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: portPoller
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: !root.pageLoaded
|
||||
onTriggered: {
|
||||
var port = settings.value("template_inner_QZWS_port", 0)
|
||||
if (!port) {
|
||||
return
|
||||
}
|
||||
var targetUrl = "http://localhost:" + port + "/workouteditor/index.html"
|
||||
if (webView.url !== targetUrl) {
|
||||
webView.url = targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
visible: root.pageLoaded
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.status === WebView.LoadSucceededStatus) {
|
||||
root.pageLoaded = true
|
||||
busy.visible = false
|
||||
busy.running = false
|
||||
portPoller.stop()
|
||||
} else if (loadRequest.status === WebView.LoadFailedStatus) {
|
||||
root.pageLoaded = false
|
||||
busy.visible = true
|
||||
busy.running = true
|
||||
portPoller.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
id: busy
|
||||
anchors.centerIn: parent
|
||||
visible: !root.pageLoaded
|
||||
running: !root.pageLoaded
|
||||
}
|
||||
|
||||
Component.onCompleted: portPoller.start()
|
||||
}
|
||||
71
src/WorkoutTypeTag.qml
Normal file
71
src/WorkoutTypeTag.qml
Normal file
@@ -0,0 +1,71 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string workoutSource: "QZ"
|
||||
property alias text: tagText.text
|
||||
|
||||
// Auto-size based on text
|
||||
width: tagText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
|
||||
// Color scheme based on workout source
|
||||
color: {
|
||||
switch(workoutSource.toUpperCase()) {
|
||||
case "PELOTON": return "#ff6b35"
|
||||
case "ZWIFT": return "#ff6900"
|
||||
case "ERG": return "#8bc34a"
|
||||
case "QZ": return "#2196f3"
|
||||
case "MANUAL": return "#757575"
|
||||
default: return "#9e9e9e"
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle border for better definition
|
||||
border.color: Qt.darker(color, 1.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: tagText
|
||||
anchors.centerIn: parent
|
||||
text: workoutSource.toUpperCase()
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: "Arial"
|
||||
}
|
||||
|
||||
// Subtle shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 1
|
||||
anchors.leftMargin: 1
|
||||
radius: parent.radius
|
||||
color: "#20000000"
|
||||
z: -1
|
||||
}
|
||||
|
||||
// Hover effect for interactivity feedback
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
910
src/WorkoutsHistory.qml
Normal file
910
src/WorkoutsHistory.qml
Normal file
@@ -0,0 +1,910 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtCharts 2.15
|
||||
import Qt.labs.calendar 1.0
|
||||
|
||||
Page {
|
||||
id: workoutHistoryPage
|
||||
|
||||
|
||||
// Signal for chart preview
|
||||
signal fitfile_preview_clicked(var url)
|
||||
|
||||
// Helper function to wrap text with emoji font only on Android
|
||||
function wrapEmoji(emoji) {
|
||||
return Qt.platform.os === "android" ?
|
||||
'<font face="' + fontManager.emojiFontFamily + '">' + emoji + '</font>' :
|
||||
emoji;
|
||||
}
|
||||
|
||||
// Sport type to icon mapping (using FIT_SPORT values)
|
||||
function getSportIcon(sport) {
|
||||
switch(parseInt(sport)) {
|
||||
case 1: // FIT_SPORT_RUNNING
|
||||
case 11: // FIT_SPORT_WALKING
|
||||
return "🏃"; // Running/Walking
|
||||
case 2: // FIT_SPORT_CYCLING
|
||||
return "🚴"; // Cycling
|
||||
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
|
||||
return "⭕"; // Elliptical
|
||||
case 15: // FIT_SPORT_ROWING
|
||||
return "🚣"; // Rowing
|
||||
case 84: // FIT_SPORT_JUMPROPE
|
||||
return "🪢"; // Jump Rope
|
||||
default:
|
||||
return "💪"; // Generic workout
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 60
|
||||
color: "#f5f5f5"
|
||||
|
||||
// Calendar Icon Button - positioned absolutely on the left
|
||||
Button {
|
||||
id: calendarButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: calendarButton.pressed ? "#e0e0e0" : "#f0f0f0"
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📅") :
|
||||
"📅"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 20
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
calendarPopup.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Title with filter status - centered
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Workout History"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: workoutModel && workoutModel.isDateFiltered ?
|
||||
"Filtered: " + workoutModel.filteredDate.toLocaleDateString() : ""
|
||||
font.pixelSize: 12
|
||||
color: "#666666"
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Filter Button - positioned absolutely on the right
|
||||
Button {
|
||||
id: clearFilterButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 12
|
||||
width: 100
|
||||
height: 36
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
|
||||
background: Rectangle {
|
||||
radius: 6
|
||||
color: clearFilterButton.pressed ? "#ff6666" : "#ff8888"
|
||||
border.color: "#ff4444"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "Clear Filter"
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.clearDateFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
BusyIndicator {
|
||||
id: loadingIndicator
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? (workoutModel.isLoading || workoutModel.isDatabaseProcessing) : false
|
||||
running: visible
|
||||
}
|
||||
|
||||
// Database processing message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? workoutModel.isDatabaseProcessing : false
|
||||
text: "Processing workout files...\nThis may take a few moments on first startup."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#666666"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
// Workout List
|
||||
ListView {
|
||||
id: workoutListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: streakBanner.visible ? streakBanner.height + 10 : 10
|
||||
model: workoutModel
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
onContentYChanged: {
|
||||
// Hide banner when scrolling down, show when at top
|
||||
streakBanner.visible = contentY <= 20
|
||||
}
|
||||
|
||||
delegate: SwipeDelegate {
|
||||
id: swipeDelegate
|
||||
width: parent.width
|
||||
height: 135
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Delegate data:", JSON.stringify({
|
||||
sport: sport,
|
||||
title: title,
|
||||
date: date,
|
||||
duration: duration,
|
||||
distance: distance,
|
||||
calories: calories,
|
||||
id: id
|
||||
}))
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: "#FF4444"
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 20
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🗑️") + " Delete" :
|
||||
"🗑️ Delete"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
color: "white"
|
||||
font.pixelSize: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipe.onCompleted: {
|
||||
// Show confirmation dialog
|
||||
confirmDialog.workoutId = model.id
|
||||
confirmDialog.workoutTitle = model.title
|
||||
confirmDialog.open()
|
||||
}
|
||||
|
||||
// Card-like container
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
radius: 10
|
||||
color: "white"
|
||||
border.color: "#e0e0e0"
|
||||
|
||||
// Workout Type Tag - positioned absolutely in top-right
|
||||
WorkoutTypeTag {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
workoutSource: workoutModel ? workoutModel.getWorkoutSource(model.id) : "QZ"
|
||||
}
|
||||
|
||||
// Action buttons - positioned absolutely in bottom-right
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
// Peloton URL button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.getWorkoutSource(model.id) === "PELOTON" &&
|
||||
workoutModel.getPelotonUrl(model.id) !== ""
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#ff8855" : "#ff6b35"
|
||||
radius: 6
|
||||
border.color: "#cc5529"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🌐") :
|
||||
"🌐"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.openPelotonUrl(model.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.hasTrainingProgram(model.id)
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#1976d2" : "#2196f3"
|
||||
radius: 6
|
||||
border.color: "#1565c0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📋") :
|
||||
"📋"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var success = workoutModel.loadTrainingProgram(model.id)
|
||||
if (success) {
|
||||
trainingProgramDialog.title = "Success"
|
||||
trainingProgramDialog.message = "Training program loaded successfully!"
|
||||
trainingProgramDialog.isSuccess = true
|
||||
} else {
|
||||
trainingProgramDialog.title = "Error"
|
||||
trainingProgramDialog.message = "Failed to load training program. Please check if the file exists."
|
||||
trainingProgramDialog.isSuccess = false
|
||||
}
|
||||
trainingProgramDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
// Sport icon
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji(getSportIcon(sport)) :
|
||||
getSportIcon(sport)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 32
|
||||
}
|
||||
}
|
||||
|
||||
// Workout info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
// Title row (without tag) with auto-scrolling
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 80 // Reserve space for tag
|
||||
Layout.preferredHeight: 24
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
text: title
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Auto-scroll animation for long titles
|
||||
SequentialAnimation on x {
|
||||
running: titleText.contentWidth > titleText.parent.width
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 1500 }
|
||||
NumberAnimation {
|
||||
from: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
to: 0
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 2000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date
|
||||
color: "#666666"
|
||||
}
|
||||
|
||||
// Stats row
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "⏱ " + duration
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "📏 " + distance.toFixed(2) + " km"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " + Math.round(calories) + " kcal" :
|
||||
"🔥 " + Math.round(calories) + " kcal"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
console.log("Workout clicked, ID:", model.id)
|
||||
|
||||
// Get workout details from the model
|
||||
var details = workoutModel.getWorkoutDetails(model.id)
|
||||
console.log("Workout details:", JSON.stringify(details))
|
||||
|
||||
// Emit signal with file URL for chart preview - same pattern as profiles.qml
|
||||
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
|
||||
// Convert to URL like profiles.qml does with FolderListModel
|
||||
var fileUrl = "file://" + details.filePath
|
||||
console.log("Converted to URL:", fileUrl)
|
||||
workoutHistoryPage.fitfile_preview_clicked(fileUrl)
|
||||
|
||||
// Push the ChartJsTest view
|
||||
stackView.push("PreviewChart.qml")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation Dialog
|
||||
Dialog {
|
||||
id: confirmDialog
|
||||
|
||||
property int workoutId
|
||||
property string workoutTitle
|
||||
|
||||
title: "Delete Workout"
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
Text {
|
||||
text: "Are you sure you want to delete '" + confirmDialog.workoutTitle + "'?"
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
workoutModel.deleteWorkout(confirmDialog.workoutId)
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
onRejected: {
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program Loading Dialog
|
||||
Dialog {
|
||||
id: trainingProgramDialog
|
||||
|
||||
property string message: ""
|
||||
property bool isSuccess: true
|
||||
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 8
|
||||
border.color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
border.width: 2
|
||||
}
|
||||
|
||||
header: Rectangle {
|
||||
height: 50
|
||||
color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
radius: 8
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: trainingProgramDialog.title
|
||||
color: "white"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
Layout.margins: 20
|
||||
Layout.preferredWidth: 300
|
||||
Layout.preferredHeight: 120
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " +
|
||||
wrapEmoji(trainingProgramDialog.isSuccess ? '✅' : '❌') +
|
||||
" " + trainingProgramDialog.message :
|
||||
"🔥 " + (trainingProgramDialog.isSuccess ? '✅ ' : '❌ ') + trainingProgramDialog.message
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streak Banner at the bottom
|
||||
Rectangle {
|
||||
id: streakBanner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 80
|
||||
visible: workoutModel
|
||||
|
||||
Behavior on visible {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Special pulsing effect for major milestones
|
||||
SequentialAnimation on opacity {
|
||||
running: workoutModel && workoutModel.currentStreak >= 30
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0.9; to: 1.0; duration: 1500; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { from: 1.0; to: 0.9; duration: 1500; easing.type: Easing.InOutSine }
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFD700" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#9932CC" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#FF1493" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF4500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF6347" : "#FF6B35"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFA500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#8A2BE2" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#DC143C" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF6B35" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF4500" : "#F7931E"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40FFFFFF" }
|
||||
GradientStop { position: 1.0; color: "#00FFFFFF" }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
// Current streak with count
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 15
|
||||
|
||||
// Fire emoji with animation
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("👑🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🎖️🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🦁🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🎊🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🏆🔥") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "👑🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🎖️🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🦁🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🎊🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🏆🔥" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 7 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 365 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Special sparkle effect for year achievement
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: 360; duration: 3000; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
|
||||
// Current streak count
|
||||
Text {
|
||||
text: workoutModel ? workoutModel.currentStreak + " day" + (workoutModel.currentStreak !== 1 ? "s" : "") + " streak" : ""
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: "white"
|
||||
visible: workoutModel
|
||||
}
|
||||
|
||||
// Another fire emoji
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("🔥👑") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🔥🎖️") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🔥🦁") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🔥🎊") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🔥🏆") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "🔥👑" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🔥🎖️" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🔥🦁" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🔥🎊" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🔥🏆" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 365 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Counter-rotation for variety
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: -360; duration: 3500; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Motivational message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? workoutModel.streakMessage : ""
|
||||
font.pixelSize: 14
|
||||
font.italic: true
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.streakMessage !== ""
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
// Best streak (smaller text)
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? "Personal best: " + workoutModel.longestStreak + " day" + (workoutModel.longestStreak !== 1 ? "s" : "") : ""
|
||||
font.pixelSize: 12
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.longestStreak > workoutModel.currentStreak && workoutModel.longestStreak > 0
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle shadow effect at the top
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40000000" }
|
||||
GradientStop { position: 1.0; color: "#00000000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Popup
|
||||
Popup {
|
||||
id: calendarPopup
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.min(parent.width * 0.9, 400)
|
||||
height: Math.min(parent.height * 0.8, 500)
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: {
|
||||
// Refresh workout dates when calendar opens
|
||||
if (workoutModel) {
|
||||
calendar.workoutDates = workoutModel.getWorkoutDates()
|
||||
console.log("Calendar opened, refreshed workout dates:", JSON.stringify(calendar.workoutDates))
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 12
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
|
||||
// Shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 2
|
||||
anchors.leftMargin: 2
|
||||
radius: parent.radius
|
||||
color: "#40000000"
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Calendar Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
text: "<"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() - 1, 1)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: calendar.selectedDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
text: ">"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Grid
|
||||
GridLayout {
|
||||
id: calendar
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
columns: 7
|
||||
|
||||
property date selectedDate: new Date()
|
||||
property var workoutDates: workoutModel ? workoutModel.getWorkoutDates() : []
|
||||
|
||||
// Debug: print workout dates when they change
|
||||
onWorkoutDatesChanged: {
|
||||
console.log("Calendar workout dates updated:", JSON.stringify(workoutDates))
|
||||
}
|
||||
|
||||
// Day headers
|
||||
Repeater {
|
||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
text: modelData
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: "#666666"
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar days
|
||||
Repeater {
|
||||
model: getCalendarDays()
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 40
|
||||
|
||||
property date dayDate: modelData.date
|
||||
property bool isCurrentMonth: modelData.currentMonth
|
||||
property bool hasWorkout: modelData.hasWorkout
|
||||
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
|
||||
color: {
|
||||
if (mouseArea.pressed) return "#e3f2fd"
|
||||
if (isToday) return "#bbdefb"
|
||||
if (!isCurrentMonth) return "#f5f5f5"
|
||||
return "white"
|
||||
}
|
||||
|
||||
border.color: isToday ? "#2196f3" : "#e0e0e0"
|
||||
border.width: isToday ? 2 : 1
|
||||
radius: 4
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: dayDate.getDate()
|
||||
color: isCurrentMonth ? "black" : "#cccccc"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
// Workout indicator dot
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: "#ff6b35"
|
||||
visible: hasWorkout
|
||||
border.width: 1
|
||||
border.color: "#cc5529"
|
||||
|
||||
// Debug: log when a dot should be visible
|
||||
Component.onCompleted: {
|
||||
if (hasWorkout) {
|
||||
console.log("Workout dot visible for date:", dayDate.toDateString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
if (isCurrentMonth) {
|
||||
var year = dayDate.getFullYear();
|
||||
var month = dayDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = dayDate.getDate();
|
||||
var dateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
workoutModel.setDateFilter(dateString);
|
||||
calendarPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "Close"
|
||||
onClicked: calendarPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript functions for calendar
|
||||
function getCalendarDays() {
|
||||
var days = []
|
||||
var firstDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth(), 1)
|
||||
var lastDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 0)
|
||||
var startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay()) // Go back to start of week
|
||||
|
||||
var workoutDates = calendar.workoutDates || []
|
||||
console.log("getCalendarDays: workoutDates received:", JSON.stringify(workoutDates))
|
||||
|
||||
// workoutDates is now a QStringList (array of strings in format "yyyy-MM-dd")
|
||||
var workoutDateStrings = workoutDates || []
|
||||
console.log("Final workout date strings:", JSON.stringify(workoutDateStrings))
|
||||
|
||||
for (var i = 0; i < 42; i++) { // 6 rows x 7 days
|
||||
var currentDate = new Date(startDate)
|
||||
currentDate.setDate(startDate.getDate() + i)
|
||||
|
||||
// Costruisci la stringa YYYY-MM-DD dai componenti della data locale per evitare problemi di fuso orario
|
||||
var year = currentDate.getFullYear();
|
||||
var month = currentDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = currentDate.getDate();
|
||||
var localDateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
var hasWorkout = workoutDateStrings.indexOf(localDateString) !== -1;
|
||||
if (hasWorkout) {
|
||||
// Questo console.log ora utilizza la stringa della data locale corretta per la corrispondenza
|
||||
console.log("Found workout match for:", localDateString);
|
||||
}
|
||||
|
||||
var isCurrentMonth = currentDate.getMonth() === calendar.selectedDate.getMonth()
|
||||
|
||||
days.push({
|
||||
date: currentDate,
|
||||
currentMonth: isCurrentMonth,
|
||||
hasWorkout: hasWorkout
|
||||
})
|
||||
}
|
||||
|
||||
console.log("getCalendarDays: returning", days.length, "days")
|
||||
return days
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.1" android:versionCode="1122" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.23" android:versionCode="1264" 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 -->
|
||||
|
||||
BIN
src/android/libs/arm64-v8a/libc++_shared.so
Executable file
BIN
src/android/libs/arm64-v8a/libc++_shared.so
Executable file
Binary file not shown.
BIN
src/android/libs/armeabi-v7a/libc++_shared.so
Executable file
BIN
src/android/libs/armeabi-v7a/libc++_shared.so
Executable file
Binary file not shown.
BIN
src/android/libs/x86/libc++_shared.so
Executable file
BIN
src/android/libs/x86/libc++_shared.so
Executable file
Binary file not shown.
BIN
src/android/libs/x86_64/libc++_shared.so
Executable file
BIN
src/android/libs/x86_64/libc++_shared.so
Executable file
Binary file not shown.
@@ -35,15 +35,21 @@ public class Ant {
|
||||
static boolean bikeRequest = false; // Added bike request flag
|
||||
static boolean garminKey = false;
|
||||
static boolean treadmill = false;
|
||||
static boolean technoGymGroupCycle = false;
|
||||
static int antBikeDeviceNumber = 0;
|
||||
static int antHeartDeviceNumber = 0;
|
||||
|
||||
// Updated antStart method with BikeRequest parameter at the end
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest) {
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
|
||||
QLog.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
bikeRequest = BikeRequest; // Set bike request flag
|
||||
technoGymGroupCycle = TechnoGymGroupCycle;
|
||||
antBikeDeviceNumber = AntBikeDeviceNumber;
|
||||
antHeartDeviceNumber = AntHeartDeviceNumber;
|
||||
activity = a;
|
||||
if(a != null)
|
||||
QLog.v(TAG, "antStart activity is valid");
|
||||
|
||||
@@ -12,6 +12,15 @@ import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnes
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentType;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.HeartRateDataSource;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.IRawPowerOnlyDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.ICalculatedPowerReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedSpeedReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedAccumulatedDistanceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.IRawSpeedAndDistanceDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc.ICalculatedCadenceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
|
||||
@@ -29,9 +38,17 @@ public class BikeChannelController {
|
||||
private Context context;
|
||||
private AntPlusFitnessEquipmentPcc fePcc = null;
|
||||
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
|
||||
private AntPlusBikePowerPcc powerPcc = null;
|
||||
private PccReleaseHandle<AntPlusBikePowerPcc> powerReleaseHandle = null;
|
||||
private AntPlusBikeSpeedDistancePcc speedCadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeSpeedDistancePcc> speedCadenceReleaseHandle = null;
|
||||
private AntPlusBikeCadencePcc cadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeCadencePcc> cadenceReleaseHandle = null;
|
||||
private boolean isConnected = false;
|
||||
private boolean isPowerConnected = false;
|
||||
private boolean isSpeedCadenceConnected = false;
|
||||
|
||||
// Bike data fields
|
||||
// Bike data fields - from fitness equipment
|
||||
public int cadence = 0; // Current cadence in RPM
|
||||
public int power = 0; // Current power in watts
|
||||
public BigDecimal speed = new BigDecimal(0); // Current speed in m/s
|
||||
@@ -42,6 +59,12 @@ public class BikeChannelController {
|
||||
public int heartRate = 0; // Heart rate from equipment
|
||||
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
|
||||
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
|
||||
|
||||
// Bike data fields - from dedicated sensors
|
||||
public int powerSensorPower = 0; // Power from dedicated power sensor
|
||||
public int speedSensorCadence = 0; // Cadence from speed/cadence sensor
|
||||
public BigDecimal speedSensorSpeed = new BigDecimal(0); // Speed from speed/cadence sensor
|
||||
public long speedSensorDistance = 0; // Distance from speed/cadence sensor
|
||||
|
||||
// Fitness equipment state receiver
|
||||
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
|
||||
@@ -63,9 +86,18 @@ public class BikeChannelController {
|
||||
}
|
||||
};
|
||||
|
||||
public BikeChannelController() {
|
||||
public BikeChannelController(boolean technoGymGroupCycle, int antBikeDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
|
||||
if (technoGymGroupCycle) {
|
||||
// For Technogym Group Cycle: disable openChannel, enable openPowerSensorChannel
|
||||
openPowerSensorChannel(antBikeDeviceNumber);
|
||||
} else {
|
||||
// Standard behavior: enable openChannel, disable openPowerSensorChannel
|
||||
openChannel();
|
||||
}
|
||||
|
||||
//openSpeedCadenceSensorChannel();
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
@@ -123,6 +155,106 @@ public class BikeChannelController {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public boolean openPowerSensorChannel(int deviceNumber) {
|
||||
// Request access to power sensor device (deviceNumber = 0 means first available)
|
||||
powerReleaseHandle = AntPlusBikePowerPcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusBikePowerPcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikePowerPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
powerPcc = result;
|
||||
isPowerConnected = true;
|
||||
QLog.d(TAG, "Connected to power sensor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToPowerSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Power Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Power Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Power Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Power Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Power Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Power Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized power sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Power Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isPowerConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean openSpeedCadenceSensorChannel() {
|
||||
// Request access to first available speed/cadence sensor device
|
||||
speedCadenceReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess((Activity)context, context,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeSpeedDistancePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
speedCadencePcc = result;
|
||||
isSpeedCadenceConnected = true;
|
||||
QLog.d(TAG, "Connected to speed/cadence sensor: " + result.getDeviceName());
|
||||
subscribeToSpeedCadenceSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Speed/Cadence Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Speed/Cadence Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Speed/Cadence Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Speed/Cadence Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized speed/cadence sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Speed/Cadence Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isSpeedCadenceConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
private void subscribeToBikeEvents() {
|
||||
if (fePcc != null) {
|
||||
// General fitness equipment data
|
||||
@@ -181,36 +313,181 @@ public class BikeChannelController {
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToPowerSensorEvents() {
|
||||
if (powerPcc != null) {
|
||||
// Subscribe to raw power-only data events
|
||||
powerPcc.subscribeRawPowerOnlyDataEvent(new IRawPowerOnlyDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawPowerOnlyData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
long powerOnlyUpdateEventCount, int instantaneousPower,
|
||||
long accumulatedPower) {
|
||||
if (instantaneousPower != -1) {
|
||||
powerSensorPower = instantaneousPower;
|
||||
QLog.d(TAG, "Power Sensor Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also subscribe to calculated power events
|
||||
powerPcc.subscribeCalculatedPowerEvent(new ICalculatedPowerReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedPower(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
AntPlusBikePowerPcc.DataSource dataSource,
|
||||
BigDecimal calculatedPower) {
|
||||
if (calculatedPower != null && calculatedPower.intValue() != -1) {
|
||||
powerSensorPower = calculatedPower.intValue();
|
||||
QLog.d(TAG, "Power Sensor Calculated Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToSpeedCadenceSensorEvents() {
|
||||
if (speedCadencePcc != null) {
|
||||
// 2.095m circumference = average 700cx23mm road tire
|
||||
BigDecimal wheelCircumference = new BigDecimal("2.095");
|
||||
|
||||
// Subscribe to calculated speed events
|
||||
speedCadencePcc.subscribeCalculatedSpeedEvent(new CalculatedSpeedReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedSpeed(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedSpeed) {
|
||||
if (calculatedSpeed != null && calculatedSpeed.doubleValue() > 0) {
|
||||
speedSensorSpeed = calculatedSpeed;
|
||||
QLog.d(TAG, "Speed Sensor Data - Speed: " + speedSensorSpeed + "m/s");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to calculated distance events
|
||||
speedCadencePcc.subscribeCalculatedAccumulatedDistanceEvent(new CalculatedAccumulatedDistanceReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedAccumulatedDistance(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedAccumulatedDistance) {
|
||||
if (calculatedAccumulatedDistance != null && calculatedAccumulatedDistance.longValue() > 0) {
|
||||
speedSensorDistance = calculatedAccumulatedDistance.longValue();
|
||||
QLog.d(TAG, "Speed Sensor Data - Distance: " + speedSensorDistance + "m");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to raw speed and distance data
|
||||
speedCadencePcc.subscribeRawSpeedAndDistanceDataEvent(new IRawSpeedAndDistanceDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawSpeedAndDistanceData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal timestampOfLastEvent, long cumulativeRevolutions) {
|
||||
QLog.d(TAG, "Speed/Distance Raw Data - Revs: " + cumulativeRevolutions + ", Time: " + timestampOfLastEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if this is a combined speed/cadence sensor
|
||||
if (speedCadencePcc.isSpeedAndCadenceCombinedSensor()) {
|
||||
// Connect to cadence functionality
|
||||
cadenceReleaseHandle = AntPlusBikeCadencePcc.requestAccess(
|
||||
(Activity)context, speedCadencePcc.getAntDeviceNumber(), 0, true,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeCadencePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeCadencePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
if (resultCode == RequestAccessResult.SUCCESS) {
|
||||
cadencePcc = result;
|
||||
cadencePcc.subscribeCalculatedCadenceEvent(new ICalculatedCadenceReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedCadence(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedCadence) {
|
||||
if (calculatedCadence != null && calculatedCadence.intValue() > 0) {
|
||||
speedSensorCadence = calculatedCadence.intValue();
|
||||
QLog.d(TAG, "Cadence Sensor Data - Cadence: " + speedSensorCadence + "rpm");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Cadence Sensor State Changed to: " + newDeviceState);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (releaseHandle != null) {
|
||||
releaseHandle.close();
|
||||
releaseHandle = null;
|
||||
}
|
||||
if (powerReleaseHandle != null) {
|
||||
powerReleaseHandle.close();
|
||||
powerReleaseHandle = null;
|
||||
}
|
||||
if (speedCadenceReleaseHandle != null) {
|
||||
speedCadenceReleaseHandle.close();
|
||||
speedCadenceReleaseHandle = null;
|
||||
}
|
||||
if (cadenceReleaseHandle != null) {
|
||||
cadenceReleaseHandle.close();
|
||||
cadenceReleaseHandle = null;
|
||||
}
|
||||
fePcc = null;
|
||||
powerPcc = null;
|
||||
speedCadencePcc = null;
|
||||
cadencePcc = null;
|
||||
isConnected = false;
|
||||
QLog.d(TAG, "Channel Closed");
|
||||
isPowerConnected = false;
|
||||
isSpeedCadenceConnected = false;
|
||||
QLog.d(TAG, "All Channels Closed");
|
||||
}
|
||||
|
||||
// Getter methods for bike data
|
||||
// Getter methods for bike data with sensor reconciliation
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
// Priority: 1) Fitness Equipment, 2) Speed/Cadence Sensor, 3) Power Sensor
|
||||
if (isConnected && cadence > 0) {
|
||||
return cadence; // From fitness equipment
|
||||
} else if (isSpeedCadenceConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From dedicated speed/cadence sensor
|
||||
} else if (isPowerConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From power sensor (if it provides cadence)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getPower() {
|
||||
return power;
|
||||
// Priority: 1) Dedicated Power Sensor, 2) Fitness Equipment
|
||||
if (isPowerConnected && powerSensorPower > 0) {
|
||||
return powerSensorPower; // From dedicated power sensor (most accurate)
|
||||
} else if (isConnected && power > 0) {
|
||||
return power; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double getSpeedKph() {
|
||||
// Convert from m/s to km/h
|
||||
return speed.doubleValue() * 3.6;
|
||||
return getSpeedMps() * 3.6;
|
||||
}
|
||||
|
||||
public double getSpeedMps() {
|
||||
return speed.doubleValue();
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorSpeed.doubleValue() > 0) {
|
||||
return speedSensorSpeed.doubleValue(); // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && speed.doubleValue() > 0) {
|
||||
return speed.doubleValue(); // From fitness equipment
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public long getDistance() {
|
||||
return distance;
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorDistance > 0) {
|
||||
return speedSensorDistance; // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && distance > 0) {
|
||||
return distance; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long getCalories() {
|
||||
@@ -236,4 +513,50 @@ public class BikeChannelController {
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
// Additional connection status methods
|
||||
public boolean isPowerSensorConnected() {
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean isSpeedCadenceSensorConnected() {
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
public boolean isAnyDeviceConnected() {
|
||||
return isConnected || isPowerConnected || isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
// Raw sensor data getters (for debugging/advanced use)
|
||||
public int getRawFitnessEquipmentPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public int getRawPowerSensorPower() {
|
||||
return powerSensorPower;
|
||||
}
|
||||
|
||||
public int getRawFitnessEquipmentCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public int getRawSpeedSensorCadence() {
|
||||
return speedSensorCadence;
|
||||
}
|
||||
|
||||
public double getRawFitnessEquipmentSpeed() {
|
||||
return speed.doubleValue();
|
||||
}
|
||||
|
||||
public double getRawSpeedSensorSpeed() {
|
||||
return speedSensorSpeed.doubleValue();
|
||||
}
|
||||
|
||||
public long getRawFitnessEquipmentDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public long getRawSpeedSensorDistance() {
|
||||
return speedSensorDistance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ public class ChannelService extends Service {
|
||||
|
||||
public void openAllChannels() throws ChannelNotAvailableException {
|
||||
if (Ant.heartRequest && heartChannelController == null)
|
||||
heartChannelController = new HeartChannelController();
|
||||
heartChannelController = new HeartChannelController(Ant.antHeartDeviceNumber);
|
||||
|
||||
if (Ant.speedRequest) {
|
||||
if(Ant.treadmill && sdmChannelController == null) {
|
||||
@@ -330,7 +330,7 @@ public class ChannelService extends Service {
|
||||
|
||||
// Add initialization for BikeChannelController (receiver)
|
||||
if (Ant.bikeRequest && bikeChannelController == null) {
|
||||
bikeChannelController = new BikeChannelController();
|
||||
bikeChannelController = new BikeChannelController(Ant.technoGymGroupCycle, Ant.antBikeDeviceNumber);
|
||||
}
|
||||
|
||||
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
|
||||
|
||||
@@ -1,96 +1,91 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.DisplayCutout;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
|
||||
public class CustomQtActivity extends QtActivity {
|
||||
private static final String TAG = "CustomQtActivity";
|
||||
private static CustomQtActivity instance;
|
||||
|
||||
|
||||
// Declare the native method that will be implemented in C++
|
||||
private static native void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = this;
|
||||
|
||||
Log.d(TAG, "onCreate: CustomQtActivity initialized");
|
||||
|
||||
// Get and log the real status bar height
|
||||
int statusBarHeight = getStatusBarHeight();
|
||||
Log.d(TAG, "Real status bar height: " + statusBarHeight + "px");
|
||||
}
|
||||
|
||||
// Native method that can be called from C++/Qt
|
||||
public static int getStatusBarHeight() {
|
||||
try {
|
||||
if (instance == null) {
|
||||
Log.e("CustomQtActivity", "Activity instance not available");
|
||||
return 72; // fallback value
|
||||
}
|
||||
|
||||
Resources resources = instance.getResources();
|
||||
android.util.DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
|
||||
// Log display metrics for analysis
|
||||
Log.d("CustomQtActivity", "Display metrics:");
|
||||
Log.d("CustomQtActivity", " density: " + metrics.density);
|
||||
Log.d("CustomQtActivity", " densityDpi: " + metrics.densityDpi);
|
||||
Log.d("CustomQtActivity", " scaledDensity: " + metrics.scaledDensity);
|
||||
Log.d("CustomQtActivity", " xdpi: " + metrics.xdpi);
|
||||
Log.d("CustomQtActivity", " ydpi: " + metrics.ydpi);
|
||||
|
||||
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
|
||||
if (resourceId > 0) {
|
||||
int heightPx = resources.getDimensionPixelSize(resourceId);
|
||||
float heightInDp = heightPx / metrics.density;
|
||||
|
||||
Log.d("CustomQtActivity", "Status bar height analysis:");
|
||||
Log.d("CustomQtActivity", " getDimensionPixelSize: " + heightPx + "px");
|
||||
Log.d("CustomQtActivity", " Calculated DP: " + heightInDp + "dp");
|
||||
Log.d("CustomQtActivity", " Returning DP value: " + Math.round(heightInDp));
|
||||
|
||||
// Return DP value instead of pixel value to let Qt handle scaling
|
||||
return Math.round(heightInDp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomQtActivity", "Error getting status bar height", e);
|
||||
|
||||
// This tells the OS that we want to handle the display cutout area ourselves
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
return 72; // fallback value ~24dp
|
||||
}
|
||||
|
||||
// Native method that can be called from C++/Qt for navigation bar
|
||||
public static int getNavigationBarHeight() {
|
||||
try {
|
||||
if (instance == null) {
|
||||
Log.e("CustomQtActivity", "Activity instance not available for navigation bar");
|
||||
return 48; // fallback value
|
||||
|
||||
// This is the core of the new solution. We set a listener on the main view.
|
||||
// The OS will call this listener whenever the insets change (e.g., on rotation).
|
||||
final View decorView = getWindow().getDecorView();
|
||||
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
|
||||
if (density > 0) {
|
||||
// Use system window insets as primary source
|
||||
top = Math.round(insets.getSystemWindowInsetTop() / density);
|
||||
bottom = Math.round(insets.getSystemWindowInsetBottom() / density);
|
||||
left = Math.round(insets.getSystemWindowInsetLeft() / density);
|
||||
right = Math.round(insets.getSystemWindowInsetRight() / density);
|
||||
|
||||
// For API 28+, also check display cutout for additional padding
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
// Use the maximum between system window inset and cutout safe inset
|
||||
left = Math.max(left, Math.round(cutout.getSafeInsetLeft() / density));
|
||||
right = Math.max(right, Math.round(cutout.getSafeInsetRight() / density));
|
||||
top = Math.max(top, Math.round(cutout.getSafeInsetTop() / density));
|
||||
bottom = Math.max(bottom, Math.round(cutout.getSafeInsetBottom() / density));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "onApplyWindowInsets - Top:" + top + " Bottom:" + bottom + " Left:" + left + " Right:" + right);
|
||||
Log.d(TAG, "Raw insets - SystemTop:" + insets.getSystemWindowInsetTop() +
|
||||
" SystemBottom:" + insets.getSystemWindowInsetBottom() +
|
||||
" SystemLeft:" + insets.getSystemWindowInsetLeft() +
|
||||
" SystemRight:" + insets.getSystemWindowInsetRight());
|
||||
Log.d(TAG, "Stable insets - StableTop:" + insets.getStableInsetTop() +
|
||||
" StableBottom:" + insets.getStableInsetBottom() +
|
||||
" StableLeft:" + insets.getStableInsetLeft() +
|
||||
" StableRight:" + insets.getStableInsetRight());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
Log.d(TAG, "Cutout insets - Top:" + cutout.getSafeInsetTop() +
|
||||
" Bottom:" + cutout.getSafeInsetBottom() +
|
||||
" Left:" + cutout.getSafeInsetLeft() +
|
||||
" Right:" + cutout.getSafeInsetRight());
|
||||
}
|
||||
}
|
||||
|
||||
// Push the new, correct inset values to the C++ layer
|
||||
onInsetsChanged(top, bottom, left, right);
|
||||
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
Resources resources = instance.getResources();
|
||||
android.util.DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
|
||||
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
|
||||
if (resourceId > 0) {
|
||||
int heightPx = resources.getDimensionPixelSize(resourceId);
|
||||
float heightInDp = heightPx / metrics.density;
|
||||
|
||||
Log.d("CustomQtActivity", "Navigation bar height analysis:");
|
||||
Log.d("CustomQtActivity", " getDimensionPixelSize: " + heightPx + "px");
|
||||
Log.d("CustomQtActivity", " Calculated DP: " + heightInDp + "dp");
|
||||
Log.d("CustomQtActivity", " Returning DP value: " + Math.round(heightInDp));
|
||||
|
||||
// Return DP value instead of pixel value to let Qt handle scaling
|
||||
return Math.round(heightInDp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("CustomQtActivity", "Error getting navigation bar height", e);
|
||||
}
|
||||
return 48; // fallback value ~16dp
|
||||
});
|
||||
}
|
||||
|
||||
// Native method that can be called from C++/Qt to get API level
|
||||
|
||||
// This method is still needed for the QML check
|
||||
public static int getApiLevel() {
|
||||
return android.os.Build.VERSION.SDK_INT;
|
||||
return Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
public class FloatingWindowGFG extends Service {
|
||||
|
||||
@@ -37,6 +41,14 @@ public class FloatingWindowGFG extends Service {
|
||||
private WindowManager.LayoutParams floatWindowLayoutParam;
|
||||
private WindowManager windowManager;
|
||||
private Button maximizeBtn;
|
||||
private Handler handler;
|
||||
private Runnable paddingTimeoutRunnable;
|
||||
private boolean isDraggingEnabled = false;
|
||||
private int originalHeight;
|
||||
private boolean isExpanded = false;
|
||||
private WebView webView;
|
||||
private int originalMargin = 20; // in dp, matching the XML layout
|
||||
private int reducedMargin = 2; // minimal margin when not dragging
|
||||
|
||||
// Retrieve the user preference node for the package com.mycompany
|
||||
SharedPreferences sharedPreferences;
|
||||
@@ -56,6 +68,9 @@ public class FloatingWindowGFG extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Initialize handler for timeout operations
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// The screen height and width are calculated, cause
|
||||
// the height and width of the floating window is set depending on this
|
||||
/*DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
|
||||
@@ -73,23 +88,30 @@ public class FloatingWindowGFG extends Service {
|
||||
// inflate a new view hierarchy from the floating_layout xml
|
||||
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
|
||||
|
||||
WebView wv = (WebView)floatView.findViewById(R.id.webview);
|
||||
wv.setWebViewClient(new WebViewClient(){
|
||||
webView = (WebView)floatView.findViewById(R.id.webview);
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
WebSettings settings = wv.getSettings();
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
wv.clearView();
|
||||
wv.measure(100, 100);
|
||||
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
|
||||
// Add JavaScript interface for communication with HTML
|
||||
webView.addJavascriptInterface(new WebAppInterface(), "Android");
|
||||
|
||||
webView.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
webView.clearView();
|
||||
webView.measure(100, 100);
|
||||
webView.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
QLog.d("QZ","loadurl");
|
||||
|
||||
// Initially set reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
|
||||
|
||||
// WindowManager.LayoutParams takes a lot of parameters to set the
|
||||
@@ -116,17 +138,18 @@ public class FloatingWindowGFG extends Service {
|
||||
// 5) Next parameter is Layout_Format. System chooses a format that supports
|
||||
// translucency by PixelFormat.TRANSLUCENT
|
||||
|
||||
originalHeight = FloatingHandler._height;
|
||||
floatWindowLayoutParam = new WindowManager.LayoutParams(
|
||||
(int) (FloatingHandler._width ),
|
||||
(int) (FloatingHandler._height ),
|
||||
(int) (originalHeight ),
|
||||
LAYOUT_TYPE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
// The Gravity of the Floating Window is set.
|
||||
// The Window will appear in the center of the screen
|
||||
floatWindowLayoutParam.gravity = Gravity.CENTER;
|
||||
// Use TOP | LEFT for free positioning without constraints
|
||||
floatWindowLayoutParam.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
|
||||
// X and Y value of the window is set
|
||||
floatWindowLayoutParam.x = 0;
|
||||
@@ -145,48 +168,86 @@ public class FloatingWindowGFG extends Service {
|
||||
// The window can be moved at any position on the screen.
|
||||
floatView.setOnTouchListener(new View.OnTouchListener() {
|
||||
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
|
||||
double x;
|
||||
double y;
|
||||
double px;
|
||||
double py;
|
||||
int initialX;
|
||||
int initialY;
|
||||
float initialTouchX;
|
||||
float initialTouchY;
|
||||
boolean isDragging = false;
|
||||
final int TOUCH_THRESHOLD = 10; // Threshold for distinguishing tap vs drag
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
||||
QLog.d("QZ","onTouch");
|
||||
QLog.d("QZ","onTouch action: " + event.getAction());
|
||||
|
||||
switch (event.getAction()) {
|
||||
// When the window will be touched,
|
||||
// the x and y position of that position
|
||||
// will be retrieved
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
x = floatWindowLayoutUpdateParam.x;
|
||||
y = floatWindowLayoutUpdateParam.y;
|
||||
|
||||
// returns the original raw X
|
||||
// coordinate of this event
|
||||
px = event.getRawX();
|
||||
|
||||
// returns the original raw Y
|
||||
// coordinate of this event
|
||||
py = event.getRawY();
|
||||
// Store initial positions
|
||||
initialX = floatWindowLayoutUpdateParam.x;
|
||||
initialY = floatWindowLayoutUpdateParam.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
isDragging = false;
|
||||
|
||||
// Enable dragging for 5 seconds
|
||||
enableDraggingTemporarily();
|
||||
break;
|
||||
// When the window will be dragged around,
|
||||
// it will update the x, y of the Window Layout Parameter
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
|
||||
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
|
||||
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.commit();
|
||||
|
||||
// updated parameter is applied to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
break;
|
||||
// Calculate distance moved
|
||||
float deltaX = event.getRawX() - initialTouchX;
|
||||
float deltaY = event.getRawY() - initialTouchY;
|
||||
|
||||
// Check if we've moved enough to consider this a drag
|
||||
if (!isDragging && (Math.abs(deltaX) > TOUCH_THRESHOLD || Math.abs(deltaY) > TOUCH_THRESHOLD)) {
|
||||
isDragging = true;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Only allow dragging if it's temporarily enabled
|
||||
if (isDragging && isDraggingEnabled) {
|
||||
// Get screen dimensions for boundary checking
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
// Calculate new position
|
||||
int newX = initialX + (int) deltaX;
|
||||
int newY = initialY + (int) deltaY;
|
||||
|
||||
// Apply boundary constraints
|
||||
// Keep window within screen bounds
|
||||
int windowWidth = FloatingHandler._width;
|
||||
int windowHeight = FloatingHandler._height;
|
||||
|
||||
if (newX < 0) newX = 0;
|
||||
if (newY < 0) newY = 0;
|
||||
if (newX + windowWidth > screenWidth) newX = screenWidth - windowWidth;
|
||||
if (newY + windowHeight > screenHeight) newY = screenHeight - windowHeight;
|
||||
|
||||
// Update position
|
||||
floatWindowLayoutUpdateParam.x = newX;
|
||||
floatWindowLayoutUpdateParam.y = newY;
|
||||
|
||||
// Save position to preferences
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.apply(); // Use apply() instead of commit() for better performance
|
||||
|
||||
// Apply updated parameter to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
// If it wasn't a drag, it's a tap - let the WebView handle it
|
||||
if (!isDragging) {
|
||||
return false; // Let the event propagate to WebView
|
||||
}
|
||||
isDragging = false;
|
||||
break;
|
||||
}
|
||||
return isDragging && isDraggingEnabled; // Consume the event only if we're dragging and dragging is enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -200,4 +261,107 @@ public class FloatingWindowGFG extends Service {
|
||||
// Window is removed from the screen
|
||||
windowManager.removeView(floatView);
|
||||
}
|
||||
|
||||
// Method to enable dragging temporarily for 5 seconds
|
||||
private void enableDraggingTemporarily() {
|
||||
isDraggingEnabled = true;
|
||||
|
||||
// Increase margin for better dragging experience
|
||||
setWebViewMargin(originalMargin);
|
||||
|
||||
// Cancel any existing timeout
|
||||
if (paddingTimeoutRunnable != null) {
|
||||
handler.removeCallbacks(paddingTimeoutRunnable);
|
||||
}
|
||||
|
||||
// Create new timeout runnable
|
||||
paddingTimeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
isDraggingEnabled = false;
|
||||
// Restore reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
QLog.d("QZ", "Dragging disabled after timeout, margin restored");
|
||||
}
|
||||
};
|
||||
|
||||
// Schedule timeout for 5 seconds
|
||||
handler.postDelayed(paddingTimeoutRunnable, 5000);
|
||||
}
|
||||
|
||||
// Method to expand window height dynamically
|
||||
private void expandWindow(int additionalHeight) {
|
||||
if (!isExpanded) {
|
||||
isExpanded = true;
|
||||
floatWindowLayoutParam.height = originalHeight + additionalHeight;
|
||||
|
||||
// Adjust Y position to keep window within screen bounds
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
if (floatWindowLayoutParam.y + floatWindowLayoutParam.height > screenHeight) {
|
||||
floatWindowLayoutParam.y = screenHeight - floatWindowLayoutParam.height;
|
||||
if (floatWindowLayoutParam.y < 0) {
|
||||
floatWindowLayoutParam.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window expanded to height: " + floatWindowLayoutParam.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to restore original window height
|
||||
private void restoreWindow() {
|
||||
if (isExpanded) {
|
||||
isExpanded = false;
|
||||
floatWindowLayoutParam.height = originalHeight;
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window restored to original height: " + originalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set WebView margin dynamically
|
||||
private void setWebViewMargin(int marginDp) {
|
||||
if (webView != null) {
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) webView.getLayoutParams();
|
||||
int marginPx = (int) (marginDp * getResources().getDisplayMetrics().density);
|
||||
params.setMargins(marginPx, marginPx, marginPx, marginPx);
|
||||
webView.setLayoutParams(params);
|
||||
QLog.d("QZ", "WebView margin set to: " + marginDp + "dp (" + marginPx + "px)");
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript interface class
|
||||
public class WebAppInterface {
|
||||
@JavascriptInterface
|
||||
public void expandFloatingWindow(int additionalHeight) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
expandWindow(additionalHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void restoreFloatingWindow() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
restoreWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void enableDraggingMargins() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
enableDraggingTemporarily();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@ public class Garmin {
|
||||
}
|
||||
|
||||
public static void init(Context c) {
|
||||
if (connectIqReady || connectIqInitializing) {
|
||||
QLog.d(TAG, "Garmin already initialized or initializing");
|
||||
return;
|
||||
}
|
||||
connectIqInitializing = true;
|
||||
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -84,6 +90,7 @@ public class Garmin {
|
||||
@Override
|
||||
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
|
||||
QLog.e(TAG, errStatus.toString());
|
||||
connectIqInitializing = false;
|
||||
connectIqReady = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,14 +42,14 @@ public class HeartChannelController {
|
||||
private boolean isConnected = false;
|
||||
public int heart = 0; // Public to be accessible from ChannelService
|
||||
|
||||
public HeartChannelController() {
|
||||
public HeartChannelController(int antHeartDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
openChannel();
|
||||
openChannel(antHeartDeviceNumber);
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
// Request access to first available heart rate device
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, 0, 0, // 0 means first available device
|
||||
public boolean openChannel(int deviceNumber) {
|
||||
// Request access to heart rate device (deviceNumber = 0 means first available)
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
@@ -57,7 +57,7 @@ public class HeartChannelController {
|
||||
case SUCCESS:
|
||||
hrPcc = result;
|
||||
isConnected = true;
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToHrEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
|
||||
@@ -7,9 +7,13 @@ import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
private static MediaButtonReceiver instance;
|
||||
private static final int TARGET_VOLUME = 7; // Middle volume value for infinite gear changes
|
||||
private static boolean restoringVolume = false; // Flag to prevent recursion
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@@ -21,8 +25,30 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
|
||||
int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1);
|
||||
|
||||
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
|
||||
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Previous: " + previousVolume + ", Max: " + maxVolume + ", Restoring: " + restoringVolume);
|
||||
|
||||
// If we're restoring volume, skip processing and reset flag
|
||||
if (restoringVolume) {
|
||||
QLog.d("MediaButtonReceiver", "Volume restore completed");
|
||||
restoringVolume = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the gear change
|
||||
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
|
||||
|
||||
// Auto-restore volume to middle value after a short delay to enable infinite gear changes
|
||||
if (currentVolume != TARGET_VOLUME) {
|
||||
final AudioManager am = audioManager;
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d("MediaButtonReceiver", "Auto-restoring volume to: " + TARGET_VOLUME);
|
||||
restoringVolume = true;
|
||||
am.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
|
||||
}
|
||||
}, 100); // 100ms delay to ensure gear change is processed first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +80,25 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
|
||||
|
||||
|
||||
// Initialize volume to target value for gear control
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (audioManager != null) {
|
||||
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
|
||||
if (currentVolume != TARGET_VOLUME) {
|
||||
QLog.d("MediaButtonReceiver", "Initializing volume to: " + TARGET_VOLUME);
|
||||
restoringVolume = true;
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
|
||||
// Reset flag after initialization
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
restoringVolume = false;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -21,77 +21,133 @@ public class QLog {
|
||||
|
||||
// Debug level methods
|
||||
public static int d(String tag, String msg) {
|
||||
sendToQt(3, tag, msg);
|
||||
try {
|
||||
sendToQt(3, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg);
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Error level methods
|
||||
public static int e(String tag, String msg) {
|
||||
sendToQt(6, tag, msg);
|
||||
try {
|
||||
sendToQt(6, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg);
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Info level methods
|
||||
public static int i(String tag, String msg) {
|
||||
sendToQt(4, tag, msg);
|
||||
try {
|
||||
sendToQt(4, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg);
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Verbose level methods
|
||||
public static int v(String tag, String msg) {
|
||||
sendToQt(2, tag, msg);
|
||||
try {
|
||||
sendToQt(2, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg);
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Warning level methods
|
||||
public static int w(String tag, String msg) {
|
||||
sendToQt(5, tag, msg);
|
||||
try {
|
||||
sendToQt(5, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg);
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg, tr);
|
||||
}
|
||||
|
||||
public static int w(String tag, Throwable tr) {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, tr);
|
||||
}
|
||||
|
||||
// What a Terrible Failure: Report an exception that should never happen
|
||||
public static int wtf(String tag, String msg) {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, tr);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, String msg, Throwable tr) {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg, tr);
|
||||
}
|
||||
|
||||
@@ -106,7 +162,11 @@ public class QLog {
|
||||
|
||||
// Additional utility methods
|
||||
public static int println(int priority, String tag, String msg) {
|
||||
sendToQt(priority, tag, msg);
|
||||
try {
|
||||
sendToQt(priority, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.println(priority, tag, msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -208,13 +208,13 @@ public class SDMChannelController {
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
payload[0] = (byte) 0x01;
|
||||
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
|
||||
payload[2] = (byte) ((lastTime % 256000) / 1000);
|
||||
payload[1] = (byte) ((lastTime % 1000) / 5); // time fractional: 0-199 in 1/200 sec units
|
||||
payload[2] = (byte) ((lastTime / 1000) % 256); // time integer: seconds mod 256
|
||||
payload[3] = (byte) 0x00;
|
||||
payload[4] = (byte) speedM_s;
|
||||
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
|
||||
payload[6] = (byte) stride_count++; // bad but it works on zwift
|
||||
payload[7] = (byte) ((double)deltaTime * 0.03125);
|
||||
payload[4] = (byte) ((int)speedM_s & 0x0F); // speed integer in lower 4 bits only
|
||||
payload[5] = (byte) Math.round((speedM_s - (double)((int)speedM_s)) * 256.0);
|
||||
payload[6] = (byte) stride_count++;
|
||||
payload[7] = (byte) 0; // update latency: no delay in real-time system
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
@@ -257,13 +257,13 @@ public class SDMChannelController {
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
payload[0] = (byte) 0x01;
|
||||
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
|
||||
payload[2] = (byte) ((lastTime % 256000) / 1000);
|
||||
payload[1] = (byte) ((lastTime % 1000) / 5); // time fractional: 0-199 in 1/200 sec units
|
||||
payload[2] = (byte) ((lastTime / 1000) % 256); // time integer: seconds mod 256
|
||||
payload[3] = (byte) 0x00;
|
||||
payload[4] = (byte) speedM_s;
|
||||
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
|
||||
payload[6] = (byte) stride_count;
|
||||
payload[7] = (byte) ((double)deltaTime * 0.03125);
|
||||
payload[4] = (byte) ((int)speedM_s & 0x0F); // speed integer in lower 4 bits only
|
||||
payload[5] = (byte) Math.round((speedM_s - (double)((int)speedM_s)) * 256.0);
|
||||
payload[6] = (byte) stride_count++;
|
||||
payload[7] = (byte) 0; // update latency: no delay in real-time system
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
|
||||
@@ -43,7 +43,11 @@ public class Usbserial {
|
||||
static int lastReadLen = 0;
|
||||
|
||||
public static void open(Context context) {
|
||||
QLog.d("QZ","UsbSerial open");
|
||||
open(context, 2400); // Default baud rate for Computrainer
|
||||
}
|
||||
|
||||
public static void open(Context context, int baudRate) {
|
||||
QLog.d("QZ","UsbSerial open with baud rate: " + baudRate);
|
||||
// Find all available drivers from attached devices.
|
||||
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
|
||||
@@ -98,13 +102,12 @@ public class Usbserial {
|
||||
port = driver.getPorts().get(0); // Most devices have just one port (port 0)
|
||||
try {
|
||||
port.open(connection);
|
||||
port.setParameters(2400, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
|
||||
port.setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
|
||||
QLog.d("QZ","UsbSerial port opened successfully at " + baudRate + " baud");
|
||||
}
|
||||
catch (IOException e) {
|
||||
// Do something here
|
||||
QLog.d("QZ","UsbSerial port open failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
QLog.d("QZ","UsbSerial port opened");
|
||||
}
|
||||
|
||||
public static void write (byte[] bytes) {
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
#include "androidstatusbar.h"
|
||||
#include <QQmlEngine>
|
||||
#include <QGuiApplication>
|
||||
#include <QScreen>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QtAndroid>
|
||||
#include <QAndroidJniObject>
|
||||
#include <QAndroidJniEnvironment>
|
||||
#endif
|
||||
|
||||
AndroidStatusBar::AndroidStatusBar(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_cachedHeight(-1)
|
||||
, m_cachedNavigationBarHeight(-1)
|
||||
AndroidStatusBar* AndroidStatusBar::m_instance = nullptr;
|
||||
|
||||
AndroidStatusBar::AndroidStatusBar(QObject *parent) : QObject(parent)
|
||||
{
|
||||
// Listen for orientation changes
|
||||
if (QGuiApplication::primaryScreen()) {
|
||||
connect(QGuiApplication::primaryScreen(), &QScreen::orientationChanged,
|
||||
this, &AndroidStatusBar::onOrientationChanged);
|
||||
}
|
||||
m_instance = this;
|
||||
}
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::instance()
|
||||
{
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void AndroidStatusBar::registerQmlType()
|
||||
{
|
||||
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
|
||||
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
|
||||
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
|
||||
Q_UNUSED(engine)
|
||||
Q_UNUSED(scriptEngine)
|
||||
@@ -31,99 +29,36 @@ void AndroidStatusBar::registerQmlType()
|
||||
});
|
||||
}
|
||||
|
||||
int AndroidStatusBar::height() const
|
||||
{
|
||||
if (m_cachedHeight == -1) {
|
||||
m_cachedHeight = getStatusBarHeightFromAndroid();
|
||||
}
|
||||
return m_cachedHeight;
|
||||
}
|
||||
|
||||
int AndroidStatusBar::navigationBarHeight() const
|
||||
{
|
||||
if (m_cachedNavigationBarHeight == -1) {
|
||||
m_cachedNavigationBarHeight = getNavigationBarHeightFromAndroid();
|
||||
}
|
||||
return m_cachedNavigationBarHeight;
|
||||
}
|
||||
|
||||
int AndroidStatusBar::apiLevel() const
|
||||
{
|
||||
return getApiLevelFromAndroid();
|
||||
}
|
||||
|
||||
void AndroidStatusBar::onOrientationChanged()
|
||||
{
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
void AndroidStatusBar::invalidateCache()
|
||||
{
|
||||
m_cachedHeight = -1;
|
||||
m_cachedNavigationBarHeight = -1;
|
||||
emit heightChanged();
|
||||
emit navigationBarHeightChanged();
|
||||
}
|
||||
|
||||
int AndroidStatusBar::getStatusBarHeightFromAndroid() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
try {
|
||||
// Call the static method that returns int directly
|
||||
int height = QAndroidJniObject::callStaticMethod<jint>(
|
||||
"org/cagnulen/qdomyoszwift/CustomQtActivity",
|
||||
"getStatusBarHeight",
|
||||
"()I"
|
||||
);
|
||||
|
||||
return height;
|
||||
} catch (...) {
|
||||
// Fallback: return a reasonable default
|
||||
return 72; // ~24dp for typical Android devices
|
||||
}
|
||||
return QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/CustomQtActivity", "getApiLevel", "()I");
|
||||
#else
|
||||
return 0; // Non-Android platforms don't have status bar
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int AndroidStatusBar::getNavigationBarHeightFromAndroid() const
|
||||
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right)
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
try {
|
||||
// Call the static method that returns int directly
|
||||
int height = QAndroidJniObject::callStaticMethod<jint>(
|
||||
"org/cagnulen/qdomyoszwift/CustomQtActivity",
|
||||
"getNavigationBarHeight",
|
||||
"()I"
|
||||
);
|
||||
|
||||
return height;
|
||||
} catch (...) {
|
||||
// Fallback: return a reasonable default
|
||||
return 48; // ~16dp for typical Android devices
|
||||
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right) {
|
||||
m_top = top;
|
||||
m_bottom = bottom;
|
||||
m_left = left;
|
||||
m_right = right;
|
||||
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left << "Right:" << m_right;
|
||||
emit insetsChanged();
|
||||
}
|
||||
#else
|
||||
return 0; // Non-Android platforms don't have navigation bar
|
||||
#endif
|
||||
}
|
||||
|
||||
int AndroidStatusBar::getApiLevelFromAndroid() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
try {
|
||||
// Call the static method that returns int directly
|
||||
int apiLevel = QAndroidJniObject::callStaticMethod<jint>(
|
||||
"org/cagnulen/qdomyoszwift/CustomQtActivity",
|
||||
"getApiLevel",
|
||||
"()I"
|
||||
);
|
||||
|
||||
return apiLevel;
|
||||
} catch (...) {
|
||||
// Fallback: return a reasonable default
|
||||
return 30; // Default to API 30 if we can't get it
|
||||
// JNI method with standard naming convention
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top, jint bottom, jint left, jint right)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
if (AndroidStatusBar::instance()) {
|
||||
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right);
|
||||
}
|
||||
#else
|
||||
return 0; // Non-Android platforms
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -3,38 +3,41 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QScreen>
|
||||
|
||||
class AndroidStatusBar : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int height READ height NOTIFY heightChanged)
|
||||
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY navigationBarHeightChanged)
|
||||
Q_PROPERTY(int height READ height NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int leftInset READ leftInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int rightInset READ rightInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit AndroidStatusBar(QObject *parent = nullptr);
|
||||
|
||||
|
||||
static void registerQmlType();
|
||||
|
||||
int height() const;
|
||||
int navigationBarHeight() const;
|
||||
static AndroidStatusBar* instance();
|
||||
|
||||
int height() const { return m_top; }
|
||||
int navigationBarHeight() const { return m_bottom; }
|
||||
int leftInset() const { return m_left; }
|
||||
int rightInset() const { return m_right; }
|
||||
int apiLevel() const;
|
||||
|
||||
signals:
|
||||
void heightChanged();
|
||||
void navigationBarHeightChanged();
|
||||
public slots:
|
||||
void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
signals:
|
||||
void insetsChanged();
|
||||
|
||||
private slots:
|
||||
void onOrientationChanged();
|
||||
|
||||
private:
|
||||
int getStatusBarHeightFromAndroid() const;
|
||||
int getNavigationBarHeightFromAndroid() const;
|
||||
int getApiLevelFromAndroid() const;
|
||||
void invalidateCache();
|
||||
mutable int m_cachedHeight;
|
||||
mutable int m_cachedNavigationBarHeight;
|
||||
int m_top = 0;
|
||||
int m_bottom = 0;
|
||||
int m_left = 0;
|
||||
int m_right = 0;
|
||||
|
||||
static AndroidStatusBar* m_instance;
|
||||
};
|
||||
|
||||
#endif // ANDROIDSTATUSBAR_H
|
||||
#endif // ANDROIDSTATUSBAR_H
|
||||
|
||||
6
src/bluetoothdevicetype.h
Normal file
6
src/bluetoothdevicetype.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef BLUETOOTHDEVICETYPE_H
|
||||
#define BLUETOOTHDEVICETYPE_H
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
|
||||
#endif // BLUETOOTHDEVICETYPE_H
|
||||
@@ -5,7 +5,7 @@ CharacteristicNotifier2A53::CharacteristicNotifier2A53(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2a53, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2A53::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
value.append(0x02); // total distance
|
||||
uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256;
|
||||
uint32_t distance = Bike->odometer() * 10000.0;
|
||||
|
||||
@@ -4,11 +4,11 @@ CharacteristicNotifier2A63::CharacteristicNotifier2A63(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2a63, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2A63::notify(QByteArray &value) {
|
||||
double normalizeWattage = Bike->wattsMetric().value();
|
||||
double normalizeWattage = Bike->wattsMetricforUI();
|
||||
if (normalizeWattage < 0)
|
||||
normalizeWattage = 0;
|
||||
|
||||
if (Bike->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (Bike->deviceType() == BIKE) {
|
||||
/*
|
||||
// set measurement
|
||||
measurement[2] = power & 0xFF;
|
||||
|
||||
@@ -7,9 +7,16 @@ CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
value.append(0x0C); // Inclination available and distance for peloton
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == TREADMILL || dt == ELLIPTICAL) {
|
||||
QSettings settings;
|
||||
bool bike_cadence_sensor = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
|
||||
if (bike_cadence_sensor) {
|
||||
value.append(0x0C); // Inclination and distance available (old behavior)
|
||||
} else {
|
||||
value.append(0x0E); // Inclination, distance and average speed available
|
||||
}
|
||||
//value.append((char)0x01); // heart rate available
|
||||
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
|
||||
|
||||
@@ -19,7 +26,33 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
QByteArray speedBytes;
|
||||
speedBytes.append(b);
|
||||
speedBytes.append(a);
|
||||
|
||||
|
||||
// average speed in 0.01 km/h (distance from startup / elapsed time)
|
||||
double elapsed_time_seconds = 0.0;
|
||||
uint16_t averageSpeed = 0;
|
||||
QByteArray averageSpeedBytes;
|
||||
if (!bike_cadence_sensor) {
|
||||
QTime sessionElapsedTime = Bike->elapsedTime();
|
||||
elapsed_time_seconds = (double)sessionElapsedTime.hour() * 3600.0 +
|
||||
(double)sessionElapsedTime.minute() * 60.0 +
|
||||
(double)sessionElapsedTime.second() +
|
||||
(double)sessionElapsedTime.msec() / 1000.0;
|
||||
if (elapsed_time_seconds > 0) {
|
||||
double distance_m = Bike->odometerFromStartup() * 1000.0;
|
||||
double avg_kmh = (distance_m * 3.6) / elapsed_time_seconds;
|
||||
averageSpeed = (uint16_t)qRound(avg_kmh * 100.0);
|
||||
}
|
||||
averageSpeedBytes.append(static_cast<char>(averageSpeed & 0xFF));
|
||||
averageSpeedBytes.append(static_cast<char>((averageSpeed >> 8) & 0xFF));
|
||||
} else {
|
||||
elapsed_time_seconds = 0.0;
|
||||
QTime sessionElapsedTime = Bike->elapsedTime();
|
||||
elapsed_time_seconds = (double)sessionElapsedTime.hour() * 3600.0 +
|
||||
(double)sessionElapsedTime.minute() * 60.0 +
|
||||
(double)sessionElapsedTime.second() +
|
||||
(double)sessionElapsedTime.msec() / 1000.0;
|
||||
}
|
||||
|
||||
// peloton wants the distance from the qz startup to handle stacked classes
|
||||
// https://github.com/cagnulein/qdomyos-zwift/issues/2018
|
||||
uint32_t normalizeDistance = (uint32_t)qRound(Bike->odometerFromStartup() * 1000);
|
||||
@@ -34,7 +67,6 @@ 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) {
|
||||
@@ -46,7 +78,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
inclination /= gain;
|
||||
}
|
||||
|
||||
if (dt == bluetoothdevice::TREADMILL)
|
||||
if (dt == TREADMILL)
|
||||
normalizeIncline = (uint32_t)qRound(inclination * 10);
|
||||
a = (normalizeIncline >> 8) & 0XFF;
|
||||
b = normalizeIncline & 0XFF;
|
||||
@@ -54,29 +86,25 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
|
||||
inclineBytes.append(b);
|
||||
inclineBytes.append(a);
|
||||
double ramp = 0;
|
||||
if (dt == bluetoothdevice::TREADMILL)
|
||||
if (dt == TREADMILL)
|
||||
ramp = qRadiansToDegrees(qAtan(inclination / 100));
|
||||
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
|
||||
int16_t normalizeRamp = (int16_t)qRound(ramp * 10);
|
||||
a = (normalizeRamp >> 8) & 0XFF;
|
||||
b = normalizeRamp & 0XFF;
|
||||
QByteArray rampBytes;
|
||||
rampBytes.append(b);
|
||||
rampBytes.append(a);
|
||||
|
||||
// Get session elapsed time - makes Runna calculations work
|
||||
QTime sessionElapsedTime = Bike->elapsedTime();
|
||||
double elapsed_time_seconds =
|
||||
(double)sessionElapsedTime.hour() * 3600.0 +
|
||||
(double)sessionElapsedTime.minute() * 60.0 +
|
||||
(double)sessionElapsedTime.second() +
|
||||
(double)sessionElapsedTime.msec() / 1000.0;
|
||||
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
|
||||
QByteArray elapsedBytes;
|
||||
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
|
||||
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
|
||||
|
||||
value.append(speedBytes); // Actual value.
|
||||
|
||||
if (!bike_cadence_sensor) {
|
||||
value.append(averageSpeedBytes); // Average speed value.
|
||||
}
|
||||
|
||||
value.append(distanceBytes); // Actual value.
|
||||
|
||||
value.append(inclineBytes); // incline
|
||||
|
||||
@@ -8,23 +8,23 @@ CharacteristicNotifier2AD2::CharacteristicNotifier2AD2(bluetoothdevice *Bike, QO
|
||||
: CharacteristicNotifier(0x2ad2, parent), Bike(Bike) {}
|
||||
|
||||
int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
|
||||
QSettings settings;
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
bool rowerAsABike = !virtual_device_rower && dt == bluetoothdevice::ROWING;
|
||||
bool rowerAsABike = !virtual_device_rower && dt == ROWING;
|
||||
bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool();
|
||||
double cadence_multiplier = 2.0;
|
||||
if (double_cadence)
|
||||
cadence_multiplier = 1.0;
|
||||
|
||||
|
||||
double normalizeWattage = Bike->wattsMetric().value();
|
||||
double normalizeWattage = Bike->wattsMetricforUI();
|
||||
if (normalizeWattage < 0)
|
||||
normalizeWattage = 0;
|
||||
|
||||
if (dt == bluetoothdevice::BIKE || rowerAsABike) {
|
||||
if (dt == BIKE || rowerAsABike) {
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
|
||||
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
|
||||
value.append((char)0x02); // heart rate
|
||||
@@ -44,7 +44,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
value.append(char(Bike->currentHeart().value())); // Actual value.
|
||||
value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix
|
||||
return CN_OK;
|
||||
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL || dt == bluetoothdevice::ROWING) {
|
||||
} else if (dt == TREADMILL || dt == ELLIPTICAL || dt == ROWING) {
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
|
||||
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
|
||||
value.append((char)0x02); // heart rate
|
||||
@@ -53,11 +53,11 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
|
||||
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
|
||||
|
||||
uint16_t cadence = 0;
|
||||
if (dt == bluetoothdevice::ELLIPTICAL)
|
||||
if (dt == ELLIPTICAL)
|
||||
cadence = ((elliptical *)Bike)->currentCadence().value();
|
||||
else if (dt == bluetoothdevice::TREADMILL)
|
||||
else if (dt == TREADMILL)
|
||||
cadence = ((treadmill *)Bike)->currentCadence().value();
|
||||
else if (dt == bluetoothdevice::ROWING)
|
||||
else if (dt == ROWING)
|
||||
cadence = ((rower *)Bike)->currentCadence().value();
|
||||
|
||||
value.append((char)((uint16_t)(cadence * cadence_multiplier) & 0xFF)); // cadence
|
||||
|
||||
@@ -10,7 +10,7 @@ CharacteristicWriteProcessor::CharacteristicWriteProcessor(double bikeResistance
|
||||
void CharacteristicWriteProcessor::changePower(uint16_t power) { Bike->changePower(power); }
|
||||
|
||||
void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr, uint8_t cw) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
QSettings settings;
|
||||
bool force_resistance =
|
||||
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
|
||||
@@ -64,7 +64,7 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
|
||||
|
||||
qDebug() << "changeSlope CRR = " << fCRR << CRR_offset << "CW = " << fCW;
|
||||
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
if (dt == BIKE) {
|
||||
|
||||
// if the bike doesn't have the inclination by hardware, i'm simulating inclination with the value received
|
||||
// from Zwift
|
||||
@@ -82,9 +82,9 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
|
||||
Bike->changeResistance((resistance_t)(round(resistance * bikeResistanceGain)) + bikeResistanceOffset + 1 +
|
||||
CRR_offset + CW_offset); // resistance start from 1
|
||||
}
|
||||
} else if (dt == bluetoothdevice::TREADMILL) {
|
||||
} else if (dt == TREADMILL) {
|
||||
emit changeInclination(grade, percentage);
|
||||
} else if (dt == bluetoothdevice::ELLIPTICAL) {
|
||||
} else if (dt == ELLIPTICAL) {
|
||||
bool inclinationAvailableByHardware = ((elliptical *)Bike)->inclinationAvailableByHardware();
|
||||
qDebug() << "inclinationAvailableByHardware" << inclinationAvailableByHardware << "erg_mode" << erg_mode;
|
||||
emit changeInclination(grade, percentage);
|
||||
|
||||
@@ -14,7 +14,7 @@ class CharacteristicWriteProcessor : public QObject {
|
||||
public:
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
double bikeResistanceGain = 1.0;
|
||||
bluetoothdevice *Bike;
|
||||
bluetoothdevice *Bike = nullptr;
|
||||
|
||||
explicit CharacteristicWriteProcessor(double bikeResistanceGain, int8_t bikeResistanceOffset,
|
||||
bluetoothdevice *bike, QObject *parent = nullptr);
|
||||
|
||||
@@ -31,7 +31,7 @@ CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003:
|
||||
}
|
||||
|
||||
double CharacteristicWriteProcessor0003::currentGear() {
|
||||
if(zwiftGearReceived)
|
||||
if(zwiftGearReceived || !((bike*)Bike))
|
||||
return currentZwiftGear;
|
||||
else
|
||||
return ((bike*)Bike)->gears();
|
||||
@@ -239,11 +239,11 @@ int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArra
|
||||
changeSlope(slopefloat, 0 /* TODO */, 0 /* TODO */);
|
||||
|
||||
reply = encodeHubRidingData(
|
||||
Bike->wattsMetric().value(),
|
||||
Bike->wattsMetricforUI(),
|
||||
Bike->currentCadence().value(),
|
||||
0,
|
||||
Bike->wattsMetric().value(),
|
||||
calculateUnknown1(Bike->wattsMetric().value()),
|
||||
Bike->wattsMetricforUI(),
|
||||
calculateUnknown1(Bike->wattsMetricforUI()),
|
||||
0
|
||||
);
|
||||
notifier0002->addAnswer(reply);
|
||||
@@ -284,15 +284,15 @@ int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArra
|
||||
QByteArray::fromHex("05") + power);
|
||||
|
||||
reply = encodeHubRidingData(
|
||||
Bike->wattsMetric().value(),
|
||||
Bike->wattsMetricforUI(),
|
||||
Bike->currentCadence().value(),
|
||||
0,
|
||||
Bike->wattsMetric().value(),
|
||||
calculateUnknown1(Bike->wattsMetric().value()),
|
||||
Bike->wattsMetricforUI(),
|
||||
calculateUnknown1(Bike->wattsMetricforUI()),
|
||||
0
|
||||
);
|
||||
notifier0002->addAnswer(reply);
|
||||
|
||||
|
||||
changePower(Power.value);
|
||||
}
|
||||
else if (receivedData.startsWith(expectedHexArray9)) {
|
||||
|
||||
@@ -13,8 +13,8 @@ CharacteristicWriteProcessor2AD9::CharacteristicWriteProcessor2AD9(double bikeRe
|
||||
|
||||
int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
|
||||
if (data.size()) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == BIKE || dt == ROWING) {
|
||||
QSettings settings;
|
||||
bool force_resistance =
|
||||
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
|
||||
@@ -82,7 +82,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
reply.append((quint8)cmd);
|
||||
reply.append((quint8)FTMS_NOT_SUPPORTED);
|
||||
}
|
||||
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
|
||||
char a, b;
|
||||
if ((char)data.at(0) == 0x02) {
|
||||
// Set Target Speed
|
||||
@@ -91,7 +91,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
|
||||
uint16_t uspeed = a + (((uint16_t)b) << 8);
|
||||
double requestSpeed = (double)uspeed / 100.0;
|
||||
if (dt == bluetoothdevice::TREADMILL) {
|
||||
if (dt == TREADMILL) {
|
||||
((treadmill *)Bike)->changeSpeed(requestSpeed);
|
||||
}
|
||||
qDebug() << QStringLiteral("new requested speed ") + QString::number(requestSpeed);
|
||||
@@ -103,10 +103,10 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
|
||||
int16_t sincline = a + (((int16_t)b) << 8);
|
||||
double requestIncline = (double)sincline / 10.0;
|
||||
|
||||
if (dt == bluetoothdevice::TREADMILL)
|
||||
if (dt == TREADMILL)
|
||||
((treadmill *)Bike)->changeInclination(requestIncline, requestIncline);
|
||||
// Resistance as incline on Sole E95s Elliptical #419
|
||||
else if (dt == bluetoothdevice::ELLIPTICAL) {
|
||||
else if (dt == ELLIPTICAL) {
|
||||
if(((elliptical *)Bike)->inclinationAvailableByHardware())
|
||||
((elliptical *)Bike)->changeInclination(requestIncline, requestIncline);
|
||||
else
|
||||
|
||||
@@ -13,8 +13,8 @@ CharacteristicWriteProcessorE005::CharacteristicWriteProcessorE005(double bikeRe
|
||||
|
||||
int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
|
||||
if (data.size()) {
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == bluetoothdevice::BIKE) {
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
if (dt == BIKE) {
|
||||
char cmd = data.at(0);
|
||||
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(), data);
|
||||
if (cmd == wahookickrsnapbike::_setSimMode && data.count() >= 7) {
|
||||
@@ -35,7 +35,7 @@ int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArra
|
||||
qDebug() << "erg mode" << watts;
|
||||
changePower(watts);
|
||||
}
|
||||
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
|
||||
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
|
||||
}
|
||||
reply.append((quint8)FTMS_RESPONSE_CODE);
|
||||
reply.append((quint8)data.at(0));
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
activiotreadmill::activiotreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -158,28 +158,7 @@ uint16_t android_antbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t android_antbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
antbike::antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -132,7 +132,7 @@ void antbike::update() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -168,28 +168,7 @@ uint16_t antbike::wattsFromResistance(double resistance) {
|
||||
}
|
||||
|
||||
resistance_t antbike::resistanceFromPowerRequest(uint16_t power) {
|
||||
//QSettings settings;
|
||||
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
|
||||
/*if(toorx_srx_3500)*/ {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
|
||||
|
||||
if (Cadence.value() == 0)
|
||||
return 1;
|
||||
|
||||
for (resistance_t i = 1; i < maxResistance(); i++) {
|
||||
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
|
||||
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
|
||||
<< wattsFromResistance(i + 1) << power;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (power < wattsFromResistance(1))
|
||||
return 1;
|
||||
else
|
||||
return maxResistance();
|
||||
} /*else {
|
||||
return power / 10;
|
||||
}*/
|
||||
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
apexbike::apexbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -146,11 +146,21 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
lastPacket = newValue;
|
||||
|
||||
if (newValue.length() == 10 && newValue.at(2) == 0x31) {
|
||||
Resistance = newValue.at(5);
|
||||
// Invert resistance: bike resistance 1-32 maps to app display 32-1
|
||||
uint8_t rawResistance = newValue.at(5);
|
||||
Resistance = 33 - rawResistance; // Invert: 1->32, 32->1
|
||||
emit resistanceRead(Resistance.value());
|
||||
m_pelotonResistance = Resistance.value();
|
||||
|
||||
qDebug() << QStringLiteral("Current resistance: ") + QString::number(Resistance.value());
|
||||
// Parse cadence from 5th byte (index 4) and multiply by 2
|
||||
uint8_t rawCadence = newValue.at(4);
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = rawCadence * 2;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value()) + QStringLiteral(", Raw cadence: ") + QString::number(rawCadence) + QStringLiteral(", Final cadence: ") + QString::number(Cadence.value());
|
||||
}
|
||||
|
||||
if (newValue.length() != 10 || newValue.at(2) != 0x31) {
|
||||
@@ -160,41 +170,13 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
uint16_t distanceData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
|
||||
double distance = ((double)distanceData);
|
||||
|
||||
if(distance != lastDistance) {
|
||||
if(lastDistance != 0) {
|
||||
double deltaDistance = distance - lastDistance;
|
||||
double deltaTime = fabs(now.msecsTo(lastTS));
|
||||
double timeHours = deltaTime / (1000.0 * 60.0 * 60.0);
|
||||
double k = 0.005333;
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = (deltaDistance *k) / timeHours; // Speed in distance units per hour
|
||||
} else {
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
Cadence = Speed.value() / 0.37497622;
|
||||
}
|
||||
}
|
||||
lastDistance = distance;
|
||||
lastTS = now;
|
||||
qDebug() << "lastDistance" << lastDistance << "lastTS" << lastTS;
|
||||
// Calculate speed using the same method as echelon bike
|
||||
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
|
||||
Speed = 0.37497622 * ((double)Cadence.value());
|
||||
} else {
|
||||
// Check if speed and cadence should be reset due to timeout (2 seconds)
|
||||
if (lastTS.msecsTo(now) > 2000) {
|
||||
if (Speed.value() > 0) {
|
||||
Speed = 0;
|
||||
qDebug() << "Speed reset to 0 due to timeout";
|
||||
}
|
||||
if (Cadence.value() > 0) {
|
||||
Cadence = 0;
|
||||
qDebug() << "Cadence reset to 0 due to timeout";
|
||||
}
|
||||
lastTS = now;
|
||||
}
|
||||
Speed = metric::calculateSpeedFromPower(
|
||||
watts(), Inclination.value(), Speed.value(),
|
||||
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
|
||||
}
|
||||
|
||||
if (watts())
|
||||
@@ -232,7 +214,7 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
@@ -429,7 +411,98 @@ bool apexbike::connected() {
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
uint16_t apexbike::watts() { return wattFromHR(true); }
|
||||
uint16_t apexbike::watts() {
|
||||
double resistance = Resistance.value();
|
||||
double cadence = Cadence.value();
|
||||
|
||||
if (cadence <= 0 || resistance <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Power table based on user-provided data
|
||||
// Format: resistance level (1-19), RPM (10-150 in steps of 10), power (watts)
|
||||
static const int powerTable[19][15] = {
|
||||
// Resistance 1: RPM 10,20,30,40,50,60,70,80,90,100,110,120,130,140,150
|
||||
{12, 24, 36, 48, 61, 73, 85, 97, 109, 121, 133, 145, 157, 170, 182},
|
||||
// Resistance 2
|
||||
{13, 27, 40, 53, 67, 80, 93, 107, 120, 133, 147, 160, 173, 187, 200},
|
||||
// Resistance 3
|
||||
{15, 29, 44, 58, 73, 87, 102, 117, 131, 146, 160, 175, 189, 204, 219},
|
||||
// Resistance 4
|
||||
{16, 32, 48, 64, 80, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239},
|
||||
// Resistance 5
|
||||
{17, 34, 51, 68, 85, 102, 118, 135, 152, 169, 186, 203, 220, 237, 254},
|
||||
// Resistance 6
|
||||
{18, 37, 55, 74, 92, 110, 129, 147, 165, 184, 202, 221, 239, 257, 276},
|
||||
// Resistance 7
|
||||
{19, 39, 58, 77, 97, 116, 136, 155, 174, 194, 213, 232, 252, 271, 291},
|
||||
// Resistance 8
|
||||
{21, 42, 62, 83, 104, 125, 146, 166, 187, 208, 229, 250, 271, 291, 312},
|
||||
// Resistance 9
|
||||
{22, 44, 66, 88, 110, 132, 154, 176, 198, 220, 242, 264, 286, 308, 330},
|
||||
// Resistance 10
|
||||
{23, 46, 69, 92, 116, 139, 162, 185, 208, 231, 254, 277, 300, 324, 347},
|
||||
// Resistance 11
|
||||
{24, 49, 73, 98, 122, 146, 171, 195, 219, 244, 268, 293, 317, 341, 366},
|
||||
// Resistance 12
|
||||
{26, 51, 77, 102, 128, 153, 179, 204, 230, 255, 281, 307, 332, 358, 383},
|
||||
// Resistance 13
|
||||
{27, 54, 80, 107, 134, 161, 188, 214, 241, 268, 295, 322, 348, 375, 402},
|
||||
// Resistance 14
|
||||
{28, 56, 83, 111, 139, 167, 195, 222, 250, 278, 306, 334, 362, 389, 417},
|
||||
// Resistance 15
|
||||
{29, 58, 87, 117, 146, 175, 204, 233, 262, 292, 321, 350, 379, 408, 437},
|
||||
// Resistance 16
|
||||
{30, 61, 91, 121, 152, 182, 212, 242, 273, 303, 333, 364, 394, 424, 455},
|
||||
// Resistance 17
|
||||
{32, 63, 95, 126, 158, 189, 221, 253, 284, 316, 347, 379, 410, 442, 473},
|
||||
// Resistance 18
|
||||
{33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495},
|
||||
// Resistance 19
|
||||
{34, 68, 102, 136, 171, 205, 239, 273, 307, 341, 375, 409, 443, 478, 512}
|
||||
};
|
||||
|
||||
// Clamp resistance to valid range (1-19)
|
||||
int res = qMax(1, qMin(19, (int)qRound(resistance)));
|
||||
|
||||
// Convert to array index (0-18)
|
||||
int resIndex = res - 1;
|
||||
|
||||
// RPM ranges from 10 to 150 in steps of 10
|
||||
// Find the two closest RPM values for interpolation
|
||||
double rpm = qMax(1.0, cadence); // Ensure RPM is at least 1
|
||||
|
||||
if (rpm <= 10.0) {
|
||||
// Below minimum RPM, extrapolate from first data point
|
||||
double factor = rpm / 10.0;
|
||||
return (uint16_t)qMax(0.0, powerTable[resIndex][0] * factor);
|
||||
}
|
||||
|
||||
if (rpm >= 150.0) {
|
||||
// Above maximum RPM, extrapolate from last data point
|
||||
double factor = rpm / 150.0;
|
||||
return (uint16_t)qMax(0.0, powerTable[resIndex][14] * factor);
|
||||
}
|
||||
|
||||
// Find the two RPM values to interpolate between
|
||||
// RPM values are: 10, 20, 30, ..., 150 (indices 0-14)
|
||||
int lowerRpmIndex = ((int)rpm - 1) / 10; // Convert RPM to array index
|
||||
if (lowerRpmIndex > 13) lowerRpmIndex = 13; // Ensure we don't go out of bounds
|
||||
|
||||
int upperRpmIndex = lowerRpmIndex + 1;
|
||||
|
||||
double lowerRpm = (lowerRpmIndex + 1) * 10.0; // Convert index back to RPM
|
||||
double upperRpm = (upperRpmIndex + 1) * 10.0;
|
||||
|
||||
int lowerPower = powerTable[resIndex][lowerRpmIndex];
|
||||
int upperPower = powerTable[resIndex][upperRpmIndex];
|
||||
|
||||
// Linear interpolation between the two power values
|
||||
double ratio = (rpm - lowerRpm) / (upperRpm - lowerRpm);
|
||||
double interpolatedPower = lowerPower + ratio * (upperPower - lowerPower);
|
||||
|
||||
return (uint16_t)qMax(0.0, interpolatedPower);
|
||||
}
|
||||
|
||||
void apexbike::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
|
||||
@@ -19,7 +19,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
bhfitnesselliptical::bhfitnesselliptical(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "qdebugfixup.h"
|
||||
#include "homeform.h"
|
||||
#include <QSettings>
|
||||
#include <cmath>
|
||||
|
||||
bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); }
|
||||
|
||||
@@ -70,6 +71,11 @@ void bike::changePower(int32_t power) {
|
||||
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
|
||||
double erg_filter_lower =
|
||||
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
|
||||
|
||||
// Apply bike power offset
|
||||
int bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toInt();
|
||||
power += bike_power_offset;
|
||||
qDebug() << QStringLiteral("changePower: original power with offset applied: ") + QString::number(power) + QStringLiteral(" (offset: ") + QString::number(bike_power_offset) + QStringLiteral(")");
|
||||
|
||||
requestPower = power; // used by some bikes that have ERG mode builtin
|
||||
|
||||
@@ -115,27 +121,60 @@ void bike::setGears(double gears) {
|
||||
gears -= gears_offset;
|
||||
qDebug() << "setGears" << gears;
|
||||
|
||||
// Check for boundaries and emit failure signals
|
||||
// Gear boundary handling with smart clamping logic:
|
||||
// - If we're trying to set a gear outside valid range AND we're already at a valid gear,
|
||||
// reject the change (normal case: user at gear 1 tries to go to 0.5, should fail)
|
||||
// - If we're trying to set a gear outside valid range BUT we're currently below minimum,
|
||||
// clamp to valid range (startup case: system starts at 0, first gearUp with 0.5 gain
|
||||
// goes to 0.5, should be clamped to 1 to allow the system to reach valid state)
|
||||
// This prevents the system from getting stuck below minGears due to fractional gains
|
||||
// while preserving normal boundary rejection behavior for users at valid gear positions
|
||||
if(gears_zwift_ratio && (gears > 24 || gears < 1)) {
|
||||
qDebug() << "new gear value ignored because of gears_zwift_ratio setting!";
|
||||
if(gears > 24) {
|
||||
emit gearFailedUp();
|
||||
if(m_gears >= 24) {
|
||||
qDebug() << "new gear value ignored - already at zwift ratio maximum: 24";
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to zwift ratio maximum: 24";
|
||||
gears = 24;
|
||||
emit gearFailedUp();
|
||||
}
|
||||
} else {
|
||||
emit gearFailedDown();
|
||||
if(m_gears >= 1) {
|
||||
qDebug() << "new gear value ignored - already at zwift ratio minimum: 1";
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to zwift ratio minimum: 1";
|
||||
gears = 1;
|
||||
emit gearFailedDown();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(gears > maxGears()) {
|
||||
qDebug() << "new gear value ignored because of maxGears" << maxGears();
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
if(m_gears >= maxGears()) {
|
||||
qDebug() << "new gear value ignored - already at maxGears" << maxGears();
|
||||
emit gearFailedUp();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to maxGears" << maxGears();
|
||||
gears = maxGears();
|
||||
emit gearFailedUp();
|
||||
}
|
||||
}
|
||||
|
||||
if(gears < minGears()) {
|
||||
qDebug() << "new gear value ignored because of minGears" << minGears();
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
if(m_gears >= minGears()) {
|
||||
qDebug() << "new gear value ignored - already at or above minGears" << minGears();
|
||||
emit gearFailedDown();
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "gear value clamped to minGears" << minGears();
|
||||
gears = minGears();
|
||||
emit gearFailedDown();
|
||||
}
|
||||
}
|
||||
|
||||
if(m_gears > gears) {
|
||||
@@ -173,7 +212,7 @@ resistance_t bike::resistanceFromPowerRequest(uint16_t power) { return power / 1
|
||||
void bike::cadenceSensor(uint8_t cadence) { Cadence.setValue(cadence); }
|
||||
void bike::powerSensor(uint16_t power) { m_watt.setValue(power, false); }
|
||||
|
||||
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
|
||||
BLUETOOTH_TYPE bike::deviceType() { return BIKE; }
|
||||
|
||||
void bike::clearStats() {
|
||||
|
||||
@@ -343,6 +382,8 @@ uint8_t bike::metrics_override_heartrate() {
|
||||
|
||||
bool bike::inclinationAvailableByHardware() { return false; }
|
||||
|
||||
bool bike::inclinationAvailableBySoftware() { return false; }
|
||||
|
||||
uint16_t bike::wattFromHR(bool useSpeedAndCadence) {
|
||||
QSettings settings;
|
||||
double watt = 0;
|
||||
@@ -428,7 +469,136 @@ double bike::gearsZwiftRatio() {
|
||||
case 23:
|
||||
return 5.14;
|
||||
case 24:
|
||||
return 5.49;
|
||||
return 5.49;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sim mode support: Physics-based power calculation from slope gradient
|
||||
double bike::computeSlopeTargetPower(double gradePercent, double speedKmh) {
|
||||
QSettings settings;
|
||||
const double riderWeight = settings.value(QZSettings::weight, QZSettings::default_weight).toDouble();
|
||||
const double bikeWeight = settings.value(QZSettings::bike_weight, QZSettings::default_bike_weight).toDouble();
|
||||
const double rollingCoeff = settings.value(QZSettings::rolling_resistance, QZSettings::default_rolling_resistance).toDouble();
|
||||
|
||||
double totalMass = riderWeight + bikeWeight;
|
||||
if (!std::isfinite(totalMass) || totalMass < 1.0) {
|
||||
totalMass = 75.0 + 10.0; // fallback to reasonable defaults
|
||||
}
|
||||
|
||||
double speedMs = speedKmh / 3.6; // convert km/h to m/s
|
||||
if (!std::isfinite(speedMs) || speedMs < 0.0) {
|
||||
speedMs = 0.0;
|
||||
}
|
||||
|
||||
// Calculate slope angle components
|
||||
const double slope = gradePercent / 100.0;
|
||||
const double denom = std::sqrt(1.0 + slope * slope);
|
||||
const double sinTheta = (denom > 0.0) ? (slope / denom) : 0.0;
|
||||
const double cosTheta = (denom > 0.0) ? (1.0 / denom) : 1.0;
|
||||
|
||||
const double g = 9.80665; // m/s² - gravitational acceleration
|
||||
|
||||
// 1. Gravitational resistance (climbing/descending)
|
||||
double powerGravity = totalMass * g * speedMs * sinTheta;
|
||||
|
||||
// 2. Rolling resistance
|
||||
double powerRolling = totalMass * g * rollingCoeff * speedMs * cosTheta;
|
||||
|
||||
// 3. Aerodynamic resistance
|
||||
const double airDensity = 1.204; // kg/m³ at 20°C
|
||||
const double dragCoefficient = 0.4; // Cd - typical cycling position
|
||||
const double frontalArea = 1.0; // m² - approximate frontal area
|
||||
double cda = dragCoefficient * frontalArea;
|
||||
double powerAerodynamic = 0.5 * airDensity * cda * std::pow(std::max(0.0, speedMs), 3);
|
||||
|
||||
// Total power required
|
||||
double totalPower = powerGravity + powerRolling + powerAerodynamic;
|
||||
if (!std::isfinite(totalPower)) {
|
||||
totalPower = 0.0;
|
||||
}
|
||||
if (totalPower < 0.0) {
|
||||
totalPower = 0.0;
|
||||
}
|
||||
|
||||
qDebug() << "computeSlopeTargetPower grade%:" << gradePercent
|
||||
<< "speedKmh:" << speedKmh
|
||||
<< "powerGravity:" << powerGravity
|
||||
<< "powerRolling:" << powerRolling
|
||||
<< "powerAero:" << powerAerodynamic
|
||||
<< "total:" << totalPower;
|
||||
|
||||
return totalPower;
|
||||
}
|
||||
|
||||
// Helper: get current speed for slope calculations with fallback to cadence-based estimation
|
||||
double bike::getCurrentSpeedForSlope() {
|
||||
double speedKmh = Speed.value();
|
||||
if (!std::isfinite(speedKmh) || speedKmh < 0.0) {
|
||||
speedKmh = 0.0;
|
||||
}
|
||||
|
||||
// If speed is very low, estimate from cadence
|
||||
if (speedKmh < 5.0) {
|
||||
double cadence = Cadence.value();
|
||||
if (std::isfinite(cadence) && cadence > 0.0) {
|
||||
// Rough approximation: 90 RPM ≈ 27 km/h
|
||||
speedKmh = std::max(0.5, cadence * 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
return speedKmh;
|
||||
}
|
||||
|
||||
// Update power target based on current slope and speed
|
||||
void bike::updateSlopeTargetPower(bool force) {
|
||||
qDebug() << "updateSlopeTargetPower called - force:" << force
|
||||
<< "autoRes:" << autoResistance()
|
||||
<< "slopeEnabled:" << m_slopeControlEnabled
|
||||
<< "currentGrade:" << m_currentSlopePercent;
|
||||
|
||||
if (!autoResistance()) {
|
||||
qDebug() << "updateSlopeTargetPower skipped: auto resistance disabled";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_slopeControlEnabled && !force) {
|
||||
qDebug() << "updateSlopeTargetPower skipped: slope control inactive";
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply gear offset to grade (0.5 scaling factor)
|
||||
double grade = m_currentSlopePercent + (gears() / 2.0);
|
||||
|
||||
// Get current speed (with fallback to cadence-based estimation)
|
||||
double speedKmh = getCurrentSpeedForSlope();
|
||||
|
||||
// Compute required power using physics model
|
||||
double targetPower = computeSlopeTargetPower(grade, speedKmh);
|
||||
int powerValue = static_cast<int>(std::round(targetPower));
|
||||
powerValue = qBound(0, powerValue, 2000);
|
||||
|
||||
// Hysteresis: avoid too frequent changes
|
||||
if (!force) {
|
||||
if (!m_slopePowerTimer.isValid()) {
|
||||
m_slopePowerTimer.start();
|
||||
}
|
||||
if (m_slopePowerTimer.elapsed() < 500 &&
|
||||
m_lastSlopeTargetPower >= 0 &&
|
||||
std::abs(powerValue - m_lastSlopeTargetPower) < 3) {
|
||||
qDebug() << "updateSlopeTargetPower skipped: within hysteresis"
|
||||
<< powerValue << "vs" << m_lastSlopeTargetPower;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply power change
|
||||
m_lastSlopeTargetPower = powerValue;
|
||||
m_slopePowerTimer.restart();
|
||||
m_slopePowerChangeInProgress = true;
|
||||
|
||||
qDebug() << "updateSlopeTargetPower -> changePower:" << powerValue;
|
||||
changePower(powerValue);
|
||||
|
||||
m_slopePowerChangeInProgress = false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "devices/bluetoothdevice.h"
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include <QObject>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
class bike : public bluetoothdevice {
|
||||
|
||||
@@ -31,7 +32,7 @@ class bike : public bluetoothdevice {
|
||||
virtual resistance_t resistanceFromPowerRequest(uint16_t power);
|
||||
virtual uint16_t powerFromResistanceRequest(resistance_t requestResistance);
|
||||
virtual bool ergManagedBySS2K() { return false; }
|
||||
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
|
||||
BLUETOOTH_TYPE deviceType() override;
|
||||
metric pelotonResistance();
|
||||
void clearStats() override;
|
||||
void setLap() override;
|
||||
@@ -51,7 +52,9 @@ class bike : public bluetoothdevice {
|
||||
*/
|
||||
metric currentSteeringAngle() { return m_steeringAngle; }
|
||||
virtual bool inclinationAvailableByHardware();
|
||||
virtual bool inclinationAvailableBySoftware();
|
||||
bool ergModeSupportedAvailableByHardware() { return ergModeSupported; }
|
||||
virtual bool ergModeSupportedAvailableBySoftware() { return ergModeSupported; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void changeResistance(resistance_t res) override;
|
||||
@@ -109,6 +112,25 @@ class bike : public bluetoothdevice {
|
||||
|
||||
double m_speedLimit = 0;
|
||||
|
||||
// Sim mode support: convert inclination to power for devices without native inclination
|
||||
bool m_slopeControlEnabled = false;
|
||||
double m_currentSlopePercent = 0.0;
|
||||
int m_lastSlopeTargetPower = -1;
|
||||
bool m_slopePowerChangeInProgress = false;
|
||||
QElapsedTimer m_slopePowerTimer;
|
||||
|
||||
// Physics-based power calculation from slope
|
||||
virtual double computeSlopeTargetPower(double gradePercent, double speedKmh);
|
||||
|
||||
// Update power based on current slope and speed (called periodically)
|
||||
virtual void updateSlopeTargetPower(bool force = false);
|
||||
|
||||
// Check if device supports native inclination control
|
||||
virtual bool supportsNativeInclination() const { return true; }
|
||||
|
||||
// Helper: get current speed for slope calculations
|
||||
double getCurrentSpeedForSlope();
|
||||
|
||||
uint16_t wattFromHR(bool useSpeedAndCadence);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
bkoolbike::bkoolbike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -49,7 +49,12 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
|
||||
if (gattWriteCharCustomId.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
|
||||
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer,
|
||||
QLowEnergyService::WriteWithoutResponse);
|
||||
} else {
|
||||
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
|
||||
}
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
|
||||
@@ -61,19 +66,28 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
|
||||
|
||||
void bkoolbike::changePower(int32_t power) {
|
||||
RequestedPower = power;
|
||||
/*
|
||||
if (power < 0)
|
||||
power = 0;
|
||||
uint8_t p[] = {0xa4, 0x09, 0x4e, 0x05, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x02, 0x00};
|
||||
p[10] = (uint8_t)((power * 4) & 0xFF);
|
||||
p[11] = (uint8_t)((power * 4) >> 8);
|
||||
for (uint8_t i = 0; i < sizeof(p) - 1; i++) {
|
||||
p[12] ^= p[i]; // the last byte is a sort of a checksum
|
||||
}
|
||||
|
||||
writeCharacteristic(p, sizeof(p), QStringLiteral("changePower"), false, false);*/
|
||||
if (power < 0) {
|
||||
power = 0;
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("Changepower not implemented");
|
||||
forcePower(power);
|
||||
}
|
||||
|
||||
void bkoolbike::forcePower(int32_t power) {
|
||||
// FE-C "Set Target Power" command (page 0x31)
|
||||
// Power is sent in 1/4 watt units (0.25W resolution)
|
||||
// Bytes: [0x31][0x25][0xFF][0xFF][0xFF][0xFF][power_low][power_high]
|
||||
|
||||
uint16_t power_quarter_watts = (uint16_t)(power * 4);
|
||||
|
||||
uint8_t power_cmd[] = {0x31, 0x25, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00};
|
||||
power_cmd[6] = (uint8_t)(power_quarter_watts & 0xFF); // Low byte
|
||||
power_cmd[7] = (uint8_t)((power_quarter_watts >> 8) & 0xFF); // High byte
|
||||
|
||||
writeCharacteristic(power_cmd, sizeof(power_cmd),
|
||||
QStringLiteral("forcePower ") + QString::number(power) + QStringLiteral("W"),
|
||||
false, false);
|
||||
}
|
||||
|
||||
void bkoolbike::forceInclination(double inclination) {
|
||||
@@ -115,13 +129,27 @@ void bkoolbike::update() {
|
||||
uint8_t init1[] = {0x30, 0x25, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
|
||||
uint8_t init2[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x1e, 0x7f, 0x00};
|
||||
uint8_t init3[] = {0x33, 0x25, 0xff, 0xff, 0xff, 0x20, 0x4e, 0x00};
|
||||
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
|
||||
uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21};
|
||||
writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, false);
|
||||
writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, false);
|
||||
writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false);
|
||||
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
|
||||
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
|
||||
|
||||
if (bkool_fitness_bike) {
|
||||
// BKOOLFITNESSBIKE specific init packets
|
||||
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
|
||||
uint8_t init5[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
|
||||
uint8_t init6[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
|
||||
uint8_t init7[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x25, 0x7f, 0x00};
|
||||
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
|
||||
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
|
||||
writeCharacteristic(init6, sizeof(init6), QStringLiteral("init6"), false, true);
|
||||
writeCharacteristic(init7, sizeof(init7), QStringLiteral("init7"), false, false);
|
||||
} else {
|
||||
// BKOOLSMARTPRO init packets
|
||||
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
|
||||
uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21};
|
||||
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
|
||||
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
|
||||
}
|
||||
|
||||
} else if (bluetoothDevice.isValid() &&
|
||||
m_control->state() == QLowEnergyController::DiscoveredState //&&
|
||||
@@ -137,6 +165,13 @@ void bkoolbike::update() {
|
||||
// updateDisplay(elapsed);
|
||||
}
|
||||
|
||||
// Send poll command for BKOOLFITNESSBIKE
|
||||
/*
|
||||
if (bkool_fitness_bike) {
|
||||
uint8_t poll[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
|
||||
writeCharacteristic(poll, sizeof(poll), QStringLiteral("poll"), false, false);
|
||||
}*/
|
||||
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance != currentResistance().value() || lastGearValue != gears()) {
|
||||
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
|
||||
@@ -146,10 +181,15 @@ void bkoolbike::update() {
|
||||
requestInclination = requestResistance / 10.0;
|
||||
}
|
||||
// forceResistance(requestResistance);;
|
||||
}
|
||||
lastGearValue = gears();
|
||||
}
|
||||
requestResistance = -1;
|
||||
}
|
||||
|
||||
if(lastGearValue != gears() && requestInclination == -100) {
|
||||
// if only gears changed, we need to update the inclination to match the gears
|
||||
requestInclination = lastRawRequestedInclinationValue;
|
||||
}
|
||||
|
||||
if (requestInclination != -100) {
|
||||
emit debug(QStringLiteral("writing inclination ") + QString::number(requestInclination));
|
||||
forceInclination(requestInclination + gears()); // since this bike doesn't have the concept of resistance,
|
||||
@@ -157,6 +197,8 @@ void bkoolbike::update() {
|
||||
requestInclination = -100;
|
||||
}
|
||||
|
||||
lastGearValue = gears();
|
||||
|
||||
if (requestPower != -1) {
|
||||
changePower(requestPower);
|
||||
requestPower = -1;
|
||||
@@ -207,60 +249,65 @@ void bkoolbike::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
if (characteristic.uuid() == QBluetoothUuid((quint16)0x2A5B)) {
|
||||
lastPacket = newValue;
|
||||
|
||||
uint8_t index = 1;
|
||||
// Only parse and update cadence from internal CSC if no external cadence sensor configured
|
||||
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
uint8_t index = 1;
|
||||
|
||||
if (newValue.at(0) == 0x02 && newValue.length() < 4) {
|
||||
emit debug(QStringLiteral("Crank revolution data present with wrong bytes ") +
|
||||
QString::number(newValue.length()));
|
||||
return;
|
||||
} else if (newValue.at(0) == 0x01 && newValue.length() < 6) {
|
||||
emit debug(QStringLiteral("Wheel revolution data present with wrong bytes ") +
|
||||
QString::number(newValue.length()));
|
||||
return;
|
||||
} else if (newValue.at(0) == 0x00) {
|
||||
emit debug(QStringLiteral("Cadence sensor notification without datas ") +
|
||||
QString::number(newValue.length()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.at(0) == 0x02) {
|
||||
CrankRevsRead =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
} else if (newValue.at(0) == 0x03) {
|
||||
index += 6;
|
||||
CrankRevsRead =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
} else {
|
||||
return;
|
||||
// CrankRevsRead = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) |
|
||||
// ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8)
|
||||
// | (uint32_t)((uint8_t)newValue.at(index)));
|
||||
}
|
||||
if (newValue.at(0) == 0x01) {
|
||||
index += 4;
|
||||
} else {
|
||||
index += 2;
|
||||
}
|
||||
uint16_t LastCrankEventTimeRead =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
|
||||
int16_t deltaT = LastCrankEventTimeRead - oldLastCrankEventTime;
|
||||
if (deltaT < 0) {
|
||||
deltaT = LastCrankEventTimeRead + 65535 - oldLastCrankEventTime;
|
||||
}
|
||||
|
||||
if (CrankRevsRead != oldCrankRevs && deltaT) {
|
||||
double cadence = (((double)CrankRevsRead - (double)oldCrankRevs) / (double)deltaT) * 1024.0 * 60.0;
|
||||
if (cadence >= 0 && cadence < 255) {
|
||||
Cadence = cadence;
|
||||
if (newValue.at(0) == 0x02 && newValue.length() < 4) {
|
||||
emit debug(QStringLiteral("Crank revolution data present with wrong bytes ") +
|
||||
QString::number(newValue.length()));
|
||||
return;
|
||||
} else if (newValue.at(0) == 0x01 && newValue.length() < 6) {
|
||||
emit debug(QStringLiteral("Wheel revolution data present with wrong bytes ") +
|
||||
QString::number(newValue.length()));
|
||||
return;
|
||||
} else if (newValue.at(0) == 0x00) {
|
||||
emit debug(QStringLiteral("Cadence sensor notification without datas ") +
|
||||
QString::number(newValue.length()));
|
||||
return;
|
||||
}
|
||||
lastGoodCadence = now;
|
||||
} else if (lastGoodCadence.msecsTo(now) > 2000) {
|
||||
Cadence = 0;
|
||||
}
|
||||
|
||||
oldLastCrankEventTime = LastCrankEventTimeRead;
|
||||
oldCrankRevs = CrankRevsRead;
|
||||
if (newValue.at(0) == 0x02) {
|
||||
CrankRevsRead =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
} else if (newValue.at(0) == 0x03) {
|
||||
index += 6;
|
||||
CrankRevsRead =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
} else {
|
||||
return;
|
||||
// CrankRevsRead = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) |
|
||||
// ((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) | ((uint32_t)((uint8_t)newValue.at(index + 1)) << 8)
|
||||
// | (uint32_t)((uint8_t)newValue.at(index)));
|
||||
}
|
||||
if (newValue.at(0) == 0x01) {
|
||||
index += 4;
|
||||
} else {
|
||||
index += 2;
|
||||
}
|
||||
uint16_t LastCrankEventTimeRead =
|
||||
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
|
||||
|
||||
int16_t deltaT = LastCrankEventTimeRead - oldLastCrankEventTime;
|
||||
if (deltaT < 0) {
|
||||
deltaT = LastCrankEventTimeRead + 65535 - oldLastCrankEventTime;
|
||||
}
|
||||
|
||||
if (CrankRevsRead != oldCrankRevs && deltaT) {
|
||||
double cadence = (((double)CrankRevsRead - (double)oldCrankRevs) / (double)deltaT) * 1024.0 * 60.0;
|
||||
if (cadence >= 0 && cadence < 255) {
|
||||
Cadence = cadence;
|
||||
}
|
||||
lastGoodCadence = now;
|
||||
} else if (lastGoodCadence.msecsTo(now) > 2000) {
|
||||
Cadence = 0;
|
||||
}
|
||||
|
||||
oldLastCrankEventTime = LastCrankEventTimeRead;
|
||||
oldCrankRevs = CrankRevsRead;
|
||||
}
|
||||
|
||||
Speed = Cadence.value() *
|
||||
settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio)
|
||||
@@ -677,6 +724,12 @@ void bkoolbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
{
|
||||
bluetoothDevice = device;
|
||||
|
||||
// Check if this is BKOOLFITNESSBIKE model
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) {
|
||||
bkool_fitness_bike = true;
|
||||
emit debug(QStringLiteral("BKOOLFITNESSBIKE model detected"));
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &bkoolbike::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &bkoolbike::serviceScanDone);
|
||||
|
||||
@@ -45,6 +45,7 @@ class bkoolbike : public bike {
|
||||
bool wait_for_response = false);
|
||||
void startDiscover();
|
||||
void forceInclination(double inclination);
|
||||
void forcePower(int32_t power);
|
||||
uint16_t watts() override;
|
||||
double bikeResistanceToPeloton(double resistance);
|
||||
|
||||
@@ -70,6 +71,8 @@ class bkoolbike : public bike {
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
bool bkool_fitness_bike = false;
|
||||
uint16_t pollCounter = 0;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
|
||||
@@ -36,6 +36,10 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc
|
||||
|
||||
QString nordictrack_2950_ip =
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
bool fake_bike =
|
||||
settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool();
|
||||
bool fake_treadmill =
|
||||
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
|
||||
|
||||
if (settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool() && !pelotonBike) {
|
||||
pelotonBike = new pelotonbike(noWriteResistance, noHeartService);
|
||||
@@ -47,7 +51,29 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(pelotonBike);
|
||||
}
|
||||
}/* else if (fake_bike) {
|
||||
fakeBike = new fakebike(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(QBluetoothDeviceInfo());
|
||||
connect(fakeBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
connect(fakeBike, &fakebike::debug, this, &bluetooth::debug);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(fakeBike);
|
||||
return;
|
||||
} else if (fake_treadmill) {
|
||||
fakeTreadmill = new faketreadmill(noWriteResistance, noHeartService, false);
|
||||
emit deviceConnected(QBluetoothDeviceInfo());
|
||||
connect(fakeTreadmill, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
connect(fakeTreadmill, &faketreadmill::debug, this, &bluetooth::debug);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
// this signal is not associated to anything in this moment, since the homeform is not loaded yet
|
||||
this->signalBluetoothDeviceConnected(fakeBike);
|
||||
return;
|
||||
}*/
|
||||
|
||||
#ifdef TEST
|
||||
schwinnIC4Bike = (schwinnic4bike *)new bike();
|
||||
@@ -115,6 +141,7 @@ void bluetooth::finished() {
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
|
||||
QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString();
|
||||
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
bool fake_bike =
|
||||
settings.value(QZSettings::applewatch_fakedevice, QZSettings::default_applewatch_fakedevice).toBool();
|
||||
bool fakedevice_elliptical =
|
||||
@@ -123,7 +150,7 @@ void bluetooth::finished() {
|
||||
bool fakedevice_treadmill =
|
||||
settings.value(QZSettings::fakedevice_treadmill, QZSettings::default_fakedevice_treadmill).toBool();
|
||||
// wifi devices on windows
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || antbike || android_antbike) {
|
||||
if (!nordictrack_2950_ip.isEmpty() || !tdf_10_ip.isEmpty() || fake_bike || fakedevice_elliptical || fakedevice_rower || fakedevice_treadmill || !proform_elliptical_ip.isEmpty() || !proform_rower_ip.isEmpty() || antbike || android_antbike) {
|
||||
// faking a bluetooth device
|
||||
qDebug() << "faking a bluetooth device for nordictrack_2950_ip";
|
||||
deviceDiscovered(QBluetoothDeviceInfo());
|
||||
@@ -423,7 +450,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool() ||
|
||||
settings.value(QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike).toBool() ||
|
||||
settings.value(QZSettings::toorx_bike_srx_500, QZSettings::default_toorx_bike_srx_500).toBool() ||
|
||||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool()) &&
|
||||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool() ||
|
||||
settings.value(QZSettings::taurua_ic90, QZSettings::default_taurua_ic90).toBool()) &&
|
||||
!toorx_ftms;
|
||||
bool snode_bike = settings.value(QZSettings::snode_bike, QZSettings::default_snode_bike).toBool();
|
||||
bool fitplus_bike = settings.value(QZSettings::fitplus_bike, QZSettings::default_fitplus_bike).toBool() ||
|
||||
@@ -440,6 +468,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
bool hammerRacerS = settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool();
|
||||
bool flywheel_life_fitness_ic8 =
|
||||
settings.value(QZSettings::flywheel_life_fitness_ic8, QZSettings::default_flywheel_life_fitness_ic8).toBool();
|
||||
bool life_fitness_ic5 =
|
||||
settings.value(QZSettings::life_fitness_ic5, QZSettings::default_life_fitness_ic5).toBool();
|
||||
bool technogym_bike =
|
||||
settings.value(QZSettings::technogym_bike, QZSettings::default_technogym_bike).toBool();
|
||||
QString powerSensorName =
|
||||
settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name).toString();
|
||||
QString eliteRizerName =
|
||||
@@ -470,8 +502,11 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString();
|
||||
QString tdf_10_ip = settings.value(QZSettings::tdf_10_ip, QZSettings::default_tdf_10_ip).toString();
|
||||
QString proform_elliptical_ip = settings.value(QZSettings::proform_elliptical_ip, QZSettings::default_proform_elliptical_ip).toString();
|
||||
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
|
||||
QString computrainerSerialPort =
|
||||
settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
|
||||
QString kettlerUsbSerialPort =
|
||||
settings.value(QZSettings::kettler_usb_serialport, QZSettings::default_kettler_usb_serialport).toString();
|
||||
QString csaferowerSerialPort = settings.value(QZSettings::csafe_rower, QZSettings::default_csafe_rower).toString();
|
||||
QString csafeellipticalSerialPort =
|
||||
settings.value(QZSettings::csafe_elliptical_port, QZSettings::default_csafe_elliptical_port).toString();
|
||||
@@ -493,6 +528,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
QString ftms_rower = settings.value(QZSettings::ftms_rower, QZSettings::default_ftms_rower).toString();
|
||||
QString ftms_bike = settings.value(QZSettings::ftms_bike, QZSettings::default_ftms_bike).toString();
|
||||
QString ftms_treadmill = settings.value(QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill).toString();
|
||||
QString ftms_elliptical = settings.value(QZSettings::ftms_elliptical, QZSettings::default_ftms_elliptical).toString();
|
||||
bool saris_trainer = settings.value(QZSettings::saris_trainer, QZSettings::default_saris_trainer).toBool();
|
||||
bool iconsole_elliptical = settings.value(QZSettings::iconsole_elliptical, QZSettings::default_iconsole_elliptical).toBool();
|
||||
bool iconsole_rower = settings.value(QZSettings::iconsole_rower, QZSettings::default_iconsole_rower).toBool();
|
||||
@@ -547,7 +583,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
!b.compare(settings.value(QZSettings::filter_device, QZSettings::default_filter_device).toString()) &&
|
||||
(b.toUpper().startsWith("IC BIKE") || b.toUpper().startsWith("C7-"))) {
|
||||
|
||||
this->stopDiscovery();
|
||||
// Don't stop discovery here to allow time for accessories (heart rate, power, cadence sensors) to be found
|
||||
// Discovery will stop naturally after 10 seconds or when all configured accessories are discovered
|
||||
schwinnIC4Bike = new schwinnic4bike(noWriteResistance, noHeartService);
|
||||
// stateFileRead();
|
||||
QBluetoothDeviceInfo bt;
|
||||
@@ -655,7 +692,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
|
||||
filter = (b.name().compare(filterDevice, Qt::CaseInsensitive) == 0);
|
||||
}
|
||||
if (b.name().startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
|
||||
const QString deviceName = b.name();
|
||||
const QString upperDeviceName = deviceName.toUpper();
|
||||
bool isRI009R = upperDeviceName.contains(QStringLiteral("RI009R"));
|
||||
bool isTrxAppGateUsbBikeTC = false;
|
||||
if (upperDeviceName.startsWith(QStringLiteral("TC")) && deviceName.length() == 5) {
|
||||
isTrxAppGateUsbBikeTC = true;
|
||||
for (int idx = 2; idx < deviceName.length(); ++idx) {
|
||||
if (!deviceName.at(idx).isDigit()) {
|
||||
isTrxAppGateUsbBikeTC = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deviceName.startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
|
||||
|
||||
if (m3ibike::isCorrectUnit(b)) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -777,6 +827,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(computrainerBike);
|
||||
} else if (!kettlerUsbSerialPort.isEmpty() && !kettlerUsbBike) {
|
||||
this->stopDiscovery();
|
||||
kettlerUsbBike =
|
||||
new kettlerusbbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
|
||||
emit deviceConnected(b);
|
||||
connect(kettlerUsbBike, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(kettlerUsbBike, &kettlerusbbike::debug, this, &bluetooth::debug);
|
||||
kettlerUsbBike->deviceDiscovered(b);
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(kettlerUsbBike);
|
||||
} else if (!csaferowerSerialPort.isEmpty() && !csafeRower) {
|
||||
this->stopDiscovery();
|
||||
csafeRower = new csaferower(noWriteResistance, noHeartService, false);
|
||||
@@ -890,8 +953,23 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbElliptical);
|
||||
} else if (!proform_rower_ip.isEmpty() && !nordictrackifitadbRower) {
|
||||
this->stopDiscovery();
|
||||
nordictrackifitadbRower = new nordictrackifitadbrower(noWriteResistance, noHeartService,
|
||||
bikeResistanceOffset, bikeResistanceGain);
|
||||
emit deviceConnected(b);
|
||||
connect(nordictrackifitadbRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
connect(nordictrackifitadbRower, &nordictrackifitadbrower::debug, this, &bluetooth::debug);
|
||||
// nordictrackifitadbRower->deviceDiscovered(b);
|
||||
// connect(this, SIGNAL(searchingStop()), nordictrackifitadbRower, SLOT(searchingStop())); //NOTE: Commented due to #358
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbRower);
|
||||
} else if (((csc_as_bike && b.name().startsWith(cscName)) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BGYM")) && b.name().length() == 8)) &&
|
||||
!cscBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -923,6 +1001,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((((power_as_treadmill && b.name().startsWith(powerSensorName))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (deviceHasService(b, QBluetoothUuid((quint16)0x1814)))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S10")) && deviceHasService(b, QBluetoothUuid((quint16)0x1814))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("NOHRD SPRINTBOK"))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ZWIFT RUNPOD"))) &&
|
||||
!powerTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -940,7 +1019,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(powerTreadmill);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("DOMYOS-ROW")) &&
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && filter) {
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && ftms_rower.contains(QZSettings::default_ftms_rower) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
domyosRower = new domyosrower(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
|
||||
@@ -957,7 +1036,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(domyosRower);
|
||||
} else if ((b.name().startsWith(QStringLiteral("Domyos-Bike")) && (!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool())) &&
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) {
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
|
||||
@@ -972,7 +1051,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(domyosBike);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower &&
|
||||
} else if ((((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) && iconsole_rower)) &&
|
||||
!trxappgateusbRower && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -991,7 +1071,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(trxappgateusbRower);
|
||||
} else if (((b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && !toorx_bike) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_elliptical)) &&
|
||||
!trxappgateusbElliptical && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
!trxappgateusbElliptical && ftms_bike.contains(QZSettings::default_ftms_bike) && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbElliptical = new trxappgateusbelliptical(noWriteResistance, noHeartService,
|
||||
@@ -1026,14 +1106,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(domyosElliptical);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SCH411/510E")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FEIER-EM-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MX-AS ")) ||
|
||||
(b.name().startsWith(QStringLiteral("Domyos-EL")) && settings.value(QZSettings::domyos_elliptical_fmts, QZSettings::default_domyos_elliptical_fmts).toBool()) ||
|
||||
(b.name().toUpper().startsWith("SF-") && b.name().midRef(3).toInt() > 0) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MYELLIPTICAL ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("CARDIOPOWER EEGO")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("E35")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical)) && !ypooElliptical && filter) {
|
||||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical) ||
|
||||
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && !horizonTreadmill && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
ypooElliptical =
|
||||
@@ -1184,6 +1267,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(soleElliptical);
|
||||
} else if (b.name().startsWith(QStringLiteral("Domyos")) &&
|
||||
!b.name().startsWith(QStringLiteral("DomyosBr")) &&
|
||||
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKE-")) &&
|
||||
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKING-")) && !domyos && !domyosElliptical && b.name().compare(ftms_treadmill, Qt::CaseInsensitive) &&
|
||||
!domyosBike && !domyosRower && !ftmsBike && !horizonTreadmill &&
|
||||
(!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts).toBool()) &&
|
||||
@@ -1299,7 +1383,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
if (this->discoveryAgent && !this->discoveryAgent->isActive())
|
||||
emit searchingStop();
|
||||
this->signalBluetoothDeviceConnected(shuaA5Treadmill);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("TRUE")) ||
|
||||
} else if (((b.name().toUpper().startsWith(QStringLiteral("TRUE")) &&
|
||||
!(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) && b.name().length() == 19)) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ASSAULT TREADMILL ")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WDWAY")) && b.name().length() == 8) || // WdWay179
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && !gem_module_inclination && !deviceHasService(b, QBluetoothUuid((quint16)0x1814)) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) &&
|
||||
@@ -1330,6 +1415,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("TRX7.5")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S77")) && sole_inclination) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F85")) && sole_inclination)) &&
|
||||
ftms_treadmill.contains(QZSettings::default_ftms_treadmill) &&
|
||||
!soleF80 && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1399,6 +1485,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(lifefitnessTreadmill);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("HORIZON")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("HZ_T101-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("AFG SPORT")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT2541")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (gem_module_inclination || deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) ||
|
||||
@@ -1431,42 +1518,59 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("XTERRA TR")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("T118_")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM4500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM6500")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("RUNN ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T9_")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BFX_T")) ||
|
||||
(b.name().toUpper().startsWith("3G PRO ")) ||
|
||||
(b.name().toUpper().startsWith("3G ELITE ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("AB300S-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("TF04-")) || // Sport Synology Z5 Treadmill #2415
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) &&
|
||||
b.name().length() == 19) || // TRUE TREADMILL followed by 4 digits (e.g. TRUE TREADMILL 0000)
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FIT-")) && !b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) || // FIT-1596 and sports tech f37s treadmill #2412
|
||||
b.name().toUpper().startsWith(QStringLiteral("FIT-TM-")) || // FIT-TM- treadmill with real inclination
|
||||
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
|
||||
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
|
||||
(b.name().toUpper().startsWith("SCHWINN 810")) ||
|
||||
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
|
||||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
|
||||
(b.name().toUpper().startsWith("SF-T")) || // Sunny Fitness Treadmill
|
||||
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FOCUS M3")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KICKR RUN")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
|
||||
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
|
||||
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TT8")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ST90")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("XT485")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI WMTP")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM4800-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("LB600")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T60-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("KETTLER TREADMILL")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("CITYSPORTS-LINKER")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15 && ftms_bike.contains(QZSettings::default_ftms_bike)) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("S77")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F89")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("X-T"))) || // FTMS (X-T421)
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TC-"))) || // FTMS (Focus Fitness Jet 7 iPlus)
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("BODYCRAFT_")) || // Bodycraft T850 Treadmill
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WT")) && b.name().length() == 5 && b.name().midRef(2).toInt() > 0) // WT treadmill (e.g. WT703)
|
||||
) &&
|
||||
!horizonTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1600,7 +1704,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral(">CABLE")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("MD")) && b.name().length() == 7) ||
|
||||
// BIKE 1, BIKE 2, BIKE 3...
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && flywheel_life_fitness_ic8 == false &&
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && (flywheel_life_fitness_ic8 || life_fitness_ic5) == false && !technogym_bike &&
|
||||
b.name().length() == 6)) &&
|
||||
!npeCableBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1626,8 +1730,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("YS_G1_")) || // Yesoul S3
|
||||
(b.name().toUpper().startsWith("YS_G1MPLUS")) || // Yesoul G1M Plus
|
||||
(b.name().toUpper().startsWith("YS_G1MMAX")) || // Yesoul G1M Max
|
||||
(b.name().toUpper().startsWith("YS_A")) || // Yesoul A6 and A1
|
||||
(b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25
|
||||
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
|
||||
(b.name().toUpper().startsWith("3G CARDIO ")) ||
|
||||
(b.name().toUpper().startsWith("ZWIFT HUB")) ||
|
||||
((b.name().toUpper().startsWith("MAGNUS ")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
@@ -1660,6 +1764,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("KETTLERBLE")) ||
|
||||
(b.name().toUpper().startsWith("JAS_C3")) ||
|
||||
(b.name().toUpper().startsWith("SCH_190U")) ||
|
||||
(b.name().toUpper().startsWith("SCH_290R")) ||
|
||||
(b.name().toUpper().startsWith("RAVE WHITE")) ||
|
||||
(b.name().toUpper().startsWith("DOMYOS-BIKING-")) ||
|
||||
(b.name().startsWith(QStringLiteral("Domyos-Bike")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && !settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool()) ||
|
||||
@@ -1677,6 +1782,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("JUSTO")) ||
|
||||
(b.name().toUpper().startsWith("MYCYCLE ")) ||
|
||||
(b.name().toUpper().startsWith("T2 ")) ||
|
||||
(b.name().toUpper().startsWith("S18")) ||
|
||||
(b.name().toUpper().startsWith("RC-MAX-")) ||
|
||||
(b.name().toUpper().startsWith("TPS-SPBIKE-2.0")) ||
|
||||
(b.name().toUpper().startsWith("NEO BIKE SMART")) ||
|
||||
@@ -1686,6 +1792,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("JFBK5.0")) ||
|
||||
(b.name().toUpper().startsWith("NEO 3M ")) ||
|
||||
(b.name().toUpper().startsWith("JFBK7.0")) ||
|
||||
(b.name().toUpper().startsWith("SPEED RACE S")) ||
|
||||
(b.name().toUpper().startsWith("SPEEDRACEX")) ||
|
||||
(b.name().toUpper().startsWith("POOBOO")) ||
|
||||
(b.name().toUpper().startsWith("ZYCLE ZPRO")) ||
|
||||
@@ -1702,22 +1809,35 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("SL010-")) ||
|
||||
(b.name().toUpper().startsWith("EXPERT-SX9")) ||
|
||||
(b.name().toUpper().startsWith("MRK-S26S-")) ||
|
||||
(b.name().toUpper().startsWith("MRK-S26C-")) ||
|
||||
(b.name().toUpper().startsWith("ROBX")) ||
|
||||
(b.name().toUpper().startsWith("ORLAUF_ARES")) ||
|
||||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
|
||||
(b.name().toUpper().startsWith("XCX-")) ||
|
||||
(b.name().toUpper().startsWith("SMARTBIKE-")) ||
|
||||
(b.name().toUpper().startsWith("D500V2")) ||
|
||||
(b.name().toUpper().startsWith("NEO BIKE PLUS ")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) && !b.name().toUpper().endsWith(QStringLiteral("SKI")) && !b.name().toUpper().endsWith(QStringLiteral("ROW"))) ||
|
||||
(b.name().toUpper().startsWith("L-") && b.name().length() == 11) ||
|
||||
(b.name().toUpper().startsWith("DMASUN-") && b.name().toUpper().endsWith("-BIKE")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
|
||||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
|
||||
(b.name().toUpper().startsWith("RIVO COG")) ||
|
||||
(b.name().toUpper().startsWith("RAVE")) ||
|
||||
(b.name().toUpper().startsWith("BESP-")) || // FITFIU BESP 250 indoor bike
|
||||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1
|
||||
(b.name().toUpper().startsWith("FS-YK-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("HT")) && (b.name().length() == 10)) ||
|
||||
(b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) ||
|
||||
(b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) ||
|
||||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
|
||||
(b.name().toUpper().startsWith("DIRETO X")) || (b.name().toUpper().startsWith("MERACH-667-")) ||
|
||||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
|
||||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
|
||||
(b.name().toUpper().startsWith("INCONDI")) || // inCondi S150i
|
||||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10) ||
|
||||
(b.name().toUpper().startsWith("JFICCYCLE"))) &&
|
||||
ftms_rower.contains(QZSettings::default_ftms_rower) &&
|
||||
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1733,6 +1853,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith("KICKR ROLLR") ||
|
||||
b.name().toUpper().startsWith("KICKR CORE") ||
|
||||
(b.name().toUpper().startsWith("KICKR MOVE ")) ||
|
||||
(b.name().toUpper().startsWith("HOI FRAME ")) ||
|
||||
(b.name().toUpper().startsWith("HAMMER ") && saris_trainer) ||
|
||||
(b.name().toUpper().startsWith("WAHOO KICKR"))) &&
|
||||
!wahooKickrSnapBike && !ftmsBike && filter) {
|
||||
@@ -1747,7 +1868,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(wahooKickrSnapBike, &wahookickrsnapbike::debug, this, &bluetooth::debug);
|
||||
wahooKickrSnapBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(wahooKickrSnapBike);
|
||||
} else if (b.name().toUpper().startsWith("BIKE ") && b.name().midRef(5).toInt() > 0 &&
|
||||
} else if (((b.name().toUpper().startsWith("BIKE ") && (flywheel_life_fitness_ic8 || life_fitness_ic5) == false && technogym_bike && b.name().midRef(5).toInt() > 0) ||
|
||||
b.name().toUpper().startsWith("MYCYCLING")) &&
|
||||
!technogymBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1799,6 +1921,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("RM")) && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DR")) && b.name().length() == 2) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DFC")) && b.name().length() == 3) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("THINK A")) && b.name().length() == 18) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
|
||||
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
|
||||
!stagesBike && !ftmsBike && filter) {
|
||||
@@ -1814,7 +1937,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// connect(stagesBike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
|
||||
stagesBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(stagesBike);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !smartrowRower && filter) {
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) && ftms_rower.contains(QZSettings::default_ftms_rower) && !smartrowRower && filter) { // Issue #4033
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
smartrowRower =
|
||||
@@ -1852,8 +1975,13 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("S4 COMMS")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("KS-WLT")) || // KS-WLT-W1
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-CRYDN-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-R06-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) && !iconsole_rower) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) || // Chaoke 107a magnetic rowing machine (Discussion #4029)
|
||||
b.name().toUpper().startsWith(QStringLiteral("NORDLYS")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWER ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DFIT-L-R")) ||
|
||||
@@ -1952,7 +2080,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(ziproTreadmill, &ziprotreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
ziproTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(ziproTreadmill);
|
||||
} else if ((b.name().toUpper().startsWith(QLatin1String("LIFESPAN-TM"))) && !lifespanTreadmill && filter) {
|
||||
} else if ((b.name().toUpper().startsWith(QLatin1String("LIFESPAN"))) && !lifespanTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
lifespanTreadmill = new lifespantreadmill(this->pollDeviceTime, noConsole, noHeartService);
|
||||
@@ -1966,9 +2094,18 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(lifespanTreadmill, &lifespantreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
lifespanTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(lifespanTreadmill);
|
||||
} else if ((b.name().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
} else if (b.name().startsWith(QStringLiteral("AT-R")) && !mobiRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
mobiRower = new mobirower(noWriteResistance, noHeartService);
|
||||
emit deviceConnected(b);
|
||||
connect(mobiRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
mobiRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(mobiRower);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWSPORT")) ||
|
||||
b.name().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
!echelonRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2010,7 +2147,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(apexBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
|
||||
apexBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(apexBike);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) && !bkoolBike && filter) {
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFBIKE")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) && !bkoolBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
bkoolBike = new bkoolbike(noWriteResistance, noHeartService);
|
||||
@@ -2066,7 +2205,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// SLOT(inclinationChanged(double)));
|
||||
schwinnIC4Bike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(schwinnIC4Bike);
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("EW-BK")) && !sportsTechBike && filter) {
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("EW-BK")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("AF")) && b.name().length() == 7)) // AF7019E
|
||||
&& !sportsTechBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
sportsTechBike = new sportstechbike(noWriteResistance, noHeartService, bikeResistanceOffset,
|
||||
@@ -2116,8 +2257,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
// SLOT(inclinationChanged(double)));
|
||||
sportsPlusBike->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(sportsPlusBike);
|
||||
} else if ((b.name().toUpper().contains(QStringLiteral("CARE")) &&
|
||||
b.name().length() >= 13) // CARE968300122
|
||||
} else if (((b.name().toUpper().contains(QStringLiteral("CARE")) && b.name().length() >= 13) || // CARE968300122
|
||||
(b.name().toUpper().startsWith(QStringLiteral("VMAX"))))
|
||||
&& !sportsPlusRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2292,7 +2433,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
this->signalBluetoothDeviceConnected(nautilusTreadmill);
|
||||
} else if ((b.name().startsWith(QStringLiteral("Flywheel")) ||
|
||||
// BIKE 1, BIKE 2, BIKE 3...
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && flywheel_life_fitness_ic8 == true &&
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && (flywheel_life_fitness_ic8 || life_fitness_ic5) == true &&
|
||||
b.name().length() == 6)) &&
|
||||
!flywheelBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -2338,7 +2479,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
toorx->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(toorx);
|
||||
} else if (((b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT")) && !b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT TREAD"))) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BH-"))) && !iConceptBike &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("BH-"))) && !iConceptBike && !toorx &&
|
||||
!iconcept_elliptical && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2390,16 +2531,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(activioTreadmill, &activiotreadmill::debug, this, &bluetooth::debug);
|
||||
activioTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(activioTreadmill);
|
||||
} else if (((b.name().startsWith(QStringLiteral("TOORX"))) ||
|
||||
(b.name().startsWith(QStringLiteral("V-RUN"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("K80_"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-RUNNING"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) &&
|
||||
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower &&
|
||||
} else if (((deviceName.startsWith(QStringLiteral("TOORX"))) ||
|
||||
(deviceName.startsWith(QStringLiteral("V-RUN"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("K80_"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("ICONSOLE+"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("I-RUNNING"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("DKN RUN"))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("ADIDAS "))) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("REEBOK")))) &&
|
||||
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) &&
|
||||
ftms_bike.contains(QZSettings::default_ftms_bike) &&
|
||||
filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2411,24 +2553,26 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(trxappgateusb, &trxappgateusbtreadmill::debug, this, &bluetooth::debug);
|
||||
trxappgateusb->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(trxappgateusb);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("FIT HI WAY")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("BIKZU_")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("PASYOU-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("VIRTUFIT")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("IBIKING+")) ||
|
||||
((b.name().startsWith(QStringLiteral("TOORX")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("VIFHTR2.1")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK"))) ||
|
||||
b.name().toUpper().contains(QStringLiteral("CR011R")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
} else if (isTrxAppGateUsbBikeTC ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("TUN ")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("FITHIWAY")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("FIT HI WAY")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("BIKZU_")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("PASYOU-")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIRTUFIT")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("IBIKING+")) ||
|
||||
isRI009R ||
|
||||
((deviceName.startsWith(QStringLiteral("TOORX")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOIE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("ICONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
|
||||
upperDeviceName.contains(QStringLiteral("CR011R")) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
(toorx_bike))) &&
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower) {
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && (filter || isRI009R) && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && !csc_as_bike) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbBike =
|
||||
@@ -2532,7 +2676,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
|
||||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
|
||||
b.name().startsWith(QStringLiteral("MRK-"))) &&
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && filter) {
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && !trxappgateusbRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
fitPlusBike =
|
||||
@@ -2564,6 +2708,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().startsWith(QStringLiteral("SW")) && b.name().length() == 14 &&
|
||||
!b.name().contains('(') && !b.name().contains(')') && !deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WINFITA"))) || // also FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T80-")) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("SW-BLE"))) || // FTMS
|
||||
(b.name().startsWith(QStringLiteral("BF70")))) &&
|
||||
!fitshowTreadmill && !iconsole_elliptical && !horizonTreadmill && filter) {
|
||||
@@ -2678,11 +2823,11 @@ void bluetooth::connectedAndDiscovered() {
|
||||
settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
|
||||
|
||||
// only at the first very connection, setting the user default resistance
|
||||
if (device() && firstConnected && device()->deviceType() == bluetoothdevice::BIKE &&
|
||||
if (device() && firstConnected && device()->deviceType() == BIKE &&
|
||||
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() != 1) {
|
||||
qobject_cast<bike *>(device())->changeResistance(
|
||||
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt());
|
||||
} else if (device() && firstConnected && device()->deviceType() == bluetoothdevice::ELLIPTICAL &&
|
||||
} else if (device() && firstConnected && device()->deviceType() == ELLIPTICAL &&
|
||||
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() !=
|
||||
1) {
|
||||
qobject_cast<elliptical *>(device())->changeResistance(
|
||||
@@ -2850,7 +2995,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
#else
|
||||
settings.setValue(QZSettings::power_sensor_address, b.deviceUuid().toString());
|
||||
#endif
|
||||
if (device() && device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (device() && device()->deviceType() == BIKE) {
|
||||
powerSensor = new stagesbike(false, false, true);
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
|
||||
@@ -2859,7 +3004,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
|
||||
&bluetoothdevice::cadenceSensor);
|
||||
powerSensor->deviceDiscovered(b);
|
||||
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
|
||||
} else if (device() && device()->deviceType() == TREADMILL) {
|
||||
powerSensorRun = new strydrunpowersensor(false, false, true);
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
|
||||
@@ -2916,7 +3061,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().startsWith(eliteSterzoSmartName))) && !eliteSterzoSmart &&
|
||||
!eliteSterzoSmartName.startsWith(QStringLiteral("Disabled")) && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
settings.setValue(QZSettings::elite_sterzo_smart_lastdevice_name, b.name());
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
@@ -2938,7 +3083,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
if(settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SRAM "))) && !sramAXSController && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
sramAXSController = new sramaxscontroller();
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
@@ -2957,7 +3102,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
if(settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("ZWIFT CLICK"))) && !zwiftClickRemote && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
if(b.manufacturerData(2378).size() > 0) {
|
||||
qDebug() << "this should be 9. is it? " << int(b.manufacturerData(2378).at(0));
|
||||
@@ -2982,7 +3127,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
eliteSquareController = new elitesquarecontroller(this->device());
|
||||
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
|
||||
@@ -3002,7 +3147,7 @@ void bluetooth::connectedAndDiscovered() {
|
||||
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
|
||||
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
|
||||
if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE") || b.name().toUpper().startsWith("ZWIFT SF2")) && zwiftPlayDevice.size() < 2 && this->device() &&
|
||||
this->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
this->device()->deviceType() == BIKE) {
|
||||
|
||||
if(b.manufacturerData(2378).size() > 0) {
|
||||
qDebug() << "this should be 3 or 2. is it? " << int(b.manufacturerData(2378).at(0));
|
||||
@@ -3034,18 +3179,23 @@ void bluetooth::connectedAndDiscovered() {
|
||||
}
|
||||
}
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() ||
|
||||
bool android_antbike =
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool();
|
||||
if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() || android_antbike ||
|
||||
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
|
||||
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
|
||||
"activity", "()Landroid/app/Activity;");
|
||||
KeepAwakeHelper::antObject(true)->callMethod<void>(
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZ)V", activity.object<jobject>(),
|
||||
"antStart", "(Landroid/app/Activity;ZZZZZZII)V", activity.object<jobject>(),
|
||||
settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(),
|
||||
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(),
|
||||
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(),
|
||||
device()->deviceType() == bluetoothdevice::TREADMILL ||
|
||||
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool());
|
||||
device()->deviceType() == TREADMILL ||
|
||||
device()->deviceType() == ELLIPTICAL,
|
||||
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool(),
|
||||
settings.value(QZSettings::technogym_group_cycle, QZSettings::default_technogym_group_cycle).toBool(),
|
||||
settings.value(QZSettings::ant_bike_device_number, QZSettings::default_ant_bike_device_number).toInt(),
|
||||
settings.value(QZSettings::ant_heart_device_number, QZSettings::default_ant_heart_device_number).toInt());
|
||||
}
|
||||
|
||||
if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) {
|
||||
@@ -3333,6 +3483,11 @@ void bluetooth::restart() {
|
||||
delete nordictrackifitadbElliptical;
|
||||
nordictrackifitadbElliptical = nullptr;
|
||||
}
|
||||
if (nordictrackifitadbRower) {
|
||||
|
||||
delete nordictrackifitadbRower;
|
||||
nordictrackifitadbRower = nullptr;
|
||||
}
|
||||
if (powerBike) {
|
||||
|
||||
delete powerBike;
|
||||
@@ -3459,6 +3614,11 @@ void bluetooth::restart() {
|
||||
delete echelonRower;
|
||||
echelonRower = nullptr;
|
||||
}
|
||||
if (mobiRower) {
|
||||
|
||||
delete mobiRower;
|
||||
mobiRower = nullptr;
|
||||
}
|
||||
if (echelonStride) {
|
||||
|
||||
delete echelonStride;
|
||||
@@ -3629,6 +3789,11 @@ void bluetooth::restart() {
|
||||
delete computrainerBike;
|
||||
computrainerBike = nullptr;
|
||||
}
|
||||
if (kettlerUsbBike) {
|
||||
|
||||
delete kettlerUsbBike;
|
||||
kettlerUsbBike = nullptr;
|
||||
}
|
||||
if (csafeRower) {
|
||||
|
||||
delete csafeRower;
|
||||
@@ -3807,6 +3972,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return android_antBike;
|
||||
} else if (nordictrackifitadbTreadmill) {
|
||||
return nordictrackifitadbTreadmill;
|
||||
} else if (nordictrackifitadbRower) {
|
||||
return nordictrackifitadbRower;
|
||||
} else if (nordictrackifitadbBike) {
|
||||
return nordictrackifitadbBike;
|
||||
} else if (nordictrackifitadbElliptical) {
|
||||
@@ -3887,6 +4054,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonConnectSport;
|
||||
} else if (echelonRower) {
|
||||
return echelonRower;
|
||||
} else if (mobiRower) {
|
||||
return mobiRower;
|
||||
} else if (echelonStride) {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
@@ -3982,6 +4151,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
#ifndef Q_OS_IOS
|
||||
} else if (computrainerBike) {
|
||||
return computrainerBike;
|
||||
} else if (kettlerUsbBike) {
|
||||
return kettlerUsbBike;
|
||||
} else if (csafeRower) {
|
||||
return csafeRower;
|
||||
} else if (csafeElliptical) {
|
||||
@@ -4047,7 +4218,7 @@ void bluetooth::stateFileUpdate() {
|
||||
if (!device()) {
|
||||
return;
|
||||
}
|
||||
if (device()->deviceType() != bluetoothdevice::TREADMILL) {
|
||||
if (device()->deviceType() != TREADMILL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "devices/coresensor/coresensor.h"
|
||||
#ifndef Q_OS_IOS
|
||||
#include "devices/computrainerbike/computrainerbike.h"
|
||||
#include "devices/kettlerusbbike/kettlerusbbike.h"
|
||||
#include "devices/csaferower/csaferower.h"
|
||||
#include "devices/csafeelliptical/csafeelliptical.h"
|
||||
#endif
|
||||
@@ -89,6 +90,7 @@
|
||||
#include "devices/nordictrackelliptical/nordictrackelliptical.h"
|
||||
#include "devices/nordictrackifitadbbike/nordictrackifitadbbike.h"
|
||||
#include "devices/nordictrackifitadbelliptical/nordictrackifitadbelliptical.h"
|
||||
#include "devices/nordictrackifitadbrower/nordictrackifitadbrower.h"
|
||||
#include "devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h"
|
||||
#include "devices/npecablebike/npecablebike.h"
|
||||
#include "devices/octaneelliptical/octaneelliptical.h"
|
||||
@@ -152,6 +154,7 @@
|
||||
|
||||
#include "zwift_play/zwiftPlayDevice.h"
|
||||
#include "zwift_play/zwiftclickremote.h"
|
||||
#include "devices/mobirower/mobirower.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
@@ -191,6 +194,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
focustreadmill *focusTreadmill = nullptr;
|
||||
#ifndef Q_OS_IOS
|
||||
computrainerbike *computrainerBike = nullptr;
|
||||
kettlerusbbike *kettlerUsbBike = nullptr;
|
||||
csaferower *csafeRower = nullptr;
|
||||
csafeelliptical *csafeElliptical = nullptr;
|
||||
#endif
|
||||
@@ -222,6 +226,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
nordictrackifitadbtreadmill *nordictrackifitadbTreadmill = nullptr;
|
||||
nordictrackifitadbbike *nordictrackifitadbBike = nullptr;
|
||||
nordictrackifitadbelliptical *nordictrackifitadbElliptical = nullptr;
|
||||
nordictrackifitadbrower *nordictrackifitadbRower = nullptr;
|
||||
octaneelliptical *octaneElliptical = nullptr;
|
||||
octanetreadmill *octaneTreadmill = nullptr;
|
||||
pelotonbike *pelotonBike = nullptr;
|
||||
@@ -265,6 +270,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonrower *echelonRower = nullptr;
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
mobirower *mobiRower = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
|
||||
@@ -20,7 +20,7 @@ bluetoothdevice::~bluetoothdevice() {
|
||||
}
|
||||
}
|
||||
|
||||
bluetoothdevice::BLUETOOTH_TYPE bluetoothdevice::deviceType() { return bluetoothdevice::UNKNOWN; }
|
||||
BLUETOOTH_TYPE bluetoothdevice::deviceType() { return UNKNOWN; }
|
||||
void bluetoothdevice::start() { requestStart = 1; lastStart = QDateTime::currentMSecsSinceEpoch(); }
|
||||
void bluetoothdevice::stop(bool pause) {
|
||||
requestStop = 1;
|
||||
@@ -109,7 +109,52 @@ QTime bluetoothdevice::maxPace() {
|
||||
double bluetoothdevice::odometerFromStartup() { return Distance.valueRaw(); }
|
||||
double bluetoothdevice::odometer() { return Distance.value(); }
|
||||
double bluetoothdevice::lapOdometer() { return Distance.lapValue(); }
|
||||
metric bluetoothdevice::calories() { return KCal; }
|
||||
metric bluetoothdevice::calories() {
|
||||
QSettings settings;
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
|
||||
|
||||
if (fromHR && Heart.value() > 0) {
|
||||
// Calculate calories based on heart rate
|
||||
double totalHRKCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value());
|
||||
hrKCal.setValue(totalHRKCal);
|
||||
|
||||
if (activeOnly) {
|
||||
activeKCal.setValue(metric::calculateActiveKCal(hrKCal.value(), elapsed.value()));
|
||||
return activeKCal;
|
||||
} else {
|
||||
return hrKCal;
|
||||
}
|
||||
} else {
|
||||
// Power-based calculation (current behavior)
|
||||
if (activeOnly) {
|
||||
activeKCal.setValue(metric::calculateActiveKCal(KCal.value(), elapsed.value()));
|
||||
return activeKCal;
|
||||
} else {
|
||||
return KCal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metric bluetoothdevice::totalCalories() {
|
||||
QSettings settings;
|
||||
bool fromHR = settings.value(QZSettings::calories_from_hr, QZSettings::default_calories_from_hr).toBool();
|
||||
|
||||
if (fromHR && Heart.value() > 0) {
|
||||
return hrKCal; // Return HR-based total calories
|
||||
} else {
|
||||
return KCal; // Return power-based total calories
|
||||
}
|
||||
}
|
||||
|
||||
metric bluetoothdevice::activeCalories() {
|
||||
return activeKCal;
|
||||
}
|
||||
|
||||
metric bluetoothdevice::hrCalories() {
|
||||
return hrKCal;
|
||||
}
|
||||
|
||||
metric bluetoothdevice::jouls() { return m_jouls; }
|
||||
uint8_t bluetoothdevice::fanSpeed() { return FanSpeed; };
|
||||
bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
|
||||
@@ -132,7 +177,18 @@ 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); }
|
||||
metric bluetoothdevice::negativeElevationGain() { return negativeElevationAcc; }
|
||||
void bluetoothdevice::heartRate(uint8_t heart) {
|
||||
Heart.setValue(heart);
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
// Write heart rate from Bluetooth to Apple Health during workout
|
||||
lockscreen h;
|
||||
if(heart > 0)
|
||||
h.setHeartRate(heart);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
void bluetoothdevice::coreBodyTemperature(double coreBodyTemperature) { CoreBodyTemperature.setValue(coreBodyTemperature); }
|
||||
void bluetoothdevice::skinTemperature(double skinTemperature) { SkinTemperature.setValue(skinTemperature); }
|
||||
void bluetoothdevice::heatStrainIndex(double heatStrainIndex) { HeatStrainIndex.setValue(heatStrainIndex); }
|
||||
@@ -192,7 +248,7 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts, const b
|
||||
!power_as_bike && !power_as_treadmill)
|
||||
watt_calc = false;
|
||||
|
||||
if(deviceType() == bluetoothdevice::BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
|
||||
if(deviceType() == BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
|
||||
_ergTable.collectData(Cadence.value(), m_watt.value(), Resistance.value());
|
||||
|
||||
if (!_firstUpdate && !paused) {
|
||||
@@ -231,9 +287,14 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts, const b
|
||||
METS = calculateMETS();
|
||||
if (currentInclination().value() > 0)
|
||||
elevationAcc += (currentSpeed().value() / 3600.0) * 1000.0 * (currentInclination().value() / 100.0) * deltaTime;
|
||||
else if (currentInclination().value() < 0)
|
||||
negativeElevationAcc += (currentSpeed().value() / 3600.0) * 1000.0 * fabs(currentInclination().value() / 100.0) * deltaTime;
|
||||
|
||||
_lastTimeUpdate = current;
|
||||
_firstUpdate = false;
|
||||
|
||||
// Update iOS Live Activity with throttling
|
||||
update_ios_live_activity();
|
||||
}
|
||||
|
||||
void bluetoothdevice::update_hr_from_external() {
|
||||
@@ -254,7 +315,17 @@ void bluetoothdevice::update_hr_from_external() {
|
||||
#ifndef IO_UNDER_QT
|
||||
lockscreen h;
|
||||
long appleWatchHeartRate = h.heartRate();
|
||||
h.setKcal(KCal.value());
|
||||
QSettings settings;
|
||||
bool activeOnly = settings.value(QZSettings::calories_active_only, QZSettings::default_calories_active_only).toBool();
|
||||
|
||||
if (activeOnly) {
|
||||
// When active calories setting is enabled, send both total and active calories
|
||||
h.setKcal(calories().value()); // This will be active calories
|
||||
h.setTotalKcal(totalCalories().value()); // This will be total calories
|
||||
} else {
|
||||
// When disabled, send total calories as before
|
||||
h.setKcal(calories().value()); // This will be total calories
|
||||
}
|
||||
h.setDistance(Distance.value());
|
||||
h.setSpeed(Speed.value());
|
||||
h.setPower(m_watt.value());
|
||||
@@ -271,6 +342,29 @@ void bluetoothdevice::update_hr_from_external() {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// Note: workoutTrackingUpdate is now called from update_ios_live_activity() with throttling
|
||||
}
|
||||
|
||||
void bluetoothdevice::update_ios_live_activity() {
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
static QDateTime lastUpdate;
|
||||
QDateTime current = QDateTime::currentDateTime();
|
||||
|
||||
// Throttle updates: only update if at least 1 second has passed since last update
|
||||
if (!lastUpdate.isValid() || lastUpdate.msecsTo(current) >= 1000) {
|
||||
QSettings settings;
|
||||
lockscreen h;
|
||||
double kcal = calories().value();
|
||||
if(kcal < 0)
|
||||
kcal = 0;
|
||||
bool useMiles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value(), useMiles, (uint8_t)Heart.value());
|
||||
|
||||
lastUpdate = current;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void bluetoothdevice::clearStats() {
|
||||
@@ -279,11 +373,14 @@ void bluetoothdevice::clearStats() {
|
||||
moving.clear(true);
|
||||
Speed.clear(false);
|
||||
KCal.clear(true);
|
||||
hrKCal.clear(true);
|
||||
activeKCal.clear(true);
|
||||
Distance.clear(true);
|
||||
Distance1s.clear(true);
|
||||
Heart.clear(false);
|
||||
m_jouls.clear(true);
|
||||
elevationAcc = 0;
|
||||
negativeElevationAcc = 0;
|
||||
m_watt.clear(false);
|
||||
m_rawWatt.clear(false);
|
||||
WeightLoss.clear(false);
|
||||
@@ -304,6 +401,8 @@ void bluetoothdevice::setPaused(bool p) {
|
||||
elapsed.setPaused(p);
|
||||
Speed.setPaused(p);
|
||||
KCal.setPaused(p);
|
||||
hrKCal.setPaused(p);
|
||||
activeKCal.setPaused(p);
|
||||
Distance.setPaused(p);
|
||||
Distance1s.setPaused(p);
|
||||
Heart.setPaused(p);
|
||||
@@ -327,6 +426,8 @@ void bluetoothdevice::setLap() {
|
||||
elapsed.setLap(true);
|
||||
Speed.setLap(false);
|
||||
KCal.setLap(true);
|
||||
hrKCal.setLap(true);
|
||||
activeKCal.setLap(true);
|
||||
Distance.setLap(true);
|
||||
Distance1s.setLap(true);
|
||||
Heart.setLap(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef BLUETOOTHDEVICE_H
|
||||
#define BLUETOOTHDEVICE_H
|
||||
|
||||
#include "bluetoothdevicetype.h"
|
||||
#include "definitions.h"
|
||||
#include "metric.h"
|
||||
#include "qzsettings.h"
|
||||
@@ -108,11 +109,19 @@ class bluetoothdevice : public QObject {
|
||||
|
||||
/**
|
||||
* @brief calories Gets a metric object to get and set the amount of energy expended.
|
||||
* Default implementation returns the protected KCal property. Units: kcal
|
||||
* Default implementation returns the protected KCal property, potentially adjusted for active calories. Units: kcal
|
||||
* Other implementations could have different units.
|
||||
* @return
|
||||
*/
|
||||
virtual metric calories();
|
||||
virtual metric activeCalories();
|
||||
virtual metric hrCalories();
|
||||
|
||||
/**
|
||||
* @brief totalCalories Gets total calories (including BMR) regardless of active calories setting.
|
||||
* @return Total calories metric
|
||||
*/
|
||||
virtual metric totalCalories();
|
||||
|
||||
/**
|
||||
* @brief jouls Gets a metric object to get and set the number of joules expended. Units: joules
|
||||
@@ -226,9 +235,12 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
double wattsMetricforUI() {
|
||||
QSettings settings;
|
||||
bool power3s = settings.value(QZSettings::power_avg_3s, QZSettings::default_power_avg_3s).toBool();
|
||||
bool power5s = settings.value(QZSettings::power_avg_5s, QZSettings::default_power_avg_5s).toBool();
|
||||
if (power5s)
|
||||
return wattsMetric().average5s();
|
||||
if (power3s)
|
||||
return wattsMetric().average3sHarmonic();
|
||||
else if (power5s)
|
||||
return wattsMetric().average5sHarmonic();
|
||||
else
|
||||
return wattsMetric().value();
|
||||
}
|
||||
@@ -244,6 +256,11 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
virtual metric elevationGain();
|
||||
|
||||
/**
|
||||
* @brief negativeElevationGain Gets a metric object to get and set the negative elevation gain (descents). Units: ?
|
||||
*/
|
||||
virtual metric negativeElevationGain();
|
||||
|
||||
/**
|
||||
* @brief clearStats Clear the statistics.
|
||||
*/
|
||||
@@ -443,7 +460,6 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
|
||||
|
||||
/**
|
||||
@@ -548,6 +564,8 @@ class bluetoothdevice : public QObject {
|
||||
* @brief KCal The number of kilocalories expended in the session. Units: kcal
|
||||
*/
|
||||
metric KCal;
|
||||
metric activeKCal;
|
||||
metric hrKCal;
|
||||
|
||||
/**
|
||||
* @brief Speed The simulated speed of the device. Units: km/h
|
||||
@@ -618,6 +636,11 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
metric elevationAcc;
|
||||
|
||||
/**
|
||||
* @brief negativeElevationAcc The negative elevation gain (descents). Units: meters
|
||||
*/
|
||||
metric negativeElevationAcc;
|
||||
|
||||
/**
|
||||
* @brief m_watt Metric to get and set the power read from the trainer or from the power sensor Unit: watts
|
||||
*/
|
||||
@@ -777,6 +800,11 @@ class bluetoothdevice : public QObject {
|
||||
*/
|
||||
void update_hr_from_external();
|
||||
|
||||
/**
|
||||
* @brief update_ios_live_activity Updates iOS Live Activity with throttling (max 1 update per second)
|
||||
*/
|
||||
void update_ios_live_activity();
|
||||
|
||||
/**
|
||||
* @brief calculateMETS Calculate the METS (Metabolic Equivalent of Tasks)
|
||||
* Units: METs (1 MET is approximately 3.5mL of Oxygen consumed per kg of body weight per minute)
|
||||
|
||||
@@ -23,7 +23,7 @@ bowflext216treadmill::bowflext216treadmill(uint32_t pollDeviceTime, bool noConso
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -23,7 +23,7 @@ bowflextreadmill::bowflextreadmill(uint32_t pollDeviceTime, bool noConsole, bool
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
//#include <QtBluetooth/private/qlowenergyserviceprivate_p.h>
|
||||
|
||||
chronobike::chronobike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
t_timeout = new QTimer(this);
|
||||
|
||||
@@ -902,6 +902,7 @@ int Computrainer::rawWrite(uint8_t *bytes, int size) // unix!!
|
||||
b[i] = bytes[i];
|
||||
env->SetByteArrayRegion(d, 0, size, b);
|
||||
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/Usbserial", "write", "([B)V", d);
|
||||
env->DeleteLocalRef(d);
|
||||
#elif defined(WIN32)
|
||||
DWORD cBytes;
|
||||
rc = WriteFile(devicePort, bytes, size, &cBytes, NULL);
|
||||
@@ -947,12 +948,38 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
|
||||
}
|
||||
|
||||
QAndroidJniEnvironment env;
|
||||
while (fullLen < size) {
|
||||
int timeout = 0;
|
||||
int maxRetries = 100; // Maximum number of retries (100 * 50ms = 5 seconds timeout)
|
||||
int retryCount = 0;
|
||||
|
||||
while (fullLen < size && retryCount < maxRetries) {
|
||||
// Push a new local frame to automatically manage JNI references
|
||||
// This prevents local reference table overflow by cleaning up refs at the end of each iteration
|
||||
if (env->PushLocalFrame(16) < 0) {
|
||||
qDebug() << "Failed to push local frame";
|
||||
return -1;
|
||||
}
|
||||
|
||||
QAndroidJniObject dd =
|
||||
QAndroidJniObject::callStaticObjectMethod("org/cagnulen/qdomyoszwift/Usbserial", "read", "()[B");
|
||||
jint len = QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/Usbserial", "readLen", "()I");
|
||||
jbyteArray d = dd.object<jbyteArray>();
|
||||
jbyte *b = env->GetByteArrayElements(d, 0);
|
||||
|
||||
// Check if we got any data
|
||||
if (len <= 0) {
|
||||
// No data available, release memory and retry after a short sleep
|
||||
env->ReleaseByteArrayElements(d, b, 0);
|
||||
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
|
||||
qDebug() << "No data available, retry" << retryCount + 1 << "of" << maxRetries;
|
||||
CTsleeper::msleep(50); // Sleep for 50ms before retrying
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset retry counter when we get data
|
||||
retryCount = 0;
|
||||
|
||||
if (len + fullLen > size) {
|
||||
QByteArray tmpDebug;
|
||||
qDebug() << "buffer overflow! Truncate from" << len + fullLen << "requested" << size;
|
||||
@@ -970,6 +997,10 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
|
||||
}
|
||||
qDebug() << len + fullLen - size << "bytes to the rxBuf" << tmpDebug.toHex(' ');
|
||||
qDebug() << size << QByteArray((const char *)b, size).toHex(' ');
|
||||
|
||||
// Release JNI memory before returning
|
||||
env->ReleaseByteArrayElements(d, b, 0);
|
||||
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
|
||||
return size;
|
||||
}
|
||||
for (int i = fullLen; i < len + fullLen; i++) {
|
||||
@@ -977,6 +1008,16 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
|
||||
}
|
||||
qDebug() << len << QByteArray((const char *)b, len).toHex(' ');
|
||||
fullLen += len;
|
||||
|
||||
// Release JNI memory after processing
|
||||
env->ReleaseByteArrayElements(d, b, 0);
|
||||
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
|
||||
}
|
||||
|
||||
// Check if we timed out
|
||||
if (retryCount >= maxRetries) {
|
||||
qDebug() << "rawRead timeout: no data after" << maxRetries << "retries";
|
||||
return -1; // Timeout error
|
||||
}
|
||||
|
||||
qDebug() << "FULL BUFFER RX: << " << fullLen << QByteArray((const char *)bytes, size).toHex(' ');
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
computrainerbike::computrainerbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
|
||||
double bikeResistanceGain) {
|
||||
QSettings settings;
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
target_watts.setType(metric::METRIC_WATT);
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
concept2skierg::concept2skierg(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -23,7 +23,7 @@ crossrope::crossrope(uint32_t pollDeviceTime, bool noConsole, bool noHeartServic
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
|
||||
@@ -4,7 +4,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
csafeelliptical::csafeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice,
|
||||
int8_t bikeResistanceOffset, double bikeResistanceGain) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -284,7 +284,7 @@ void csafeelliptical::update() {
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
csaferower::csaferower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
@@ -78,7 +78,12 @@ void cscbike::update() {
|
||||
double rpm = currentCadence().value();
|
||||
m_watt = 0.000602337 * pow(rpm, 3.11762) + 32.6404;
|
||||
} else {
|
||||
m_watt = wattFromHR(false);
|
||||
// When cadence is zero, watts should be zero regardless of HR
|
||||
if (currentCadence().value() == 0) {
|
||||
m_watt = 0;
|
||||
} else {
|
||||
m_watt = wattFromHR(false);
|
||||
}
|
||||
}
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
|
||||
@@ -207,6 +212,22 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist
|
||||
emit debug(QStringLiteral("Current Crank Event Time: ") + QString::number(_LastCrankEventTime));
|
||||
}
|
||||
|
||||
// CSC Combo Sensor Fallback Logic
|
||||
//
|
||||
// Some combo sensors (e.g., Giant Combo) advertise both speed and cadence capabilities
|
||||
// by setting CrankPresent=true in the CSC flags byte, but only transmit valid wheel data
|
||||
// while sending crank data as zeros. This happens when:
|
||||
// 1. The sensor supports dual mode (speed+cadence) but only speed sensor is mounted
|
||||
// 2. The cadence sensor is not activated/calibrated
|
||||
// 3. Firmware always sets CrankPresent flag regardless of actual crank sensor status
|
||||
//
|
||||
// In these cases, we use wheel revolutions as a fallback to calculate cadence.
|
||||
// This works when the wheel circumference is set to a small value (e.g., 20cm for
|
||||
// indoor trainers), which effectively converts wheel RPM to a cadence-like metric.
|
||||
//
|
||||
// Note: When using wheel revs as cadence, the calculated RPM can exceed 256 (the
|
||||
// typical limit for real crank cadence). For example, with 20cm wheel circumference
|
||||
// at 12.5 km/h, wheel RPM ≈ 1000. The validation logic below accounts for this.
|
||||
if ((!CrankPresent || _CrankRevs == 0) && WheelPresent) {
|
||||
CrankRevs = _WheelRevs;
|
||||
LastCrankEventTime = _LastWheelEventTime;
|
||||
@@ -222,7 +243,23 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist
|
||||
|
||||
if (CrankRevs != oldCrankRevs && deltaT) {
|
||||
double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * 1024 * 60;
|
||||
if ((cadence >= 0 && cadence < 256 && CrankPresent) || (!CrankPresent && WheelPresent))
|
||||
|
||||
// Cadence Validation Logic
|
||||
//
|
||||
// Normal cadence validation applies a 256 RPM limit for real crank sensors (no human
|
||||
// can pedal faster than 256 RPM). However, when using wheel revs as fallback
|
||||
// (_CrankRevs == 0), we bypass this limit because:
|
||||
// - Wheel RPM with small circumferences (e.g., 20cm) can legitimately exceed 256
|
||||
// - Example: 12.5 km/h with 20cm circumference = ~1042 wheel RPM
|
||||
// - This high RPM represents wheel rotation rate, not actual pedaling cadence
|
||||
//
|
||||
// The condition breakdown:
|
||||
// Part 1: (cadence >= 0 && (cadence < 256 || _CrankRevs == 0) && CrankPresent)
|
||||
// - For real crank data: applies 256 RPM limit
|
||||
// - For wheel fallback: no limit when _CrankRevs == 0
|
||||
// Part 2: (!CrankPresent && WheelPresent)
|
||||
// - Pure speed sensors with no crank capability
|
||||
if ((cadence >= 0 && (cadence < 256 || _CrankRevs == 0) && CrankPresent) || (!CrankPresent && WheelPresent))
|
||||
Cadence = cadence;
|
||||
lastGoodCadence = now;
|
||||
} else if (lastGoodCadence.msecsTo(now) > 2000) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
cycleopsphantombike::cycleopsphantombike(bool noWriteResistance, bool noHeartService) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -738,7 +738,7 @@ void cycleopsphantombike::characteristicChanged(const QLowEnergyCharacteristic &
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)currentHeart().value());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
|
||||
|
||||
deerruntreadmill::deerruntreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
|
||||
double forceInitInclination) {
|
||||
m_watt.setType(metric::METRIC_WATT);
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
this->noConsole = noConsole;
|
||||
this->noHeartService = noHeartService;
|
||||
@@ -75,6 +75,51 @@ void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::waitForAPacket() {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
connect(this, &deerruntreadmill::packetReceived, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(3000, &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
void deerruntreadmill::writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
|
||||
if(!unlock_service) {
|
||||
qDebug() << "ERROR! Unlock service not found!";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(unlock_service, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
|
||||
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
|
||||
|
||||
if (unlock_service->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
|
||||
m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit debug(QStringLiteral("writeUnlockCharacteristic error because the connection is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeBuffer) {
|
||||
delete writeBuffer;
|
||||
}
|
||||
writeBuffer = new QByteArray((const char *)data, data_len);
|
||||
|
||||
unlock_service->writeCharacteristic(unlock_characteristic, *writeBuffer);
|
||||
|
||||
if (!disable_log) {
|
||||
emit debug(QStringLiteral(" >> unlock ") + writeBuffer->toHex(' ') +
|
||||
QStringLiteral(" // ") + info);
|
||||
}
|
||||
|
||||
loop.exec();
|
||||
|
||||
if (timeout.isActive() == false) {
|
||||
emit debug(QStringLiteral(" exit for timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
|
||||
uint8_t result = 0;
|
||||
|
||||
@@ -90,18 +135,70 @@ uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
|
||||
uint8_t result = 0;
|
||||
|
||||
if (size < 5) {
|
||||
qDebug() << QStringLiteral("array too small for PitPat checksum");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// For PitPat protocol:
|
||||
// 1. XOR from byte 5 to byte (size - 3) for long messages (>= 7 bytes)
|
||||
// or from byte 2 to byte (size - 3) for short messages (< 7 bytes)
|
||||
// 2. XOR the result with byte 1
|
||||
size_t startIdx = (size < 7) ? 2 : 5;
|
||||
|
||||
for (size_t i = startIdx; i <= size - 3; i++) {
|
||||
result ^= arr[i];
|
||||
}
|
||||
|
||||
// XOR with byte 1 (command byte)
|
||||
result ^= arr[1];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
void deerruntreadmill::forceSpeed(double requestSpeed) {
|
||||
QSettings settings;
|
||||
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
|
||||
|
||||
writeSpeed[2] = pollCounter;
|
||||
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
|
||||
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
|
||||
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
|
||||
if (pitpat) {
|
||||
// PitPat speed template
|
||||
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
|
||||
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
|
||||
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x07, 0x6c, 0x01, 0x00, 0x8a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x2e, 0x0c, 0xc3, 0x43};
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
|
||||
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
|
||||
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
|
||||
writeSpeed[7] = speed & 0xFF; // Low byte
|
||||
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed), false, true);
|
||||
} else if (superun_ba04) {
|
||||
// Superun BA04 speed template
|
||||
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
|
||||
|
||||
writeSpeed[2] = pollCounter;
|
||||
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
|
||||
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
|
||||
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed BA04 speed=") + QString::number(requestSpeed), false, false);
|
||||
} else {
|
||||
// Default speed template
|
||||
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
|
||||
|
||||
writeSpeed[2] = pollCounter;
|
||||
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
|
||||
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
|
||||
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
|
||||
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::forceIncline(double requestIncline) {
|
||||
@@ -183,33 +280,40 @@ void deerruntreadmill::update() {
|
||||
lastSpeed = 0.5;
|
||||
}
|
||||
|
||||
// should be:
|
||||
// 0x49 = inited
|
||||
// 0x8a = tape stopped after a pause
|
||||
/*if (lastState == 0x49)*/ {
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43};
|
||||
initData2[2] = pollCounter;
|
||||
if (pitpat) {
|
||||
forceSpeed(1.0);
|
||||
} else {
|
||||
// should be:
|
||||
// 0x49 = inited
|
||||
// 0x8a = tape stopped after a pause
|
||||
/*if (lastState == 0x49)*/ {
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x0c, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0x2a, 0x43};
|
||||
initData2[2] = pollCounter;
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"),
|
||||
false, true);
|
||||
} /*else {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("start"),
|
||||
false, true);
|
||||
} /*else {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
}*/
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
}*/
|
||||
}
|
||||
|
||||
requestStart = -1;
|
||||
emit tapeStarted();
|
||||
} else if (requestStop != -1) {
|
||||
emit debug(QStringLiteral("stopping... ") + paused);
|
||||
/*if (lastState == PAUSED) {
|
||||
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
|
||||
true);
|
||||
|
||||
} else*/ {
|
||||
if (pitpat) {
|
||||
uint8_t stop[] = {
|
||||
0x6a, 0x17, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x05, 0x00,
|
||||
0x8a, 0x00, 0x02, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x12, 0x2e,
|
||||
0x0c, 0xaa, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, stop, sizeof(stop), QStringLiteral("stop"), false, true);
|
||||
} else {
|
||||
uint8_t stop[] = {0x4d, 0x00, 0x48, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x50, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd6, 0x43};
|
||||
stop[2] = pollCounter;
|
||||
|
||||
@@ -219,11 +323,16 @@ void deerruntreadmill::update() {
|
||||
|
||||
requestStop = -1;
|
||||
} else {
|
||||
uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
poll[2] = pollCounter;
|
||||
if (pitpat) {
|
||||
uint8_t poll[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("pitpat poll"), false, true);
|
||||
} else {
|
||||
uint8_t poll[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
poll[2] = pollCounter;
|
||||
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false,
|
||||
true);
|
||||
writeCharacteristic(gattWriteCharacteristic, poll, sizeof(poll), QStringLiteral("poll"), false,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
pollCounter++;
|
||||
@@ -265,13 +374,16 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
emit debug(QStringLiteral(" << ") + QString::number(value.length()) + QStringLiteral(" ") + value.toHex(' '));
|
||||
emit packetReceived();
|
||||
|
||||
if (newValue.length() < 51)
|
||||
if ((newValue.length() < 51 && !pitpat) || (newValue.length() < 50 && pitpat))
|
||||
return;
|
||||
|
||||
lastPacket = value;
|
||||
// lastState = value.at(0);
|
||||
|
||||
double speed = ((double)(((value[9] << 8) & 0xff) + value[10]) / 100.0);
|
||||
if(pitpat) {
|
||||
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
|
||||
}
|
||||
double incline = 0.0;
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -343,6 +455,42 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
void deerruntreadmill::btinit(bool startTape) {
|
||||
if (pitpat) {
|
||||
// PitPat treadmill initialization sequence
|
||||
uint8_t initData1[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("pitpat init 1"), false, true);
|
||||
|
||||
uint8_t unlockData[] = {0x6b, 0x05, 0x9d, 0x98, 0x43};
|
||||
writeUnlockCharacteristic(unlockData, sizeof(unlockData), QStringLiteral("pitpat unlock"), false);
|
||||
|
||||
uint8_t initData2[] = {0x6a, 0x05, 0xd7, 0xd2, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("pitpat init 2"), false, true);
|
||||
|
||||
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
|
||||
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
|
||||
} else if (superun_ba04) {
|
||||
// Superun BA04 treadmill initialization sequence
|
||||
// Wait for initial packet from treadmill before sending init
|
||||
emit debug(QStringLiteral("BA04: waiting for initial packet..."));
|
||||
waitForAPacket();
|
||||
|
||||
// Init 1: pollCounter = 0
|
||||
uint8_t initData1[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
initData1[2] = 0; // pollCounter = 0
|
||||
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("BA04 init 1"), false, true);
|
||||
|
||||
uint8_t initData2[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
|
||||
initData1[2] = 1; // pollCounter = 0
|
||||
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("BA04 init 2"), false, true);
|
||||
|
||||
// Init 2: pollCounter = 1
|
||||
uint8_t initData3[] = {0x4d, 0x00, 0x01, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x05, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0x7c, 0x43};
|
||||
initData3[2] = 2; // pollCounter = 1
|
||||
writeCharacteristic(gattWriteCharacteristic, initData3, sizeof(initData3), QStringLiteral("BA04 init 3"), false, true);
|
||||
|
||||
// Start pollCounter from 2 after init
|
||||
pollCounter = 3;
|
||||
}
|
||||
initDone = true;
|
||||
}
|
||||
|
||||
@@ -352,11 +500,38 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
|
||||
QBluetoothUuid _gattWriteCharacteristicId((quint16)0xfff1);
|
||||
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2);
|
||||
QBluetoothUuid _pitpatWriteCharacteristicId((quint16)0xfba1);
|
||||
QBluetoothUuid _pitpatNotifyCharacteristicId((quint16)0xfba2);
|
||||
QBluetoothUuid _superunWriteCharacteristicId((quint16)0xff01);
|
||||
QBluetoothUuid _superunNotifyCharacteristicId((quint16)0xff02);
|
||||
QBluetoothUuid _unlockCharacteristicId((quint16)0x2b2a);
|
||||
QBluetoothUuid _unlockCharacteristicId2((quint16)0x2b11);
|
||||
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
|
||||
QLowEnergyService* service = qobject_cast<QLowEnergyService*>(sender());
|
||||
if (service == unlock_service && pitpat) {
|
||||
// Handle unlock service characteristics
|
||||
auto characteristics_list = unlock_service->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("unlock char uuid") << c.uuid() << QStringLiteral("handle") << c.handle()
|
||||
<< c.properties();
|
||||
}
|
||||
|
||||
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId);
|
||||
if (unlock_characteristic.isValid()) {
|
||||
emit debug(QStringLiteral("unlock characteristic found"));
|
||||
} else {
|
||||
qDebug() << "unlock char not found, let's try the other one";
|
||||
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId2);
|
||||
}
|
||||
|
||||
qDebug() << "unlock_characteristic" << unlock_characteristic.isValid();
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << gattCommunicationChannelService->characteristics();
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
@@ -364,8 +539,17 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
<< c.properties();
|
||||
}
|
||||
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
if (pitpat) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_pitpatWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_pitpatNotifyCharacteristicId);
|
||||
} else if (superun_ba04) {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_superunWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_superunNotifyCharacteristicId);
|
||||
} else {
|
||||
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
|
||||
}
|
||||
|
||||
Q_ASSERT(gattWriteCharacteristic.isValid());
|
||||
Q_ASSERT(gattNotifyCharacteristic.isValid());
|
||||
|
||||
@@ -403,6 +587,10 @@ void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha
|
||||
|
||||
void deerruntreadmill::serviceScanDone(void) {
|
||||
QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0);
|
||||
QBluetoothUuid _pitpatServiceId((quint16)0xfba0);
|
||||
QBluetoothUuid _superunServiceId((quint16)0xffff);
|
||||
QBluetoothUuid _unlockServiceId((quint16)0x1801);
|
||||
QBluetoothUuid _unlockServiceId2((quint16)0x1910);
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
@@ -411,7 +599,42 @@ void deerruntreadmill::serviceScanDone(void) {
|
||||
emit debug(s.toString());
|
||||
}
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
// Try to create service objects for each variant
|
||||
// On iOS, services_list.contains() doesn't work reliably, so we try to create the service directly
|
||||
QLowEnergyService* pitpat_service = m_control->createServiceObject(_pitpatServiceId);
|
||||
QLowEnergyService* superun_service = m_control->createServiceObject(_superunServiceId);
|
||||
QLowEnergyService* default_service = m_control->createServiceObject(_gattCommunicationChannelServiceId);
|
||||
|
||||
// Check which service was successfully created
|
||||
if (pitpat_service) {
|
||||
pitpat = true;
|
||||
emit debug(QStringLiteral("Detected pitpat treadmill variant"));
|
||||
gattCommunicationChannelService = pitpat_service;
|
||||
unlock_service = m_control->createServiceObject(_unlockServiceId);
|
||||
if(!unlock_service) {
|
||||
qDebug() << "unlock service not found, let's try with another one";
|
||||
unlock_service = m_control->createServiceObject(_unlockServiceId2);
|
||||
}
|
||||
|
||||
qDebug() << "unlock service " << unlock_service;
|
||||
|
||||
// Clean up unused services
|
||||
if (superun_service) delete superun_service;
|
||||
if (default_service) delete default_service;
|
||||
} else if (superun_service) {
|
||||
superun_ba04 = true;
|
||||
pitpat = false;
|
||||
emit debug(QStringLiteral("Detected Superun BA04 treadmill variant"));
|
||||
gattCommunicationChannelService = superun_service;
|
||||
|
||||
// Clean up unused services
|
||||
if (default_service) delete default_service;
|
||||
} else if (default_service) {
|
||||
pitpat = false;
|
||||
emit debug(QStringLiteral("Detected default treadmill variant"));
|
||||
gattCommunicationChannelService = default_service;
|
||||
}
|
||||
|
||||
if (gattCommunicationChannelService) {
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
|
||||
&deerruntreadmill::stateChanged);
|
||||
@@ -419,6 +642,12 @@ void deerruntreadmill::serviceScanDone(void) {
|
||||
} else {
|
||||
emit debug(QStringLiteral("error on find Service"));
|
||||
}
|
||||
|
||||
if (pitpat && unlock_service) {
|
||||
connect(unlock_service, &QLowEnergyService::stateChanged, this,
|
||||
&deerruntreadmill::stateChanged);
|
||||
unlock_service->discoverDetails();
|
||||
}
|
||||
}
|
||||
|
||||
void deerruntreadmill::errorService(QLowEnergyService::ServiceError err) {
|
||||
|
||||
@@ -41,6 +41,7 @@ class deerruntreadmill : public treadmill {
|
||||
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
|
||||
bool connected() override;
|
||||
double minStepInclination() override;
|
||||
double minStepSpeed() override { return 0.1; }
|
||||
|
||||
private:
|
||||
void forceSpeed(double requestSpeed);
|
||||
@@ -48,8 +49,11 @@ class deerruntreadmill : public treadmill {
|
||||
void btinit(bool startTape);
|
||||
void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len,
|
||||
const QString &info, bool disable_log = false, bool wait_for_response = false);
|
||||
void writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false);
|
||||
void waitForAPacket();
|
||||
void startDiscover();
|
||||
uint8_t calculateXOR(uint8_t arr[], size_t size);
|
||||
uint8_t calculatePitPatChecksum(uint8_t arr[], size_t size);
|
||||
bool noConsole = false;
|
||||
bool noHeartService = false;
|
||||
uint32_t pollDeviceTime = 200;
|
||||
@@ -66,6 +70,12 @@ class deerruntreadmill : public treadmill {
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattWriteCharacteristic;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
QLowEnergyService *unlock_service = nullptr;
|
||||
QLowEnergyCharacteristic unlock_characteristic;
|
||||
|
||||
bool pitpat = false;
|
||||
bool superun_ba04 = false;
|
||||
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
@@ -27,6 +27,9 @@ using namespace std::chrono_literals;
|
||||
OP(WAHOO_RPM_SPEED, "Wahoo SPEED $uuid_hex$", DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
|
||||
OP(WAHOO_TREADMILL, "Wahoo TREAD $uuid_hex$", DM_MACHINE_TYPE_TREADMILL, P1, P2, P3)
|
||||
|
||||
#define DM_MACHINE_OP_ROUVY(OP, P1, P2, P3) \
|
||||
OP(WAHOO_KICKR, "ELITE AVANTI $uuid_hex$ W", DM_MACHINE_TYPE_TREADMILL | DM_MACHINE_TYPE_BIKE, P1, P2, P3)
|
||||
|
||||
#define DP_PROCESS_WRITE_0003() (zwift_play_emulator ? writeP0003 : 0)
|
||||
#define DP_PROCESS_WRITE_2AD9() writeP2AD9
|
||||
#define DP_PROCESS_WRITE_2AD9T() writeP2AD9
|
||||
@@ -43,7 +46,7 @@ using namespace std::chrono_literals;
|
||||
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
|
||||
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0xE005, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_E005, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2AD2, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_CYCLE, 0x2AD3, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x00\x01"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
@@ -51,7 +54,7 @@ using namespace std::chrono_literals;
|
||||
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_TREADMILL, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
|
||||
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
|
||||
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
|
||||
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
|
||||
P3) \
|
||||
OP(FITNESS_MACHINE_TREADMILL, 0x2ACD, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, \
|
||||
P3) \
|
||||
@@ -127,13 +130,17 @@ enum {
|
||||
} \
|
||||
} \
|
||||
if (P2.size()) { \
|
||||
QString dircon_id = QString("%1").arg(settings.value(QZSettings::dircon_id, \
|
||||
QZSettings::default_dircon_id).toInt(), 4, 10, QChar('0')); \
|
||||
int dircon_id_int = settings.value(QZSettings::dircon_id, \
|
||||
QZSettings::default_dircon_id).toInt(); \
|
||||
if (rouvy_compatibility && dircon_id_int == 0) { \
|
||||
dircon_id_int = 1234; \
|
||||
} \
|
||||
QString dircon_id = QString("%1").arg(dircon_id_int, rouvy_compatibility ? 5 : 4, 10, QChar('0')); \
|
||||
DirconProcessor *processor = new DirconProcessor( \
|
||||
P2, \
|
||||
QString(QStringLiteral(NAME)) \
|
||||
.replace(QStringLiteral("$uuid_hex$"), dircon_id), \
|
||||
server_base_port + DM_MACHINE_##DESC, QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \
|
||||
server_base_port + DM_MACHINE_##DESC, rouvy_compatibility ? dircon_id : QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \
|
||||
this); \
|
||||
QString servdesc; \
|
||||
foreach (DirconProcessorService *s, P2) { servdesc += *s + QStringLiteral(","); } \
|
||||
@@ -146,6 +153,24 @@ enum {
|
||||
}
|
||||
|
||||
QString DirconManager::getMacAddress() {
|
||||
QSettings settings;
|
||||
bool rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
|
||||
int dircon_id = settings.value(QZSettings::dircon_id, QZSettings::default_dircon_id).toInt();
|
||||
|
||||
// When Rouvy compatibility is enabled and dircon_id is 0, use 1234 instead
|
||||
if (rouvy_compatibility && dircon_id == 0) {
|
||||
dircon_id = 1234;
|
||||
}
|
||||
|
||||
// When Rouvy compatibility is enabled, use a specific MAC address with the last byte set to dircon_id
|
||||
if (rouvy_compatibility) {
|
||||
// Use base MAC address "24:DC:C3:E3:B5:XX" where XX is the dircon_id
|
||||
// Ensure dircon_id is in the valid range 0-255
|
||||
int last_byte = dircon_id & 0xFF;
|
||||
return QString("24:DC:C3:E3:B5:%1").arg(last_byte, 2, 16, QChar('0')).toUpper();
|
||||
}
|
||||
|
||||
// Default behavior: get MAC address from network interfaces
|
||||
QString addr;
|
||||
foreach (QNetworkInterface netInterface, QNetworkInterface::allInterfaces()) {
|
||||
// Return only the first non-loopback MAC Address
|
||||
@@ -171,15 +196,16 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
|
||||
QSettings settings;
|
||||
DirconProcessorService *service;
|
||||
QList<DirconProcessorService *> services, proc_services;
|
||||
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
BLUETOOTH_TYPE dt = Bike->deviceType();
|
||||
bt = Bike;
|
||||
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
|
||||
uint8_t type = dt == TREADMILL || dt == ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
|
||||
: DM_MACHINE_TYPE_BIKE;
|
||||
qDebug() << "Building Dircom Manager";
|
||||
uint16_t server_base_port =
|
||||
settings.value(QZSettings::dircon_server_base_port, QZSettings::default_dircon_server_base_port).toUInt();
|
||||
bool bike_wheel_revs = settings.value(QZSettings::bike_wheel_revs, QZSettings::default_bike_wheel_revs).toBool();
|
||||
bool zwift_play_emulator = settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool();
|
||||
bool rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
|
||||
|
||||
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_BUILD_OP, Bike, 0, 0)
|
||||
|
||||
@@ -209,7 +235,11 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
|
||||
|
||||
QObject::connect(&bikeTimer, &QTimer::timeout, this, &DirconManager::bikeProvider);
|
||||
QString mac = getMacAddress();
|
||||
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
|
||||
if (rouvy_compatibility) {
|
||||
DM_MACHINE_OP_ROUVY(DM_MACHINE_INIT_OP, services, proc_services, type)
|
||||
} else {
|
||||
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
|
||||
}
|
||||
|
||||
if (zwift_play_emulator || settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
|
||||
bikeTimer.start(50ms);
|
||||
@@ -236,7 +266,7 @@ double DirconManager::currentGear() {
|
||||
QSettings settings;
|
||||
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool() && writeP0003)
|
||||
return writeP0003->currentGear();
|
||||
else if(bt && bt->deviceType() == bluetoothdevice::BIKE)
|
||||
else if(bt && bt->deviceType() == BIKE)
|
||||
return ((bike*)bt)->gears();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ int DirconPacket::parse(const QByteArray &buf, int last_seq_number) {
|
||||
} else
|
||||
return DPKT_PARSE_ERROR - rembuf;
|
||||
} else if (this->Identifier == DPKT_MSGID_ENABLE_CHARACTERISTIC_NOTIFICATIONS) {
|
||||
if (this->Length == 16 || this->Length == 17) {
|
||||
if (this->Length >= 16) {
|
||||
quint16 uuid = ((quint16)buf.at(DPKT_MESSAGE_HEADER_LENGTH + DPKT_POS_SH8)) << 8;
|
||||
uuid |= ((quint16)buf.at(DPKT_MESSAGE_HEADER_LENGTH + DPKT_POS_SH0)) & 0x00FF;
|
||||
this->uuid = uuid;
|
||||
if (this->Length == 17) {
|
||||
if (this->Length >= 17) {
|
||||
this->isRequest = true;
|
||||
this->additional_data = buf.mid(DPKT_MESSAGE_HEADER_LENGTH + 16, 1);
|
||||
}
|
||||
@@ -117,6 +117,12 @@ int DirconPacket::parse(const QByteArray &buf, int last_seq_number) {
|
||||
return rembuf;
|
||||
} else
|
||||
return DPKT_PARSE_ERROR - rembuf;
|
||||
} else if (this->Identifier == DPKT_MSGID_UNKNOWN_0x07) {
|
||||
if (this->Length == 0) {
|
||||
this->isRequest = this->checkIsRequest(last_seq_number);
|
||||
return DPKT_MESSAGE_HEADER_LENGTH;
|
||||
} else
|
||||
return DPKT_PARSE_ERROR - rembuf;
|
||||
} else
|
||||
return DPKT_PARSE_ERROR - rembuf;
|
||||
} else
|
||||
@@ -182,6 +188,10 @@ QByteArray DirconPacket::encode(int last_seq_number) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this->Identifier == DPKT_MSGID_UNKNOWN_0x07) {
|
||||
// Unknown message 0x07 - always respond with empty payload
|
||||
this->Length = 0;
|
||||
byteout.append(2, 0);
|
||||
} else if (this->Identifier == DPKT_MSGID_DISCOVER_CHARACTERISTICS && !this->isRequest) {
|
||||
this->Length = 16 + this->uuids.size() * 17;
|
||||
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#define DPKT_MSGID_WRITE_CHARACTERISTIC 0x04
|
||||
#define DPKT_MSGID_ENABLE_CHARACTERISTIC_NOTIFICATIONS 0x05
|
||||
#define DPKT_MSGID_UNSOLICITED_CHARACTERISTIC_NOTIFICATION 0x06
|
||||
#define DPKT_MSGID_UNKNOWN_0x07 0x07
|
||||
#define DPKT_RESPCODE_SUCCESS_REQUEST 0x00
|
||||
#define DPKT_RESPCODE_UNKNOWN_MESSAGE_TYPE 0x01
|
||||
#define DPKT_RESPCODE_UNEXPECTED_ERROR 0x02
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user