mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
345 Commits
build-1206
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a3696886f | ||
|
|
9bb1bd68cd | ||
|
|
40aedaec71 | ||
|
|
8ce5ec8468 | ||
|
|
8477731f89 | ||
|
|
61f3602980 | ||
|
|
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 | ||
|
|
ddab20e841 | ||
|
|
a0eb19690a | ||
|
|
9375f15207 | ||
|
|
24183a4968 | ||
|
|
694c895fac | ||
|
|
f48f28df1f | ||
|
|
9e1537caad | ||
|
|
9fe72d13c0 | ||
|
|
df5e80a5be | ||
|
|
3b751d44e6 | ||
|
|
3815e45107 | ||
|
|
580eb3f092 | ||
|
|
aba59cd136 | ||
|
|
369fbc4bc0 | ||
|
|
871e704852 | ||
|
|
b574e86804 | ||
|
|
19ca844968 | ||
|
|
24f9b72875 | ||
|
|
7c2f97fe31 | ||
|
|
ae7fe8d2db | ||
|
|
2458d009bd | ||
|
|
8b90ab8b00 | ||
|
|
c7bace3112 | ||
|
|
9a97eee780 | ||
|
|
ad39c8d51d | ||
|
|
c0ba8dcf62 | ||
|
|
fda71cda7a | ||
|
|
3c4c654378 | ||
|
|
40f9926ea0 | ||
|
|
730e78c042 | ||
|
|
6ea8ba581d | ||
|
|
0499272421 | ||
|
|
27f8883830 | ||
|
|
539b930164 | ||
|
|
0663bca5e0 | ||
|
|
bcec1b1978 | ||
|
|
b154e98289 | ||
|
|
2e98769ef1 | ||
|
|
e8f6ea07ac | ||
|
|
651cf6a59c | ||
|
|
3c229b9ae8 | ||
|
|
fab3026b84 | ||
|
|
0ce8bc9efc | ||
|
|
4201478c59 | ||
|
|
67b845b5fe | ||
|
|
c968d8ad57 | ||
|
|
8905b1ab4d | ||
|
|
ef2c6f662b | ||
|
|
5596a6cd4f | ||
|
|
fef8abea6d | ||
|
|
3889fac141 | ||
|
|
f9d8ba6925 | ||
|
|
40219ebda9 | ||
|
|
12b0cc7924 | ||
|
|
025406e170 | ||
|
|
df369471aa | ||
|
|
7df442b528 | ||
|
|
b5c4da9420 | ||
|
|
d9e1d9a1be | ||
|
|
1c85feedca | ||
|
|
660f55ad48 | ||
|
|
b871c795b8 | ||
|
|
9256af6391 | ||
|
|
c844276d86 | ||
|
|
71648a6305 | ||
|
|
a9b60bb193 | ||
|
|
7f4f652a5d | ||
|
|
1cd106b026 | ||
|
|
fc00fbf9cc | ||
|
|
2120ff6f6a | ||
|
|
bd92a66e09 | ||
|
|
ed45eac44a | ||
|
|
4d667e9ba4 | ||
|
|
0b5c2745b7 | ||
|
|
3436a6e43c | ||
|
|
e8e64e040a | ||
|
|
11c6f3b52c | ||
|
|
0b126a0aae | ||
|
|
bfe296c3a3 | ||
|
|
7f474580a2 | ||
|
|
828bb350d0 | ||
|
|
4532b05e7e | ||
|
|
277d1d7390 | ||
|
|
cd4e6b0335 | ||
|
|
57f929a3bf | ||
|
|
9728af939e | ||
|
|
a3c4916ded | ||
|
|
e5b5ba1e1e | ||
|
|
326ea8c2a2 | ||
|
|
c9c8e2ce16 | ||
|
|
8f6930709c | ||
|
|
a6c66ab9ee | ||
|
|
9f42d6a6ac | ||
|
|
0b20087da6 | ||
|
|
e37a6b28d6 | ||
|
|
26c89b0d80 | ||
|
|
7c8e411374 | ||
|
|
a31bf49121 | ||
|
|
c2ec6a9a9b | ||
|
|
fa2ff41e4e | ||
|
|
0ff3fb3651 | ||
|
|
39cc4f75f4 | ||
|
|
8b6ce6fa9d | ||
|
|
f84ec511ad | ||
|
|
019264c6c0 | ||
|
|
b4a9369a43 | ||
|
|
d1bd43ea2b | ||
|
|
21e7b0b1ce | ||
|
|
6b85ba1d3a | ||
|
|
99eb5c5f57 | ||
|
|
59f9d0a553 | ||
|
|
9d3039d748 | ||
|
|
249e0191fb | ||
|
|
7a4861f265 | ||
|
|
f85e1fd39e | ||
|
|
a2ba9c69f7 | ||
|
|
df58ff226f | ||
|
|
bd95b67e06 | ||
|
|
f1d1929846 | ||
|
|
fa4bdb2a6b | ||
|
|
a84b57f1d9 | ||
|
|
cc86e26eac | ||
|
|
87a1e125ca | ||
|
|
6bdf6170c3 | ||
|
|
7369623dfd | ||
|
|
a00ddc5890 | ||
|
|
74fbfcda63 | ||
|
|
22b5ba6a02 | ||
|
|
49bdea89a3 | ||
|
|
42c9d170c3 | ||
|
|
89896c5ee9 | ||
|
|
1c9044a66d | ||
|
|
eb573d1029 | ||
|
|
29a93eb315 | ||
|
|
54bc585323 | ||
|
|
f5b26776d2 | ||
|
|
fccf1f2073 | ||
|
|
6e9093bc3c | ||
|
|
df37d4f2a6 | ||
|
|
d9dbe5db20 | ||
|
|
dd1c0c1cb0 | ||
|
|
b8c0a560bf | ||
|
|
89b62c8b6d | ||
|
|
70ea4bfc24 | ||
|
|
00c9d28af0 | ||
|
|
752f3aaf19 | ||
|
|
7e8f744c7b | ||
|
|
1dbdd63b3c | ||
|
|
6b8d96cf7c | ||
|
|
a0bcd8caab | ||
|
|
e46e4daf64 | ||
|
|
8fbd55262d | ||
|
|
487ec5d187 | ||
|
|
090e68979e | ||
|
|
23eebc8be1 | ||
|
|
2eee3e3cc3 | ||
|
|
1f371248d5 | ||
|
|
2bb1cb20de | ||
|
|
16b8805164 | ||
|
|
ae149876a5 | ||
|
|
9042f4857d | ||
|
|
45e06cc807 | ||
|
|
21e341d3d4 |
3690
.github/workflows/main.yml
vendored
3690
.github/workflows/main.yml
vendored
File diff suppressed because it is too large
Load Diff
@@ -370,5 +370,7 @@ The ProForm 995i implementation serves as the reference example:
|
||||
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
- When adding a new setting in QML (settings.qml), you must:
|
||||
* Add the property at the END of the properties list (before the closing brace)
|
||||
* NEVER add properties in the middle of the properties list
|
||||
* This applies to ALL QML settings properties, not just setting-tiles.qml
|
||||
@@ -523,6 +523,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 */; };
|
||||
@@ -1613,6 +1615,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>"; };
|
||||
@@ -2333,6 +2338,9 @@
|
||||
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
|
||||
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */,
|
||||
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */,
|
||||
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */,
|
||||
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
|
||||
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
|
||||
@@ -4032,6 +4040,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 */,
|
||||
@@ -4569,7 +4579,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4770,7 +4780,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5007,7 +5017,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5058,7 +5068,7 @@
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 6.0;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -5103,7 +5113,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5150,7 +5160,7 @@
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 6.0;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -5195,7 +5205,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5266,7 +5276,7 @@
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 6.0;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -5311,7 +5321,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5379,7 +5389,7 @@
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 6.0;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -5421,7 +5431,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5512,7 +5522,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1206;
|
||||
CURRENT_PROJECT_VERSION = 1241;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
||||
@@ -29,6 +29,7 @@ class WatchKitConnection: NSObject {
|
||||
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() {
|
||||
@@ -85,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)
|
||||
})
|
||||
|
||||
@@ -37,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 {
|
||||
@@ -177,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 {
|
||||
@@ -188,6 +190,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
}
|
||||
@@ -206,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())
|
||||
@@ -354,7 +359,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
// Guard to check if steps quantity type is available
|
||||
guard let quantityTypeSteps = HKQuantityType.quantityType(
|
||||
forIdentifier: .stepCount) else {
|
||||
@@ -362,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,
|
||||
@@ -370,55 +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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,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
|
||||
@@ -443,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)
|
||||
|
||||
@@ -10,6 +10,6 @@ INCLUDEPATH += $$PWD/src/qmdnsengine/src/include
|
||||
|
||||
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/src/android
|
||||
|
||||
ANDROID_ABIS = armeabi-v7a arm64-v8a x86 x86_64
|
||||
ANDROID_ABIS = arm64-v8a
|
||||
|
||||
#QMAKE_CXXFLAGS += -Werror=suggest-override
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 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
|
||||
@@ -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,7 +104,7 @@ 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 qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
@@ -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
|
||||
|
||||
91
src/GrupettoDisclaimer.qml
Normal file
91
src/GrupettoDisclaimer.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
|
||||
Dialog {
|
||||
id: disclaimerDialog
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Dialog.NoAutoClose
|
||||
|
||||
width: Math.min(parent.width * 0.9, 600)
|
||||
height: Math.min(parent.height * 0.8, 500)
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
property bool wasShown: settings.grupetto_disclaimer_shown || false
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.Orange
|
||||
|
||||
header: Rectangle {
|
||||
height: 60
|
||||
color: Material.color(Material.Orange)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Legal Disclaimer - Grupetto Integration"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
color: "white"
|
||||
font.pixelSize: 14
|
||||
lineHeight: 1.3
|
||||
|
||||
text: "IMPORTANT LEGAL NOTICE - THIRD-PARTY CODE DISCLAIMER\n\n" +
|
||||
"This application incorporates code derived from the Grupetto project " +
|
||||
"(https://github.com/spencerpayne/grupetto), which enables communication " +
|
||||
"with Peloton fitness equipment sensors.\n\n" +
|
||||
|
||||
"LIABILITY DISCLAIMER:\n\n" +
|
||||
"1. The Grupetto-derived code is provided \"AS IS\" without any warranties " +
|
||||
"of any kind, either expressed or implied.\n\n" +
|
||||
|
||||
"2. The author of QDomyos-Zwift DISCLAIMS ALL RESPONSIBILITY AND LIABILITY " +
|
||||
"for any damages, losses, or issues arising from the use of Grupetto-derived code, " +
|
||||
"including but not limited to:\n" +
|
||||
" • Equipment damage or malfunction\n" +
|
||||
" • Data loss or corruption\n" +
|
||||
" • Personal injury\n" +
|
||||
" • Software crashes or instability\n" +
|
||||
" • Unauthorized access to device systems\n\n" +
|
||||
|
||||
"3. Users assume full responsibility and risk when using features that rely " +
|
||||
"on Grupetto-derived code for Peloton sensor integration.\n\n" +
|
||||
|
||||
"4. This disclaimer does not affect the warranty or liability for other " +
|
||||
"parts of QDomyos-Zwift not derived from Grupetto.\n\n" +
|
||||
|
||||
"5. By clicking 'OK', you acknowledge that you have read, understood, " +
|
||||
"and agree to this disclaimer.\n\n" +
|
||||
|
||||
"ATTRIBUTION:\n" +
|
||||
"Portions of this software are derived from Grupetto, developed by Spencer Payne. " +
|
||||
"Original project: https://github.com/spencerpayne/grupetto"
|
||||
}
|
||||
}
|
||||
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
onAccepted: {
|
||||
settings.grupetto_disclaimer_shown = true
|
||||
close()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!wasShown) {
|
||||
open()
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Home.qml
48
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;
|
||||
@@ -185,6 +189,8 @@ HomeForm {
|
||||
gridView.leftMargin = (parent.width % cellWidth) / 2;
|
||||
}
|
||||
|
||||
Accessible.ignored: true
|
||||
|
||||
delegate: Item {
|
||||
id: id1
|
||||
width: 170 * settings.ui_zoom / 100
|
||||
@@ -193,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 }
|
||||
@@ -226,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 {
|
||||
@@ -256,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"
|
||||
@@ -270,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"
|
||||
@@ -285,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
|
||||
@@ -299,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
|
||||
@@ -311,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
|
||||
@@ -323,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
|
||||
@@ -336,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -74,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 {
|
||||
@@ -95,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 {
|
||||
@@ -106,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
|
||||
@@ -145,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)
|
||||
|
||||
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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -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.11" android:versionCode="1155" 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.21" android:versionCode="1240" 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 -->
|
||||
@@ -140,4 +140,8 @@
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- Peloton sensor integration permissions (based on Grupetto analysis) -->
|
||||
<uses-permission android:name="onepeloton.permission.ACCESS_SENSOR_SERVICE" />
|
||||
<uses-permission android:name="onepeloton.permission.SUBSCRIPTION_TYPE_ACCESS" />
|
||||
</manifest>
|
||||
|
||||
21
src/android/assets/ca_cert.pem
Normal file
21
src/android/assets/ca_cert.pem
Normal file
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDeTCCAmGgAwIBAgIUbbOvLluQ8WhwXEL54Z4s9/T3BO4wDQYJKoZIhvcNAQEL
|
||||
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIz
|
||||
MTAyNTIxMDkzM1oXDTMzMTAyMjIxMDkzM1owVjELMAkGA1UEBhMCQVUxEzARBgNV
|
||||
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
|
||||
ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEAySGlxDrbSL9U1N65oYCNpnlXgFgY/uZViJ1wPN92xbsiYCKV5VBEKhA6fKh9
|
||||
K9+VvMqxNycXMpXhXj4YI2hP6MktnOGkz/7RA5lKQGu7fCY/1tutGECfKmKhudWn
|
||||
kvDgPJPxZr1mwqQjuFVSVcV0e763lGE/QdrdsndHjIjJOB5nZ1Q67Ga6tkXQYjtb
|
||||
A6fw0LiZ9xJB/dpZ90wVIfaP22tFVgBBkFvnb91+/fA9dNsjtCRVgzz/qdoQbWF0
|
||||
WMP8PE9jlA0x0cmd+yP6MIQaTqf1j3XSiLvPph/4DeWjcpA3R6Xh515iVRbAXrfO
|
||||
tl5p44mjQYUpOxcZmrl7szGOqwIDAQABoz8wPTAMBgNVHRMEBTADAQH/MA4GA1Ud
|
||||
DwEB/wQEAwICBDAdBgNVHQ4EFgQUpbZ5I+JmUaNH8Idzi8j4D9PiepkwDQYJKoZI
|
||||
hvcNAQELBQADggEBAK+9zI1R56gAXv1bHsb6lQrMHHkWdY/xtiDBrTGC9WssKcx3
|
||||
Lfzy9ajzb7T0tVwus2qfM1QUFD53WqusYpA969r3t17/J+7esIyld6193g3aPS5r
|
||||
STrCn8LOmJ+GDgMWU57a2KFNgi3LxtZQeXP1wP10bBWZ8TbYZ5Z5rKbLsnVdc7su
|
||||
gvdg/cH5XQol2jiA1QT076yiUereNkQHNnQW/XuPL30p11Lwzvm0mtBp7lohGZK3
|
||||
zshpXndf741pjdjkUU0OJ/ZhJJycZs6j9xBvElZcFiPiA7S3fuE9APSHaXiTb/AZ
|
||||
4ypwTg9TrqpWG/foB8OdtRe0nbpdOyVPZVC1kSk=
|
||||
-----END CERTIFICATE-----
|
||||
20
src/android/assets/client_cert.pem
Normal file
20
src/android/assets/client_cert.pem
Normal file
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDQTCCAimgAwIBAgIUXcT0gdvvszPRFgr0N1RpnEpZqkgwDQYJKoZIhvcNAQEL
|
||||
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0
|
||||
MDcwMzE5MDc1NVoXDTM0MDcwMTE5MDc1NVowGzEZMBcGA1UEAwwQY29tLmlmaXQu
|
||||
ZXJpYWRvcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALP7690AqHha
|
||||
e2HQ+Adr6awIb9ebJd5g46Feu+WF4XAFOQGvVihXEOHANpriN1c6Rz6xuEPRTtZR
|
||||
B+Wt82ajHPi/tWwfImWGvQd0wdqOs+hcdR5Hxg15CxHHFGvGdFWZumO3gSm62mvo
|
||||
yBUlZX2RpZ0ZJYFuy8Z1GZQmiym4peQZpCNi8YLzKZQNefBXfqLra6/W9vwN35zt
|
||||
UW82jMT4VQEWRU7PgF7U1Svbu8fja4cK5mh8JX/vESXXkIUOlLAonjCNJ+0eh+5k
|
||||
+HEd3sxeKdb4bAkB4UixtUWSf4kzkqzRufwwC/0Mry3UE8byL8J+Bk5L4H5AT3Rl
|
||||
sBMGPeYeWzMCAwEAAaNCMEAwHQYDVR0OBBYEFOZ3xbUHLCiCbX//Qj87HlmYhbvL
|
||||
MB8GA1UdIwQYMBaAFKW2eSPiZlGjR/CHc4vI+A/T4nqZMA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQBXjaqAEgOtaGkmmQLus0sNItE6hJH7r58tmHF19iQGcXnOaMYxyF36i9M2
|
||||
rFBinybQUJ68A74Uz/R7YdOJxcOonSXC1A5/8mUJmlUAQmp+mkdgU68P/pZ1uxUV
|
||||
tyHd+u+J6CUN1qJfmeb0dq532cVJD0TUK8/NbmySpvhsKpVFCIEnUh4DQinkvgAk
|
||||
zheN/qabNwBYflUQOc9Ce5BPYYIGJM96KMofN0ZqbDjjtqgPqvq4SvDBAjvaof9y
|
||||
4Wjiz2TTJCWwmE1/MnRs0N56j147BvTX+9r5k90CESWUv3sCiHYtTN81LcL01DxS
|
||||
mBpnVS9EDui2Lm4FslkSkerCnTfa
|
||||
-----END CERTIFICATE-----
|
||||
28
src/android/assets/client_key.pem
Normal file
28
src/android/assets/client_key.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz++vdAKh4Wnth
|
||||
0PgHa+msCG/XmyXeYOOhXrvlheFwBTkBr1YoVxDhwDaa4jdXOkc+sbhD0U7WUQfl
|
||||
rfNmoxz4v7VsHyJlhr0HdMHajrPoXHUeR8YNeQsRxxRrxnRVmbpjt4Eputpr6MgV
|
||||
JWV9kaWdGSWBbsvGdRmUJospuKXkGaQjYvGC8ymUDXnwV36i62uv1vb8Dd+c7VFv
|
||||
NozE+FUBFkVOz4Be1NUr27vH42uHCuZofCV/7xEl15CFDpSwKJ4wjSftHofuZPhx
|
||||
Hd7MXinW+GwJAeFIsbVFkn+JM5Ks0bn8MAv9DK8t1BPG8i/CfgZOS+B+QE90ZbAT
|
||||
Bj3mHlszAgMBAAECggEAUN959Cw/hxThK+rCCFOtA+gmmTLVqT7QCcqPk2q9CaDP
|
||||
JLqsdCPrKgU8hAvx4fgF213v9kkuq45thf7Lx+qzMfKyiorS4dvRRHBqStKkdFxX
|
||||
I+wMSjGBj9NskaDy1SPmZLgoCaA0VRicDyRmni27xQNvnuEyH1Ku06seDPkzUXKS
|
||||
+7YtDdHjuh7rfZdN9phkwcM4qJ7ScElr+WP5DL42AhuL7e0bu8EYCZNrgdV826p+
|
||||
/I8eRu4LNYEZ/XnNhKt6I+Qovlq2dLgb0cyMFqOUjPp2CDRkJFIC9E2Llg3AUOnX
|
||||
jJCvBdNkXIh/PsUHx2C7pxg7cUuNvyqnUP/dyxSbgQKBgQDgpTxKEPnit412huRB
|
||||
6J7XbcQHJWypzm2634rIguAKdf+lPFmBcAAVQAJ0mkzX0K9a+6xAlyimrjrMFwVn
|
||||
WndFL9N8KKOsGPryDBMiUtCwROwYjZNQ4ToTMwtOB1Ih1+e6hWLqJWM38nlp1RW7
|
||||
R0qpcYeRoqnl+sirw08DOoh+6wKBgQDNGuX4J1wWs049Kmq4v0BPUacMqq5T7Y1S
|
||||
PgYn16A69lC2qW/cgAB2HAOoOBS+0i6GbQmF/tptN97XOD5an2c4vSQbKKqGkyYk
|
||||
oXl46uqACJBMgR0WaergrcBKuKvnfURVpVNlG08+wsnEGb5apCiyIK4H+g68R/Qr
|
||||
68jniWrS2QKBgQCv81u0W3WNiNzpICA6Kzv2Wgf23O4uVfwGKT6nbDKUnvV78zfb
|
||||
tOCrxDXoJE7Znp8qMQMql/qECuUMo19dIzNV4m7PyXjgu7QZzzFRafIAjgsp9AGV
|
||||
kMMO9KT/GabP0S60HfNql5wN3wIPzZE23VDyRHS9sd1Gv1Vbix8g1UDBvwKBgGBO
|
||||
sg88xBPwq9sysJwBSbw09gCPoH3OPJ6Seyd4K0ekYy/yDZF3FUBgVSNG+g7D+I6s
|
||||
Yl1l1sCUDHH4eebplHli7rJF/RRlwfJPVA+AFw55dvBFbBgbMevAClvLrQRsoIqq
|
||||
r6b5FNO+eSk4gVZkYKuLhsw+EW89RhzdgR+fOea5AoGAPNa30OpFIRY1ViyAu+Nm
|
||||
0bAKDHZXRajOSYzsSeJI7BjlNtRDNDJfcUjYtpJGk8SOFV2Y0IOIlN3GYCO1x/0V
|
||||
G7U6EDAYYun+mlP91d8IHRAWcvIiZNuqP8IO2MZRen1jEOhTF9GKsrAdN+1moeB5
|
||||
qziU9kATRT7PSCd0NhvhDXE=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -26,11 +26,35 @@ apply plugin: 'com.google.protobuf'
|
||||
def amazon = System.getenv('AMAZON')
|
||||
println(amazon)
|
||||
|
||||
// FIXED: Force resolution con versioni consistenti
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
// TUTTE le versioni devono essere consistenti
|
||||
force 'com.google.protobuf:protobuf-javalite:3.25.3'
|
||||
force 'io.grpc:grpc-okhttp:1.63.0'
|
||||
force 'io.grpc:grpc-protobuf-lite:1.63.0'
|
||||
force 'io.grpc:grpc-stub:1.63.0'
|
||||
force 'io.grpc:grpc-core:1.63.0'
|
||||
}
|
||||
// Exclude full protobuf from ALL dependencies
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core:1.12.0"
|
||||
implementation "androidx.core:core-ktx:1.12.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
|
||||
// Peloton sensor integration uses Android system service binding
|
||||
// No additional external dependencies required beyond standard Android APIs
|
||||
|
||||
// FIXED: Una sola versione di protobuf-javalite
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.3'
|
||||
|
||||
implementation 'io.grpc:grpc-okhttp:1.63.0'
|
||||
implementation 'io.grpc:grpc-protobuf-lite:1.63.0'
|
||||
implementation 'io.grpc:grpc-stub:1.63.0'
|
||||
implementation 'javax.annotation:javax.annotation-api:1.3.2'
|
||||
|
||||
if(amazon == "1") {
|
||||
// amazon app store
|
||||
@@ -47,12 +71,12 @@ dependencies {
|
||||
implementation "com.android.billingclient:billing:8.0.0"
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
||||
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation files('libs/usb-serial-for-android-3.8.1.aar')
|
||||
androidTestImplementation "com.android.support:support-annotations:28.0.0"
|
||||
implementation 'com.google.android.gms:play-services-wearable:+'
|
||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
||||
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation files('libs/usb-serial-for-android-3.8.1.aar')
|
||||
androidTestImplementation "com.android.support:support-annotations:28.0.0"
|
||||
implementation 'com.google.android.gms:play-services-wearable:+'
|
||||
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
|
||||
@@ -66,7 +90,12 @@ def archSuffix = Os.isFamily(Os.FAMILY_MAC) ? ':osx-x86_64' : ''
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:3.25.1$archSuffix"
|
||||
artifact = "com.google.protobuf:protoc:3.25.3$archSuffix"
|
||||
}
|
||||
plugins {
|
||||
grpc {
|
||||
artifact = "io.grpc:protoc-gen-grpc-java:1.63.0"
|
||||
}
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().configureEach { task ->
|
||||
@@ -75,6 +104,11 @@ protobuf {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
task.plugins {
|
||||
grpc {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +151,7 @@ android {
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
// Do not compress Qt binary resources file
|
||||
@@ -132,14 +166,43 @@ android {
|
||||
targetSdkVersion = 36
|
||||
}
|
||||
|
||||
tasks.all { task ->
|
||||
if (task.name == 'compileDebugJavaWithJavac' && amazon == "1") {
|
||||
task.dependsOn copyArm64Directory
|
||||
task.dependsOn copyArm32Directory
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXED: Packaging options ottimizzato per gestire i conflitti protobuf
|
||||
packagingOptions {
|
||||
// EXCLUDE problematic META-INF files instead of pickFirst to avoid collisions
|
||||
exclude 'META-INF/MANIFEST.MF'
|
||||
exclude 'META-INF/INDEX.LIST'
|
||||
exclude 'META-INF/io.netty.versions.properties'
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
exclude 'META-INF/AL2.0'
|
||||
exclude 'META-INF/LGPL2.1'
|
||||
|
||||
// Keep pickFirst only for files that are actually needed
|
||||
pickFirst '**/META-INF/okio.kotlin_module'
|
||||
pickFirst '**/META-INF/*.kotlin_module'
|
||||
|
||||
// CRITICAL: Handle duplicate protobuf classes - this is crucial for your error
|
||||
pickFirst '**/com/google/protobuf/**'
|
||||
|
||||
// Handle native libraries
|
||||
pickFirst '**/libprotobuf-lite.so'
|
||||
pickFirst '**/libprotoc.so'
|
||||
|
||||
// Additional common conflicts
|
||||
pickFirst '**/META-INF/services/**'
|
||||
pickFirst '**/kotlin/**'
|
||||
}
|
||||
|
||||
tasks.all { task ->
|
||||
if (task.name == 'compileDebugJavaWithJavac' && amazon == "1") {
|
||||
task.dependsOn copyArm64Directory
|
||||
task.dependsOn copyArm32Directory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task copyArm64Directory(type: Copy) {
|
||||
from "libs/arm64-v8a/"
|
||||
|
||||
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.
0
src/android/proguard-rules.pro
vendored
Normal file
0
src/android/proguard-rules.pro
vendored
Normal file
239
src/android/src/BikeData.java
Normal file
239
src/android/src/BikeData.java
Normal file
@@ -0,0 +1,239 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class BikeData implements Parcelable {
|
||||
public static final Creator<BikeData> CREATOR = new Creator<BikeData>() {
|
||||
|
||||
@Override // android.os.Parcelable.Creator
|
||||
public BikeData createFromParcel(Parcel parcel) {
|
||||
return new BikeData(parcel);
|
||||
}
|
||||
|
||||
@Override // android.os.Parcelable.Creator
|
||||
public BikeData[] newArray(int i) {
|
||||
return new BikeData[i];
|
||||
}
|
||||
};
|
||||
private long mRPM;
|
||||
private long mPower;
|
||||
private int mTargetResistance;
|
||||
|
||||
// For full compatibility, include all fields from original
|
||||
private int mADValue;
|
||||
private int mAppliedPositionOffset;
|
||||
private String mBikeFrameSerial;
|
||||
private int mCalibrationState;
|
||||
private int mCurrentResistance;
|
||||
private int mDataWriteCycle;
|
||||
private String mDataWriteDate;
|
||||
private String mDataWriteTime;
|
||||
private int mEncoderAngle;
|
||||
private int mError1Code;
|
||||
private String mError1Time;
|
||||
private int mError2Code;
|
||||
private String mError2Time;
|
||||
private int mError3Code;
|
||||
private String mError3Time;
|
||||
private int mError4Code;
|
||||
private String mError4Time;
|
||||
private int mError5Code;
|
||||
private String mError5Time;
|
||||
private int mErrorIndex;
|
||||
private int[] mErrorMap;
|
||||
private String mFWVersionNumber;
|
||||
private String mHardwareVersion;
|
||||
private int mLoadCellCalSpan;
|
||||
private float mLoadCellOffset;
|
||||
private long mLoadCellReading;
|
||||
private String mLoadCellSerial;
|
||||
private String mLoadCellTable;
|
||||
private int mLoadCellTableCrc;
|
||||
private int mLoadCellTableStatus;
|
||||
private int mLoadCellTempCount;
|
||||
private String mLoadCellVersion;
|
||||
private int mLoadCellZeroData;
|
||||
private String mPSerial;
|
||||
private int mPZAFMaxResistanceSetPoint;
|
||||
private int mPZAFMinUpdateRPM;
|
||||
private int mPZAFRampDownRate;
|
||||
private int mPZAFRampUpRate;
|
||||
private byte[] mPacketData;
|
||||
private String mPacketTime;
|
||||
private int mPositionOffset;
|
||||
private int mPowerZoneAutoFollowEnabled;
|
||||
private int mPowerZoneAutoFollowPowerSetPoint;
|
||||
private int mPowerZoneAutoFollowStatus;
|
||||
private float mPowerZoneAutoFollowTargetResistance;
|
||||
private String mQSerial;
|
||||
private float mResistanceOffset;
|
||||
private int mStallThreshold;
|
||||
private int mStepperMotorEndPosition;
|
||||
private long mStepperMotorPosition;
|
||||
private int mStepperMotorStartPosition;
|
||||
private int mSystemState;
|
||||
private float mV1Resistance;
|
||||
|
||||
@Override // android.os.Parcelable
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long getRPM() {
|
||||
return this.mRPM;
|
||||
}
|
||||
|
||||
public long getPower() {
|
||||
return this.mPower;
|
||||
}
|
||||
|
||||
public int getTargetResistance() {
|
||||
return this.mTargetResistance;
|
||||
}
|
||||
|
||||
public int getCurrentResistance() {
|
||||
return this.mCurrentResistance;
|
||||
}
|
||||
|
||||
public void setRPM(long rpm) {
|
||||
this.mRPM = rpm;
|
||||
}
|
||||
|
||||
public void setPower(long power) {
|
||||
this.mPower = power;
|
||||
}
|
||||
|
||||
public void setTargetResistance(int resistance) {
|
||||
this.mTargetResistance = resistance;
|
||||
}
|
||||
|
||||
public void setCurrentResistance(int resistance) {
|
||||
this.mCurrentResistance = resistance;
|
||||
}
|
||||
|
||||
private BikeData(Parcel parcel) {
|
||||
readFromParcel(parcel);
|
||||
}
|
||||
|
||||
@Override // android.os.Parcelable
|
||||
public void writeToParcel(Parcel parcel, int i) {
|
||||
parcel.writeLong(this.mRPM);
|
||||
parcel.writeLong(this.mPower);
|
||||
parcel.writeLong(this.mStepperMotorPosition);
|
||||
parcel.writeLong(this.mLoadCellReading);
|
||||
parcel.writeInt(this.mCurrentResistance);
|
||||
parcel.writeInt(this.mTargetResistance);
|
||||
parcel.writeString(this.mFWVersionNumber);
|
||||
parcel.writeByteArray(this.mPacketData);
|
||||
parcel.writeString(this.mPacketTime);
|
||||
parcel.writeInt(this.mStepperMotorStartPosition);
|
||||
parcel.writeInt(this.mStepperMotorEndPosition);
|
||||
parcel.writeInt(this.mCalibrationState);
|
||||
parcel.writeInt(this.mEncoderAngle);
|
||||
parcel.writeInt(this.mSystemState);
|
||||
parcel.writeInt(this.mErrorIndex);
|
||||
parcel.writeInt(this.mError1Code);
|
||||
parcel.writeString(this.mError1Time);
|
||||
parcel.writeInt(this.mError2Code);
|
||||
parcel.writeString(this.mError2Time);
|
||||
parcel.writeInt(this.mError3Code);
|
||||
parcel.writeString(this.mError3Time);
|
||||
parcel.writeInt(this.mError4Code);
|
||||
parcel.writeString(this.mError4Time);
|
||||
parcel.writeInt(this.mError5Code);
|
||||
parcel.writeString(this.mError5Time);
|
||||
parcel.writeIntArray(this.mErrorMap);
|
||||
parcel.writeString(this.mLoadCellTable);
|
||||
parcel.writeInt(this.mLoadCellTableCrc);
|
||||
parcel.writeString(this.mPSerial);
|
||||
parcel.writeString(this.mQSerial);
|
||||
parcel.writeString(this.mBikeFrameSerial);
|
||||
parcel.writeString(this.mLoadCellSerial);
|
||||
parcel.writeFloat(this.mLoadCellOffset);
|
||||
parcel.writeInt(this.mDataWriteCycle);
|
||||
parcel.writeString(this.mDataWriteDate);
|
||||
parcel.writeString(this.mDataWriteTime);
|
||||
parcel.writeInt(this.mLoadCellZeroData);
|
||||
parcel.writeInt(this.mLoadCellCalSpan);
|
||||
parcel.writeInt(this.mLoadCellTempCount);
|
||||
parcel.writeFloat(this.mResistanceOffset);
|
||||
parcel.writeInt(this.mPositionOffset);
|
||||
parcel.writeInt(this.mLoadCellTableStatus);
|
||||
parcel.writeFloat(this.mV1Resistance);
|
||||
parcel.writeString(this.mLoadCellVersion);
|
||||
parcel.writeInt(this.mAppliedPositionOffset);
|
||||
parcel.writeInt(this.mStallThreshold);
|
||||
parcel.writeString(this.mHardwareVersion);
|
||||
parcel.writeInt(this.mADValue);
|
||||
parcel.writeInt(this.mPowerZoneAutoFollowEnabled);
|
||||
parcel.writeInt(this.mPowerZoneAutoFollowPowerSetPoint);
|
||||
parcel.writeFloat(this.mPowerZoneAutoFollowTargetResistance);
|
||||
parcel.writeInt(this.mPowerZoneAutoFollowStatus);
|
||||
parcel.writeInt(this.mPZAFRampUpRate);
|
||||
parcel.writeInt(this.mPZAFRampDownRate);
|
||||
parcel.writeInt(this.mPZAFMaxResistanceSetPoint);
|
||||
parcel.writeInt(this.mPZAFMinUpdateRPM);
|
||||
}
|
||||
|
||||
private void readFromParcel(Parcel parcel) {
|
||||
this.mRPM = parcel.readLong();
|
||||
this.mPower = parcel.readLong();
|
||||
this.mStepperMotorPosition = parcel.readLong();
|
||||
this.mLoadCellReading = parcel.readLong();
|
||||
this.mCurrentResistance = parcel.readInt();
|
||||
this.mTargetResistance = parcel.readInt();
|
||||
this.mFWVersionNumber = parcel.readString();
|
||||
this.mPacketData = parcel.createByteArray();
|
||||
this.mPacketTime = parcel.readString();
|
||||
this.mStepperMotorStartPosition = parcel.readInt();
|
||||
this.mStepperMotorEndPosition = parcel.readInt();
|
||||
this.mCalibrationState = parcel.readInt();
|
||||
this.mEncoderAngle = parcel.readInt();
|
||||
this.mSystemState = parcel.readInt();
|
||||
this.mErrorIndex = parcel.readInt();
|
||||
this.mError1Code = parcel.readInt();
|
||||
this.mError1Time = parcel.readString();
|
||||
this.mError2Code = parcel.readInt();
|
||||
this.mError2Time = parcel.readString();
|
||||
this.mError3Code = parcel.readInt();
|
||||
this.mError3Time = parcel.readString();
|
||||
this.mError4Code = parcel.readInt();
|
||||
this.mError4Time = parcel.readString();
|
||||
this.mError5Code = parcel.readInt();
|
||||
this.mError5Time = parcel.readString();
|
||||
int[] iArr = new int[15];
|
||||
this.mErrorMap = iArr;
|
||||
parcel.readIntArray(iArr);
|
||||
this.mLoadCellTable = parcel.readString();
|
||||
this.mLoadCellTableCrc = parcel.readInt();
|
||||
this.mPSerial = parcel.readString();
|
||||
this.mQSerial = parcel.readString();
|
||||
this.mBikeFrameSerial = parcel.readString();
|
||||
this.mLoadCellSerial = parcel.readString();
|
||||
this.mLoadCellOffset = parcel.readFloat();
|
||||
this.mDataWriteCycle = parcel.readInt();
|
||||
this.mDataWriteDate = parcel.readString();
|
||||
this.mDataWriteTime = parcel.readString();
|
||||
this.mLoadCellZeroData = parcel.readInt();
|
||||
this.mLoadCellCalSpan = parcel.readInt();
|
||||
this.mLoadCellTempCount = parcel.readInt();
|
||||
this.mResistanceOffset = parcel.readFloat();
|
||||
this.mPositionOffset = parcel.readInt();
|
||||
this.mLoadCellTableStatus = parcel.readInt();
|
||||
this.mV1Resistance = parcel.readFloat();
|
||||
this.mLoadCellVersion = parcel.readString();
|
||||
this.mAppliedPositionOffset = parcel.readInt();
|
||||
this.mStallThreshold = parcel.readInt();
|
||||
this.mHardwareVersion = parcel.readString();
|
||||
this.mADValue = parcel.readInt();
|
||||
this.mPowerZoneAutoFollowEnabled = parcel.readInt();
|
||||
this.mPowerZoneAutoFollowPowerSetPoint = parcel.readInt();
|
||||
this.mPowerZoneAutoFollowTargetResistance = parcel.readFloat();
|
||||
this.mPowerZoneAutoFollowStatus = parcel.readInt();
|
||||
this.mPZAFRampUpRate = parcel.readInt();
|
||||
this.mPZAFRampDownRate = parcel.readInt();
|
||||
this.mPZAFMaxResistanceSetPoint = parcel.readInt();
|
||||
this.mPZAFMinUpdateRPM = parcel.readInt();
|
||||
}
|
||||
}
|
||||
863
src/android/src/GrpcTreadmillService.java
Normal file
863
src/android/src/GrpcTreadmillService.java
Normal file
@@ -0,0 +1,863 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.okhttp.OkHttpChannelBuilder;
|
||||
import io.grpc.stub.MetadataUtils;
|
||||
|
||||
import com.ifit.glassos.util.Empty;
|
||||
import com.ifit.glassos.workout.SpeedMetric;
|
||||
import com.ifit.glassos.workout.SpeedServiceGrpc;
|
||||
import com.ifit.glassos.workout.SpeedRequest;
|
||||
import com.ifit.glassos.workout.InclineMetric;
|
||||
import com.ifit.glassos.workout.InclineServiceGrpc;
|
||||
import com.ifit.glassos.workout.InclineRequest;
|
||||
import com.ifit.glassos.workout.WattsMetric;
|
||||
import com.ifit.glassos.workout.WattsServiceGrpc;
|
||||
import com.ifit.glassos.console.constantwatts.ConstantWattsMessage;
|
||||
import com.ifit.glassos.console.constantwatts.ConstantWattsServiceGrpc;
|
||||
import com.ifit.glassos.workout.ResistanceMetric;
|
||||
import com.ifit.glassos.workout.ResistanceServiceGrpc;
|
||||
import com.ifit.glassos.workout.ResistanceRequest;
|
||||
import com.ifit.glassos.workout.CadenceMetric;
|
||||
import com.ifit.glassos.workout.CadenceServiceGrpc;
|
||||
import com.ifit.glassos.workout.RpmMetric;
|
||||
import com.ifit.glassos.workout.RpmServiceGrpc;
|
||||
import com.ifit.glassos.settings.FanState;
|
||||
import com.ifit.glassos.settings.FanStateMessage;
|
||||
import com.ifit.glassos.settings.FanStateServiceGrpc;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class GrpcTreadmillService {
|
||||
|
||||
private static final String TAG = "GrpcTreadmillService";
|
||||
|
||||
// Singleton instance for static access
|
||||
private static GrpcTreadmillService instance = null;
|
||||
private static Context staticContext = null;
|
||||
private static String serverHost = "localhost";
|
||||
private static final int SERVER_PORT = 54321;
|
||||
private static final int UPDATE_INTERVAL_MS = 500;
|
||||
|
||||
// Threading components
|
||||
private Handler mainHandler;
|
||||
private ExecutorService executorService;
|
||||
private Runnable metricsUpdateRunnable;
|
||||
|
||||
// gRPC components
|
||||
private ManagedChannel channel;
|
||||
private SpeedServiceGrpc.SpeedServiceBlockingStub speedStub;
|
||||
private InclineServiceGrpc.InclineServiceBlockingStub inclineStub;
|
||||
private WattsServiceGrpc.WattsServiceBlockingStub wattsStub;
|
||||
private ConstantWattsServiceGrpc.ConstantWattsServiceBlockingStub constantWattsStub;
|
||||
private ResistanceServiceGrpc.ResistanceServiceBlockingStub resistanceStub;
|
||||
private CadenceServiceGrpc.CadenceServiceBlockingStub cadenceStub;
|
||||
private RpmServiceGrpc.RpmServiceBlockingStub rpmStub;
|
||||
private FanStateServiceGrpc.FanStateServiceBlockingStub fanStub;
|
||||
|
||||
// Control flags and current values
|
||||
private volatile boolean isUpdating = false;
|
||||
private volatile double currentSpeed = 0.0;
|
||||
private volatile double currentIncline = 0.0;
|
||||
private volatile double currentResistance = 0.0;
|
||||
private volatile double currentWatts = 0.0;
|
||||
private volatile double currentCadence = 0.0;
|
||||
private volatile double currentRpm = 0.0;
|
||||
private volatile int currentFanSpeed = 0;
|
||||
|
||||
// Context for accessing assets
|
||||
private Context context;
|
||||
|
||||
// Metrics listener interface
|
||||
public interface MetricsListener {
|
||||
void onSpeedUpdated(double speed);
|
||||
void onInclineUpdated(double incline);
|
||||
void onWattsUpdated(double watts);
|
||||
void onResistanceUpdated(double resistance);
|
||||
void onCadenceUpdated(double cadence);
|
||||
void onRpmUpdated(double rpm);
|
||||
void onFanSpeedUpdated(int fanSpeed);
|
||||
void onError(String metric, String error);
|
||||
}
|
||||
|
||||
private MetricsListener metricsListener;
|
||||
|
||||
public GrpcTreadmillService(Context context) {
|
||||
this.context = context;
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
public void setMetricsListener(MetricsListener listener) {
|
||||
this.metricsListener = listener;
|
||||
}
|
||||
|
||||
private void initializeInstance() throws Exception {
|
||||
initializeGrpcConnection();
|
||||
}
|
||||
|
||||
private void startMetricsUpdatesInstance() {
|
||||
if (isUpdating) return;
|
||||
|
||||
isUpdating = true;
|
||||
|
||||
metricsUpdateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isUpdating) return;
|
||||
|
||||
executorService.execute(() -> {
|
||||
fetchAllMetricsFromServer();
|
||||
|
||||
if (isUpdating) {
|
||||
mainHandler.postDelayed(metricsUpdateRunnable, UPDATE_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mainHandler.post(metricsUpdateRunnable);
|
||||
QLog.i(TAG, "Started periodic metrics updates");
|
||||
}
|
||||
|
||||
private void stopMetricsUpdatesInstance() {
|
||||
isUpdating = false;
|
||||
|
||||
if (metricsUpdateRunnable != null) {
|
||||
mainHandler.removeCallbacks(metricsUpdateRunnable);
|
||||
}
|
||||
|
||||
QLog.i(TAG, "Stopped periodic metrics updates");
|
||||
}
|
||||
|
||||
private void adjustSpeedInstance(double delta) {
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
double newSpeed = Math.max(0.0, currentSpeed + delta);
|
||||
|
||||
Metadata headers = createHeaders();
|
||||
SpeedServiceGrpc.SpeedServiceBlockingStub stubWithHeaders = speedStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
|
||||
SpeedRequest request = SpeedRequest.newBuilder().setKph(newSpeed).build();
|
||||
stubWithHeaders.setSpeed(request);
|
||||
|
||||
QLog.d(TAG, String.format("Set speed to %.1f km/h", newSpeed));
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to set speed", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("speed", e.getMessage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void adjustInclineInstance(double delta) {
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
double newIncline = Math.max(-50.0, currentIncline + delta);
|
||||
|
||||
Metadata headers = createHeaders();
|
||||
InclineServiceGrpc.InclineServiceBlockingStub stubWithHeaders = inclineStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
|
||||
InclineRequest request = InclineRequest.newBuilder().setPercent(newIncline).build();
|
||||
stubWithHeaders.setIncline(request);
|
||||
|
||||
QLog.d(TAG, String.format("Set incline to %.1f%%", newIncline));
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to set incline", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("incline", e.getMessage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void adjustResistanceInstance(double delta) {
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
double newResistance = Math.max(0.0, currentResistance + delta);
|
||||
|
||||
Metadata headers = createHeaders();
|
||||
ResistanceServiceGrpc.ResistanceServiceBlockingStub stubWithHeaders = resistanceStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
|
||||
ResistanceRequest request = ResistanceRequest.newBuilder().setResistance(newResistance).build();
|
||||
stubWithHeaders.setResistance(request);
|
||||
|
||||
QLog.d(TAG, String.format("Set resistance to %.0f level", newResistance));
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to set resistance", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("resistance", e.getMessage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setWattsInstance(double watts) {
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
Metadata headers = createHeaders();
|
||||
ConstantWattsServiceGrpc.ConstantWattsServiceBlockingStub stubWithHeaders = constantWattsStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
|
||||
if (watts <= 0) {
|
||||
// Disable constant watts mode when watts is 0 or negative
|
||||
stubWithHeaders.disable(Empty.newBuilder().build());
|
||||
QLog.d(TAG, "Disabled constant watts mode");
|
||||
} else {
|
||||
// Set target watts
|
||||
int targetWatts = (int) watts;
|
||||
ConstantWattsMessage request = ConstantWattsMessage.newBuilder().setWatts(targetWatts).build();
|
||||
stubWithHeaders.setConstantWatts(request);
|
||||
QLog.d(TAG, String.format("Set constant watts to %d", targetWatts));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to set watts", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("watts", e.getMessage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void disableConstantWattsInstance() {
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
Metadata headers = createHeaders();
|
||||
ConstantWattsServiceGrpc.ConstantWattsServiceBlockingStub stubWithHeaders = constantWattsStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
|
||||
stubWithHeaders.disable(Empty.newBuilder().build());
|
||||
QLog.d(TAG, "Explicitly disabled constant watts mode");
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to disable constant watts", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("watts", e.getMessage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setFanSpeedInstance(int fanSpeed) {
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
Metadata headers = createHeaders();
|
||||
FanStateServiceGrpc.FanStateServiceBlockingStub stubWithHeaders = fanStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
|
||||
FanState fanState;
|
||||
switch (fanSpeed) {
|
||||
case 0:
|
||||
fanState = FanState.FAN_STATE_OFF;
|
||||
break;
|
||||
case 1:
|
||||
fanState = FanState.FAN_STATE_LOW;
|
||||
break;
|
||||
case 2:
|
||||
fanState = FanState.FAN_STATE_MEDIUM;
|
||||
break;
|
||||
case 3:
|
||||
fanState = FanState.FAN_STATE_HIGH;
|
||||
break;
|
||||
case 4:
|
||||
fanState = FanState.FAN_STATE_AUTO;
|
||||
break;
|
||||
default:
|
||||
fanState = FanState.FAN_STATE_OFF;
|
||||
break;
|
||||
}
|
||||
|
||||
FanStateMessage request = FanStateMessage.newBuilder().setState(fanState).build();
|
||||
stubWithHeaders.setFanState(request);
|
||||
|
||||
QLog.d(TAG, String.format("Set fan speed to %d (%s)", fanSpeed, fanState.name()));
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to set fan speed", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("fan", e.getMessage()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void shutdownInstance() {
|
||||
stopMetricsUpdates();
|
||||
|
||||
if (channel != null) {
|
||||
try {
|
||||
channel.shutdown();
|
||||
if (!channel.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
channel.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e(TAG, "Error shutting down gRPC channel", e);
|
||||
channel.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e(TAG, "Error shutting down executor service", e);
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGrpcConnection() throws Exception {
|
||||
AssetManager assets = context.getAssets();
|
||||
|
||||
String[] requiredFiles = {"client_cert.pem", "client_key.pem"};
|
||||
for (String file : requiredFiles) {
|
||||
try {
|
||||
assets.open(file).close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Required certificate file missing: " + file +
|
||||
". Please add it to app/src/main/assets/");
|
||||
}
|
||||
}
|
||||
|
||||
InputStream caCertStream = null;
|
||||
try {
|
||||
caCertStream = assets.open("ca_cert.pem");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "ca_cert.pem not found, continuing with insecure mode");
|
||||
}
|
||||
InputStream clientCertStream = assets.open("client_cert.pem");
|
||||
InputStream clientKeyStream = assets.open("client_key.pem");
|
||||
|
||||
QLog.i(TAG, "Loading TLS certificates (insecure server validation mode)...");
|
||||
|
||||
SSLContext sslContext = createSSLContext(caCertStream, clientCertStream, clientKeyStream);
|
||||
|
||||
channel = OkHttpChannelBuilder.forAddress(serverHost, SERVER_PORT)
|
||||
.sslSocketFactory(sslContext.getSocketFactory())
|
||||
.build();
|
||||
|
||||
if (caCertStream != null) caCertStream.close();
|
||||
clientCertStream.close();
|
||||
clientKeyStream.close();
|
||||
|
||||
speedStub = SpeedServiceGrpc.newBlockingStub(channel);
|
||||
inclineStub = InclineServiceGrpc.newBlockingStub(channel);
|
||||
wattsStub = WattsServiceGrpc.newBlockingStub(channel);
|
||||
constantWattsStub = ConstantWattsServiceGrpc.newBlockingStub(channel);
|
||||
resistanceStub = ResistanceServiceGrpc.newBlockingStub(channel);
|
||||
cadenceStub = CadenceServiceGrpc.newBlockingStub(channel);
|
||||
rpmStub = RpmServiceGrpc.newBlockingStub(channel);
|
||||
fanStub = FanStateServiceGrpc.newBlockingStub(channel);
|
||||
|
||||
QLog.i(TAG, "gRPC connection initialized with client certificates");
|
||||
}
|
||||
|
||||
private SSLContext createSSLContext(InputStream caCertStream, InputStream clientCertStream,
|
||||
InputStream clientKeyStream) throws Exception {
|
||||
|
||||
QLog.d(TAG, "Creating SSL context with client certificates (insecure server validation)...");
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate clientCert = (X509Certificate) cf.generateCertificate(clientCertStream);
|
||||
QLog.d(TAG, "Loaded client certificate: " + clientCert.getSubjectDN());
|
||||
|
||||
byte[] keyData = readAllBytesCompat(clientKeyStream);
|
||||
String keyString = new String(keyData, StandardCharsets.UTF_8);
|
||||
keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
|
||||
byte[] keyBytes = Base64.getDecoder().decode(keyString);
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
|
||||
QLog.d(TAG, "Loaded private key");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null, null);
|
||||
keyStore.setKeyEntry("client", privateKey, "".toCharArray(), new Certificate[]{clientCert});
|
||||
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(keyStore, "".toCharArray());
|
||||
|
||||
javax.net.ssl.TrustManager[] insecureTrustManagers = new javax.net.ssl.TrustManager[] {
|
||||
new javax.net.ssl.X509TrustManager() {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
||||
QLog.d(TAG, "Accepting server certificate without validation (insecure mode)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(kmf.getKeyManagers(), insecureTrustManagers, new SecureRandom());
|
||||
|
||||
QLog.i(TAG, "SSL context created with client authentication but insecure server validation");
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
private byte[] readAllBytesCompat(InputStream inputStream) throws Exception {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
|
||||
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private Metadata createHeaders() {
|
||||
Metadata headers = new Metadata();
|
||||
headers.put(Metadata.Key.of("client_id", Metadata.ASCII_STRING_MARSHALLER),
|
||||
"com.ifit.eriador");
|
||||
return headers;
|
||||
}
|
||||
|
||||
private void fetchAllMetricsFromServer() {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
QLog.d(TAG, "Making gRPC calls for all metrics...");
|
||||
|
||||
long headersStartTime = System.currentTimeMillis();
|
||||
Metadata headers = createHeaders();
|
||||
Empty request = Empty.newBuilder().build();
|
||||
long headersEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Headers creation took: " + (headersEndTime - headersStartTime) + "ms");
|
||||
|
||||
// Fetch speed
|
||||
try {
|
||||
long speedStartTime = System.currentTimeMillis();
|
||||
SpeedServiceGrpc.SpeedServiceBlockingStub speedStubWithHeaders = speedStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long speedInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Speed interceptor setup took: " + (speedInterceptorTime - speedStartTime) + "ms");
|
||||
|
||||
SpeedMetric speedResponse = speedStubWithHeaders.getSpeed(request);
|
||||
long speedCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Speed gRPC call took: " + (speedCallTime - speedInterceptorTime) + "ms");
|
||||
|
||||
currentSpeed = speedResponse.getLastKph();
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onSpeedUpdated(currentSpeed));
|
||||
}
|
||||
long speedEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Speed total processing took: " + (speedEndTime - speedStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch speed", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("speed", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch inclination
|
||||
try {
|
||||
long inclineStartTime = System.currentTimeMillis();
|
||||
InclineServiceGrpc.InclineServiceBlockingStub inclineStubWithHeaders = inclineStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long inclineInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Incline interceptor setup took: " + (inclineInterceptorTime - inclineStartTime) + "ms");
|
||||
|
||||
InclineMetric inclineResponse = inclineStubWithHeaders.getIncline(request);
|
||||
long inclineCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Incline gRPC call took: " + (inclineCallTime - inclineInterceptorTime) + "ms");
|
||||
|
||||
currentIncline = inclineResponse.getLastInclinePercent();
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onInclineUpdated(currentIncline));
|
||||
}
|
||||
long inclineEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Incline total processing took: " + (inclineEndTime - inclineStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch inclination", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("inclination", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch watts
|
||||
try {
|
||||
long wattsStartTime = System.currentTimeMillis();
|
||||
WattsServiceGrpc.WattsServiceBlockingStub wattsStubWithHeaders = wattsStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long wattsInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Watts interceptor setup took: " + (wattsInterceptorTime - wattsStartTime) + "ms");
|
||||
|
||||
WattsMetric wattsResponse = wattsStubWithHeaders.getWatts(request);
|
||||
long wattsCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Watts gRPC call took: " + (wattsCallTime - wattsInterceptorTime) + "ms");
|
||||
|
||||
currentWatts = wattsResponse.getLastWatts();
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onWattsUpdated(currentWatts));
|
||||
}
|
||||
long wattsEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Watts total processing took: " + (wattsEndTime - wattsStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch watts", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("watts", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch resistance
|
||||
try {
|
||||
long resistanceStartTime = System.currentTimeMillis();
|
||||
ResistanceServiceGrpc.ResistanceServiceBlockingStub resistanceStubWithHeaders = resistanceStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long resistanceInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Resistance interceptor setup took: " + (resistanceInterceptorTime - resistanceStartTime) + "ms");
|
||||
|
||||
ResistanceMetric resistanceResponse = resistanceStubWithHeaders.getResistance(request);
|
||||
long resistanceCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Resistance gRPC call took: " + (resistanceCallTime - resistanceInterceptorTime) + "ms");
|
||||
|
||||
currentResistance = resistanceResponse.getLastResistance();
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onResistanceUpdated(currentResistance));
|
||||
}
|
||||
long resistanceEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Resistance total processing took: " + (resistanceEndTime - resistanceStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch resistance", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("resistance", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch RPM (for bikes)
|
||||
try {
|
||||
long rpmStartTime = System.currentTimeMillis();
|
||||
RpmServiceGrpc.RpmServiceBlockingStub rpmStubWithHeaders = rpmStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long rpmInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "RPM interceptor setup took: " + (rpmInterceptorTime - rpmStartTime) + "ms");
|
||||
|
||||
RpmMetric rpmResponse = rpmStubWithHeaders.getRpm(request);
|
||||
long rpmCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "RPM gRPC call took: " + (rpmCallTime - rpmInterceptorTime) + "ms");
|
||||
|
||||
currentRpm = rpmResponse.getLastRpm();
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onRpmUpdated(currentRpm));
|
||||
}
|
||||
long rpmEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "RPM total processing took: " + (rpmEndTime - rpmStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch RPM", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("rpm", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch cadence (for treadmills)
|
||||
try {
|
||||
long cadenceStartTime = System.currentTimeMillis();
|
||||
CadenceServiceGrpc.CadenceServiceBlockingStub cadenceStubWithHeaders = cadenceStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long cadenceInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Cadence interceptor setup took: " + (cadenceInterceptorTime - cadenceStartTime) + "ms");
|
||||
|
||||
CadenceMetric cadenceResponse = cadenceStubWithHeaders.getCadence(request);
|
||||
long cadenceCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Cadence gRPC call took: " + (cadenceCallTime - cadenceInterceptorTime) + "ms");
|
||||
|
||||
currentCadence = cadenceResponse.getLastStepsPerMinute();
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onCadenceUpdated(currentCadence));
|
||||
}
|
||||
long cadenceEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Cadence total processing took: " + (cadenceEndTime - cadenceStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch cadence", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("cadence", "Error"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fan speed
|
||||
/*
|
||||
try {
|
||||
long fanStartTime = System.currentTimeMillis();
|
||||
FanStateServiceGrpc.FanStateServiceBlockingStub fanStubWithHeaders = fanStub.withInterceptors(
|
||||
MetadataUtils.newAttachHeadersInterceptor(headers)
|
||||
);
|
||||
long fanInterceptorTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Fan interceptor setup took: " + (fanInterceptorTime - fanStartTime) + "ms");
|
||||
|
||||
FanStateMessage fanResponse = fanStubWithHeaders.getFanState(request);
|
||||
long fanCallTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Fan gRPC call took: " + (fanCallTime - fanInterceptorTime) + "ms");
|
||||
|
||||
int fanSpeed;
|
||||
switch (fanResponse.getState()) {
|
||||
case FAN_STATE_OFF:
|
||||
fanSpeed = 0;
|
||||
break;
|
||||
case FAN_STATE_LOW:
|
||||
fanSpeed = 1;
|
||||
break;
|
||||
case FAN_STATE_MEDIUM:
|
||||
fanSpeed = 2;
|
||||
break;
|
||||
case FAN_STATE_HIGH:
|
||||
fanSpeed = 3;
|
||||
break;
|
||||
case FAN_STATE_AUTO:
|
||||
fanSpeed = 4;
|
||||
break;
|
||||
default:
|
||||
fanSpeed = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
currentFanSpeed = fanSpeed;
|
||||
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onFanSpeedUpdated(currentFanSpeed));
|
||||
}
|
||||
long fanEndTime = System.currentTimeMillis();
|
||||
QLog.d(TAG, "Fan total processing took: " + (fanEndTime - fanStartTime) + "ms");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Failed to fetch fan speed", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> metricsListener.onError("fan", "Error"));
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
long totalEndTime = System.currentTimeMillis();
|
||||
long totalTime = totalEndTime - startTime;
|
||||
QLog.d(TAG, "=== TIMING SUMMARY ===");
|
||||
QLog.d(TAG, "Total fetchAllMetricsFromServer execution time: " + totalTime + "ms");
|
||||
QLog.d(TAG, "Completed all metrics fetch");
|
||||
|
||||
} catch (Exception e) {
|
||||
long totalEndTime = System.currentTimeMillis();
|
||||
long totalTime = totalEndTime - startTime;
|
||||
QLog.e(TAG, "Failed to fetch metrics after " + totalTime + "ms", e);
|
||||
if (metricsListener != null) {
|
||||
mainHandler.post(() -> {
|
||||
metricsListener.onError("speed", "Error");
|
||||
metricsListener.onError("inclination", "Error");
|
||||
metricsListener.onError("watts", "Error");
|
||||
metricsListener.onError("resistance", "Error");
|
||||
metricsListener.onError("cadence", "Error");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static wrapper methods for JNI calls
|
||||
public static void initialize() {
|
||||
initialize("localhost");
|
||||
}
|
||||
|
||||
public static void initialize(String host) {
|
||||
try {
|
||||
if (staticContext == null) {
|
||||
QLog.e(TAG, "Context not set. Call setContext() first.");
|
||||
return;
|
||||
}
|
||||
|
||||
serverHost = host;
|
||||
|
||||
if (instance == null) {
|
||||
instance = new GrpcTreadmillService(staticContext);
|
||||
}
|
||||
|
||||
instance.initializeInstance();
|
||||
QLog.i(TAG, "Static initialize completed with host: " + host);
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Static initialize failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
staticContext = context;
|
||||
}
|
||||
|
||||
public static void startMetricsUpdates() {
|
||||
if (instance != null) {
|
||||
instance.startMetricsUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMetricsUpdates() {
|
||||
if (instance != null) {
|
||||
instance.stopMetricsUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static double getCurrentSpeed() {
|
||||
if (instance != null) {
|
||||
return instance.currentSpeed;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static double getCurrentIncline() {
|
||||
if (instance != null) {
|
||||
return instance.currentIncline;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static double getCurrentWatts() {
|
||||
if (instance != null) {
|
||||
return instance.currentWatts;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static double getCurrentCadence() {
|
||||
if (instance != null) {
|
||||
return instance.currentCadence;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static double getCurrentRpm() {
|
||||
if (instance != null) {
|
||||
return instance.currentRpm;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static int getCurrentFanSpeed() {
|
||||
if (instance != null) {
|
||||
return instance.currentFanSpeed;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static double getCurrentResistance() {
|
||||
if (instance != null) {
|
||||
return instance.currentResistance;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static void adjustSpeed(double delta) {
|
||||
if (instance != null) {
|
||||
instance.adjustSpeedInstance(delta);
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void adjustIncline(double delta) {
|
||||
if (instance != null) {
|
||||
instance.adjustInclineInstance(delta);
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void adjustResistance(double delta) {
|
||||
if (instance != null) {
|
||||
instance.adjustResistanceInstance(delta);
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void setWatts(double watts) {
|
||||
if (instance != null) {
|
||||
instance.setWattsInstance(watts);
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void disableConstantWatts() {
|
||||
if (instance != null) {
|
||||
instance.disableConstantWattsInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void setFanSpeed(int fanSpeed) {
|
||||
if (instance != null) {
|
||||
instance.setFanSpeedInstance(fanSpeed);
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (instance != null) {
|
||||
instance.shutdownInstance();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
295
src/android/src/PelotonCallbackSensor.java
Normal file
295
src/android/src/PelotonCallbackSensor.java
Normal file
@@ -0,0 +1,295 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
/**
|
||||
* Callback-based sensor implementation from Grupetto v1 develop
|
||||
* Based on: https://github.com/selalipop/grupetto/pull/10
|
||||
* More efficient than polling - receives data only when it changes
|
||||
*/
|
||||
public abstract class PelotonCallbackSensor {
|
||||
|
||||
private static final String TAG = "PelotonCallbackSensor";
|
||||
|
||||
// Transaction codes from Grupetto v1 CallbackSensor.kt
|
||||
private static final int TRANSACTION_REGISTER_CALLBACK = 1;
|
||||
private static final int TRANSACTION_UNREGISTER_CALLBACK = 2;
|
||||
|
||||
// Interface descriptors from Grupetto v1
|
||||
private static final String IV1_INTERFACE = "com.onepeloton.affernetservice.IV1Interface";
|
||||
private static final String IV1_CALLBACK_INTERFACE = "com.onepeloton.affernetservice.IV1Callback";
|
||||
|
||||
private IBinder binder;
|
||||
private boolean isRegistered = false;
|
||||
private PelotonCallbackBinder callbackBinder;
|
||||
|
||||
// Callback interface for receiving sensor data
|
||||
public interface SensorDataCallback {
|
||||
void onSensorDataReceived(float value);
|
||||
void onSensorError(long errorCode);
|
||||
}
|
||||
|
||||
private SensorDataCallback callback;
|
||||
|
||||
public PelotonCallbackSensor(IBinder binder) {
|
||||
this.binder = binder;
|
||||
this.callbackBinder = new PelotonCallbackBinder();
|
||||
}
|
||||
|
||||
public void setCallback(SensorDataCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void start() throws RemoteException {
|
||||
if (isRegistered) {
|
||||
QLog.w(TAG, "Sensor already started");
|
||||
return;
|
||||
}
|
||||
|
||||
registerCallback();
|
||||
isRegistered = true;
|
||||
QLog.d(TAG, "Callback sensor started successfully");
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!isRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
unregisterCallback();
|
||||
isRegistered = false;
|
||||
QLog.d(TAG, "Callback sensor stopped successfully");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to stop callback sensor", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerCallback() throws RemoteException {
|
||||
Parcel data = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
|
||||
try {
|
||||
data.writeInterfaceToken(IV1_INTERFACE);
|
||||
data.writeStrongBinder(callbackBinder);
|
||||
data.writeString("QDomyos-Zwift"); // Identifier like Grupetto
|
||||
|
||||
QLog.d(TAG, "Registering callback with interface: " + IV1_INTERFACE);
|
||||
boolean success = binder.transact(TRANSACTION_REGISTER_CALLBACK, data, reply, 0);
|
||||
if (success) {
|
||||
reply.readException();
|
||||
QLog.i(TAG, "Successfully registered callback");
|
||||
} else {
|
||||
throw new RemoteException("Failed to register callback");
|
||||
}
|
||||
} finally {
|
||||
data.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterCallback() throws RemoteException {
|
||||
Parcel data = Parcel.obtain();
|
||||
Parcel reply = Parcel.obtain();
|
||||
|
||||
try {
|
||||
data.writeInterfaceToken(IV1_INTERFACE);
|
||||
data.writeStrongBinder(callbackBinder);
|
||||
data.writeString("QDomyos-Zwift"); // Identifier like Grupetto
|
||||
|
||||
boolean success = binder.transact(TRANSACTION_UNREGISTER_CALLBACK, data, reply, 0);
|
||||
if (success) {
|
||||
reply.readException();
|
||||
QLog.d(TAG, "Successfully unregistered callback");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Error unregistering callback", e);
|
||||
} finally {
|
||||
data.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the specific sensor value from BikeData
|
||||
* Override in subclasses for different sensor types
|
||||
*/
|
||||
protected abstract float extractValue(BikeData bikeData);
|
||||
|
||||
/**
|
||||
* Apply sensor-specific value mapping
|
||||
* Override in subclasses if needed
|
||||
*/
|
||||
protected float mapValue(float rawValue) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binder implementation for receiving callbacks from Peloton service
|
||||
*/
|
||||
private class PelotonCallbackBinder extends android.os.Binder {
|
||||
|
||||
@Override
|
||||
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
|
||||
QLog.d(TAG, "Callback onTransact called with code: " + code);
|
||||
|
||||
switch (code) {
|
||||
case 1: // onSensorDataChange
|
||||
try {
|
||||
data.enforceInterface(IV1_CALLBACK_INTERFACE);
|
||||
QLog.d(TAG, "Interface enforced successfully");
|
||||
|
||||
int hasData = data.readInt();
|
||||
QLog.d(TAG, "Has data flag: " + hasData);
|
||||
|
||||
if (hasData != 0) {
|
||||
QLog.d(TAG, "Creating BikeData from parcel");
|
||||
BikeData bikeData = BikeData.CREATOR.createFromParcel(data);
|
||||
|
||||
float rawValue = extractValue(bikeData);
|
||||
float mappedValue = mapValue(rawValue);
|
||||
|
||||
if (callback != null) {
|
||||
callback.onSensorDataReceived(mappedValue);
|
||||
}
|
||||
|
||||
QLog.i(TAG, "Received sensor data: " + mappedValue);
|
||||
} else {
|
||||
QLog.d(TAG, "No bike data received");
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error processing sensor data", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
case 2: // onSensorError
|
||||
try {
|
||||
data.enforceInterface(IV1_CALLBACK_INTERFACE);
|
||||
long errorCode = data.readLong();
|
||||
QLog.w(TAG, "Sensor error: " + errorCode);
|
||||
|
||||
if (callback != null) {
|
||||
callback.onSensorError(errorCode);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error processing sensor error", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
case 3: // onCalibrationStatus
|
||||
try {
|
||||
data.enforceInterface(IV1_CALLBACK_INTERFACE);
|
||||
int status = data.readInt();
|
||||
boolean success = data.readInt() != 0;
|
||||
long errorCode = data.readLong();
|
||||
QLog.d(TAG, "Calibration status: status=" + status + " success=" + success + " error=" + errorCode);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error processing calibration status", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
QLog.d(TAG, "Unknown transaction code: " + code + ", calling super");
|
||||
return super.onTransact(code, data, reply, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Power sensor implementation
|
||||
*/
|
||||
public static class PowerSensor extends PelotonCallbackSensor {
|
||||
|
||||
public PowerSensor(IBinder binder) {
|
||||
super(binder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float extractValue(BikeData bikeData) {
|
||||
return (float) bikeData.getPower();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float mapValue(float rawValue) {
|
||||
// From Grupetto v1: divide by 100 to normalize power values
|
||||
float normalizedValue = rawValue / 100.0f;
|
||||
|
||||
// Filter out spurious readings
|
||||
if (normalizedValue < 0 || normalizedValue > 1000) {
|
||||
QLog.w(TAG, "Filtering spurious power reading: " + normalizedValue);
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RPM sensor implementation
|
||||
*/
|
||||
public static class RpmSensor extends PelotonCallbackSensor {
|
||||
|
||||
public RpmSensor(IBinder binder) {
|
||||
super(binder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float extractValue(BikeData bikeData) {
|
||||
return (float) bikeData.getRPM();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resistance sensor implementation with moving window filtering
|
||||
*/
|
||||
public static class ResistanceSensor extends PelotonCallbackSensor {
|
||||
|
||||
// Moving window for resistance filtering (from Grupetto approach)
|
||||
private static final int FILTER_WINDOW_SIZE = 3;
|
||||
private float[] resistanceWindow = new float[FILTER_WINDOW_SIZE];
|
||||
private int windowIndex = 0;
|
||||
private boolean windowFilled = false;
|
||||
|
||||
public ResistanceSensor(IBinder binder) {
|
||||
super(binder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float extractValue(BikeData bikeData) {
|
||||
return (float) bikeData.getTargetResistance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float mapValue(float rawValue) {
|
||||
// Add value to moving window
|
||||
resistanceWindow[windowIndex] = rawValue;
|
||||
windowIndex = (windowIndex + 1) % FILTER_WINDOW_SIZE;
|
||||
|
||||
if (!windowFilled && windowIndex == 0) {
|
||||
windowFilled = true;
|
||||
}
|
||||
|
||||
// If window not full yet, return current value
|
||||
if (!windowFilled) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
// Return minimum value from window (Grupetto strategy for spike filtering)
|
||||
float minValue = resistanceWindow[0];
|
||||
for (int i = 1; i < FILTER_WINDOW_SIZE; i++) {
|
||||
if (resistanceWindow[i] < minValue) {
|
||||
minValue = resistanceWindow[i];
|
||||
}
|
||||
}
|
||||
|
||||
return minValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/android/src/PelotonSensorBinder.java
Normal file
106
src/android/src/PelotonSensorBinder.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
/**
|
||||
* Service binder for connecting to Peloton Grupetto v1 callback-based service
|
||||
* Based on: https://github.com/selalipop/grupetto/pull/10
|
||||
* More efficient than polling - receives data only when it changes
|
||||
*/
|
||||
public class PelotonSensorBinder {
|
||||
|
||||
private static final String TAG = "PelotonSensorBinder";
|
||||
|
||||
// Peloton service constants (from Grupetto v1 develop - callback-based)
|
||||
private static final String SERVICE_ACTION = "com.onepeloton.affernetservice.IV1Interface";
|
||||
private static final String SERVICE_PACKAGE = "com.onepeloton.affernetservice";
|
||||
private static final String SERVICE_INTENT = "com.onepeloton.affernetservice.AffernetService";
|
||||
|
||||
// Using callback-based sensors from Grupetto v1 develop
|
||||
// No transaction codes needed here - handled by PelotonCallbackSensor
|
||||
|
||||
private Context context;
|
||||
private IBinder serviceBinder = null;
|
||||
private boolean isConnected = false;
|
||||
|
||||
public PelotonSensorBinder(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously connects to the Peloton sensor service
|
||||
* Based on Grupetto's v1 Binder.kt implementation
|
||||
*/
|
||||
public CompletableFuture<IBinder> getBinder() {
|
||||
CompletableFuture<IBinder> future = new CompletableFuture<>();
|
||||
CountDownLatch connectionLatch = new CountDownLatch(1);
|
||||
|
||||
ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
QLog.i(TAG, "V1 service connected: " + name.getClassName());
|
||||
serviceBinder = service;
|
||||
isConnected = true;
|
||||
future.complete(service);
|
||||
connectionLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
QLog.w(TAG, "V1 service disconnected: " + name.getClassName());
|
||||
serviceBinder = null;
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
QLog.e(TAG, "V1 service binding died: " + name.getClassName());
|
||||
serviceBinder = null;
|
||||
isConnected = false;
|
||||
if (!future.isDone()) {
|
||||
future.completeExceptionally(new RuntimeException("V1 service binding died"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNullBinding(ComponentName name) {
|
||||
QLog.i(TAG, "V1 service null binding: " + name.getClassName());
|
||||
if (!future.isDone()) {
|
||||
future.completeExceptionally(new RuntimeException("V1 service null binding"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Intent intent = new Intent(SERVICE_INTENT);
|
||||
intent.setAction(SERVICE_ACTION);
|
||||
intent.setPackage(SERVICE_PACKAGE);
|
||||
|
||||
boolean bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
if (!bound) {
|
||||
QLog.e(TAG, "Failed to bind to Peloton V1 sensor service");
|
||||
future.completeExceptionally(new RuntimeException("Failed to bind to V1 service"));
|
||||
return future;
|
||||
}
|
||||
|
||||
QLog.i(TAG, "Binding to Peloton V1 sensor service...");
|
||||
return future;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public IBinder getServiceBinder() {
|
||||
return serviceBinder;
|
||||
}
|
||||
}
|
||||
279
src/android/src/PelotonSensorHelper.java
Normal file
279
src/android/src/PelotonSensorHelper.java
Normal file
@@ -0,0 +1,279 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import org.cagnulen.qdomyoszwift.PelotonSensorBinder;
|
||||
import org.cagnulen.qdomyoszwift.PelotonCallbackSensor;
|
||||
|
||||
/**
|
||||
* Peloton sensor helper class using callback-based approach from Grupetto v1 develop
|
||||
* Based on: https://github.com/selalipop/grupetto/pull/10
|
||||
* More efficient than polling - receives data only when it changes
|
||||
*/
|
||||
public class PelotonSensorHelper {
|
||||
|
||||
private static final String TAG = "PelotonSensorHelper";
|
||||
|
||||
// Singleton instance for static access
|
||||
private static PelotonSensorHelper instance = null;
|
||||
private static Context staticContext = null;
|
||||
|
||||
// Threading components (reduced need with callback approach)
|
||||
private Handler mainHandler;
|
||||
private ExecutorService executorService;
|
||||
|
||||
// Sensor components (callback-based from Grupetto v1)
|
||||
private PelotonSensorBinder sensorBinder;
|
||||
private PelotonCallbackSensor.PowerSensor powerSensor;
|
||||
private PelotonCallbackSensor.RpmSensor rpmSensor;
|
||||
private PelotonCallbackSensor.ResistanceSensor resistanceSensor;
|
||||
|
||||
// Control flags and current values
|
||||
private volatile boolean isInitialized = false;
|
||||
private volatile boolean isUpdating = false;
|
||||
private volatile float currentPower = 0.0f;
|
||||
private volatile float currentCadence = 0.0f;
|
||||
private volatile float currentResistance = 0.0f;
|
||||
private volatile float currentSpeed = 0.0f;
|
||||
|
||||
// Context for accessing system services
|
||||
private Context context;
|
||||
|
||||
public PelotonSensorHelper(Context context) {
|
||||
this.context = context;
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
this.sensorBinder = new PelotonSensorBinder(context);
|
||||
}
|
||||
|
||||
private void initializeInstance() throws Exception {
|
||||
QLog.i(TAG, "Initializing Peloton sensor connection...");
|
||||
|
||||
// Get binder to Peloton service (async operation)
|
||||
IBinder serviceBinder = sensorBinder.getBinder().get(10, TimeUnit.SECONDS);
|
||||
|
||||
if (serviceBinder == null) {
|
||||
throw new Exception("Failed to get service binder");
|
||||
}
|
||||
|
||||
// Initialize individual callback-based sensors
|
||||
powerSensor = new PelotonCallbackSensor.PowerSensor(serviceBinder);
|
||||
rpmSensor = new PelotonCallbackSensor.RpmSensor(serviceBinder);
|
||||
resistanceSensor = new PelotonCallbackSensor.ResistanceSensor(serviceBinder);
|
||||
|
||||
// Set up callbacks to receive sensor data
|
||||
powerSensor.setCallback(new PelotonCallbackSensor.SensorDataCallback() {
|
||||
@Override
|
||||
public void onSensorDataReceived(float value) {
|
||||
currentPower = value;
|
||||
currentSpeed = calculateSpeedFromPelotonV1Power(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorError(long errorCode) {
|
||||
QLog.w(TAG, "Power sensor error: " + errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
rpmSensor.setCallback(new PelotonCallbackSensor.SensorDataCallback() {
|
||||
@Override
|
||||
public void onSensorDataReceived(float value) {
|
||||
currentCadence = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorError(long errorCode) {
|
||||
QLog.w(TAG, "RPM sensor error: " + errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
resistanceSensor.setCallback(new PelotonCallbackSensor.SensorDataCallback() {
|
||||
@Override
|
||||
public void onSensorDataReceived(float value) {
|
||||
currentResistance = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorError(long errorCode) {
|
||||
QLog.w(TAG, "Resistance sensor error: " + errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
QLog.i(TAG, "Peloton sensor initialization completed");
|
||||
}
|
||||
|
||||
private void startSensorUpdatesInstance() {
|
||||
if (isUpdating || !isInitialized) {
|
||||
QLog.w(TAG, "Cannot start sensor updates - not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdating = true;
|
||||
|
||||
try {
|
||||
// Start callback-based sensors (no polling needed)
|
||||
if (powerSensor != null) powerSensor.start();
|
||||
if (rpmSensor != null) rpmSensor.start();
|
||||
if (resistanceSensor != null) resistanceSensor.start();
|
||||
|
||||
QLog.i(TAG, "Started callback-based sensor updates");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to start sensor updates", e);
|
||||
isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSensorUpdatesInstance() {
|
||||
isUpdating = false;
|
||||
|
||||
// Stop callback-based sensors
|
||||
if (powerSensor != null) powerSensor.stop();
|
||||
if (rpmSensor != null) rpmSensor.stop();
|
||||
if (resistanceSensor != null) resistanceSensor.stop();
|
||||
|
||||
QLog.i(TAG, "Stopped callback-based sensor updates");
|
||||
}
|
||||
|
||||
// Sensor values are now updated via callbacks - no polling needed
|
||||
|
||||
/**
|
||||
* Calculate speed from power using Peloton V1 bike formula
|
||||
* Based on Grupetto's SensorInterface.kt implementation
|
||||
*/
|
||||
private float calculateSpeedFromPelotonV1Power(float power) {
|
||||
if (power < 0.1f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Use exact formula from Grupetto Peloton.kt
|
||||
double pwrSqrt = Math.sqrt(power);
|
||||
if (power < 26f) {
|
||||
return (float)(0.057f - (0.172f * pwrSqrt) + (0.759f * Math.pow(pwrSqrt, 2)) - (0.079f * Math.pow(pwrSqrt, 3)));
|
||||
} else {
|
||||
return (float)(-1.635f + (2.325f * pwrSqrt) - (0.064f * Math.pow(pwrSqrt, 2)) + (0.001f * Math.pow(pwrSqrt, 3)));
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownInstance() {
|
||||
stopSensorUpdates();
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e(TAG, "Error shutting down executor service", e);
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up sensors
|
||||
powerSensor = null;
|
||||
rpmSensor = null;
|
||||
resistanceSensor = null;
|
||||
sensorBinder = null;
|
||||
|
||||
isInitialized = false;
|
||||
}
|
||||
|
||||
// Static wrapper methods for JNI calls
|
||||
public static void initialize() {
|
||||
try {
|
||||
if (staticContext == null) {
|
||||
QLog.e(TAG, "Context not set. Call setContext() first.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance == null) {
|
||||
instance = new PelotonSensorHelper(staticContext);
|
||||
}
|
||||
|
||||
instance.initializeInstance();
|
||||
QLog.i(TAG, "Static initialize completed");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Static initialize failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
staticContext = context;
|
||||
}
|
||||
|
||||
public static void startSensorUpdates() {
|
||||
if (instance != null) {
|
||||
instance.startSensorUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Helper not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopSensorUpdates() {
|
||||
if (instance != null) {
|
||||
instance.stopSensorUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Helper not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
// Getter methods for current sensor values
|
||||
public static float getCurrentPower() {
|
||||
if (instance != null) {
|
||||
return instance.currentPower;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentCadence() {
|
||||
if (instance != null) {
|
||||
return instance.currentCadence;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentResistance() {
|
||||
if (instance != null) {
|
||||
return instance.currentResistance;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentSpeed() {
|
||||
if (instance != null) {
|
||||
return instance.currentSpeed;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static boolean isConnected() {
|
||||
if (instance != null && instance.sensorBinder != null) {
|
||||
return instance.sensorBinder.isConnected();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isInitialized() {
|
||||
if (instance != null) {
|
||||
return instance.isInitialized;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (instance != null) {
|
||||
instance.shutdownInstance();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
286
src/android/src/PelotonSensorHelperV1.java
Normal file
286
src/android/src/PelotonSensorHelperV1.java
Normal file
@@ -0,0 +1,286 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import org.cagnulen.qdomyoszwift.PelotonSensorBinder;
|
||||
import org.cagnulen.qdomyoszwift.PelotonCallbackSensor;
|
||||
|
||||
/**
|
||||
* Peloton sensor helper class using callback-based approach from Grupetto v1 develop
|
||||
* Based on: https://github.com/selalipop/grupetto/pull/10
|
||||
* More efficient than polling - receives data only when it changes
|
||||
*/
|
||||
public class PelotonSensorHelperV1 {
|
||||
|
||||
private static final String TAG = "PelotonSensorHelperV1";
|
||||
|
||||
// Singleton instance for static access
|
||||
private static PelotonSensorHelperV1 instance = null;
|
||||
private static Context staticContext = null;
|
||||
|
||||
// Threading components (reduced need with callback approach)
|
||||
private Handler mainHandler;
|
||||
private ExecutorService executorService;
|
||||
|
||||
// Sensor components (callback-based from Grupetto v1)
|
||||
private PelotonSensorBinder sensorBinder;
|
||||
private PelotonCallbackSensor.PowerSensor powerSensor;
|
||||
private PelotonCallbackSensor.RpmSensor rpmSensor;
|
||||
private PelotonCallbackSensor.ResistanceSensor resistanceSensor;
|
||||
|
||||
// Control flags and current values
|
||||
private volatile boolean isInitialized = false;
|
||||
private volatile boolean isUpdating = false;
|
||||
private volatile float currentPower = 0.0f;
|
||||
private volatile float currentCadence = 0.0f;
|
||||
private volatile float currentResistance = 0.0f;
|
||||
private volatile float currentSpeed = 0.0f;
|
||||
|
||||
// Context for accessing system services
|
||||
private Context context;
|
||||
|
||||
public PelotonSensorHelperV1(Context context) {
|
||||
this.context = context;
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
this.sensorBinder = new PelotonSensorBinder(context);
|
||||
}
|
||||
|
||||
private void initializeInstance() throws Exception {
|
||||
QLog.i(TAG, "Initializing Peloton V1 callback sensor connection...");
|
||||
|
||||
// Get binder to Peloton service (async operation)
|
||||
IBinder serviceBinder = sensorBinder.getBinder().get(10, TimeUnit.SECONDS);
|
||||
|
||||
if (serviceBinder == null) {
|
||||
throw new Exception("Failed to get service binder");
|
||||
}
|
||||
|
||||
// Initialize individual callback-based sensors
|
||||
powerSensor = new PelotonCallbackSensor.PowerSensor(serviceBinder);
|
||||
rpmSensor = new PelotonCallbackSensor.RpmSensor(serviceBinder);
|
||||
resistanceSensor = new PelotonCallbackSensor.ResistanceSensor(serviceBinder);
|
||||
|
||||
// Set up callbacks to receive sensor data
|
||||
powerSensor.setCallback(new PelotonCallbackSensor.SensorDataCallback() {
|
||||
@Override
|
||||
public void onSensorDataReceived(float value) {
|
||||
currentPower = value;
|
||||
currentSpeed = calculateSpeedFromPelotonV1Power(value);
|
||||
QLog.d(TAG, "Power updated: " + value + "W, Speed: " + currentSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorError(long errorCode) {
|
||||
QLog.w(TAG, "Power sensor error: " + errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
rpmSensor.setCallback(new PelotonCallbackSensor.SensorDataCallback() {
|
||||
@Override
|
||||
public void onSensorDataReceived(float value) {
|
||||
currentCadence = value;
|
||||
QLog.d(TAG, "Cadence updated: " + value + " RPM");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorError(long errorCode) {
|
||||
QLog.w(TAG, "RPM sensor error: " + errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
resistanceSensor.setCallback(new PelotonCallbackSensor.SensorDataCallback() {
|
||||
@Override
|
||||
public void onSensorDataReceived(float value) {
|
||||
currentResistance = value;
|
||||
QLog.d(TAG, "Resistance updated: " + value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorError(long errorCode) {
|
||||
QLog.w(TAG, "Resistance sensor error: " + errorCode);
|
||||
}
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
QLog.i(TAG, "Peloton V1 callback sensor initialization completed");
|
||||
}
|
||||
|
||||
private void startSensorUpdatesInstance() {
|
||||
if (isUpdating || !isInitialized) {
|
||||
QLog.w(TAG, "Cannot start sensor updates - not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdating = true;
|
||||
|
||||
try {
|
||||
// Start callback-based sensors (no polling needed)
|
||||
if (powerSensor != null) powerSensor.start();
|
||||
if (rpmSensor != null) rpmSensor.start();
|
||||
if (resistanceSensor != null) resistanceSensor.start();
|
||||
|
||||
QLog.i(TAG, "Started callback-based sensor updates");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to start sensor updates", e);
|
||||
isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSensorUpdatesInstance() {
|
||||
isUpdating = false;
|
||||
|
||||
// Stop callback-based sensors
|
||||
if (powerSensor != null) powerSensor.stop();
|
||||
if (rpmSensor != null) rpmSensor.stop();
|
||||
if (resistanceSensor != null) resistanceSensor.stop();
|
||||
|
||||
QLog.i(TAG, "Stopped callback-based sensor updates");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate speed from power using Peloton V1 bike formula
|
||||
* Based on Grupetto's SensorInterface.kt implementation
|
||||
*/
|
||||
private float calculateSpeedFromPelotonV1Power(float power) {
|
||||
if (power < 0.1f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Use exact formula from Grupetto Peloton.kt
|
||||
double pwrSqrt = Math.sqrt(power);
|
||||
if (power < 26f) {
|
||||
return (float)(0.057f - (0.172f * pwrSqrt) + (0.759f * Math.pow(pwrSqrt, 2)) - (0.079f * Math.pow(pwrSqrt, 3)));
|
||||
} else {
|
||||
return (float)(-1.635f + (2.325f * pwrSqrt) - (0.064f * Math.pow(pwrSqrt, 2)) + (0.001f * Math.pow(pwrSqrt, 3)));
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownInstance() {
|
||||
stopSensorUpdates();
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e(TAG, "Error shutting down executor service", e);
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up sensors
|
||||
powerSensor = null;
|
||||
rpmSensor = null;
|
||||
resistanceSensor = null;
|
||||
sensorBinder = null;
|
||||
|
||||
isInitialized = false;
|
||||
}
|
||||
|
||||
// Static wrapper methods for JNI calls
|
||||
public static void initialize() {
|
||||
try {
|
||||
if (staticContext == null) {
|
||||
QLog.e(TAG, "Context not set. Call setContext() first.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance == null) {
|
||||
instance = new PelotonSensorHelperV1(staticContext);
|
||||
}
|
||||
|
||||
instance.initializeInstance();
|
||||
QLog.i(TAG, "Static V1 initialize completed");
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Peloton V1 service not available - continuing without sensor integration: " + e.getMessage());
|
||||
// Create instance anyway to provide fallback behavior
|
||||
if (instance == null) {
|
||||
instance = new PelotonSensorHelperV1(staticContext);
|
||||
}
|
||||
// Mark as not initialized but don't crash the app
|
||||
instance.isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
staticContext = context;
|
||||
}
|
||||
|
||||
public static void startSensorUpdates() {
|
||||
if (instance != null) {
|
||||
instance.startSensorUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Helper not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopSensorUpdates() {
|
||||
if (instance != null) {
|
||||
instance.stopSensorUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Helper not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
// Getter methods for current sensor values
|
||||
public static float getCurrentPower() {
|
||||
if (instance != null) {
|
||||
return instance.currentPower;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentCadence() {
|
||||
if (instance != null) {
|
||||
return instance.currentCadence;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentResistance() {
|
||||
if (instance != null) {
|
||||
return instance.currentResistance;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentSpeed() {
|
||||
if (instance != null) {
|
||||
return instance.currentSpeed;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static boolean isConnected() {
|
||||
if (instance != null && instance.sensorBinder != null) {
|
||||
return instance.sensorBinder.isConnected();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isInitialized() {
|
||||
if (instance != null) {
|
||||
return instance.isInitialized;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (instance != null) {
|
||||
instance.shutdownInstance();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
379
src/android/src/PelotonSensorService.java
Normal file
379
src/android/src/PelotonSensorService.java
Normal file
@@ -0,0 +1,379 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.ComponentName;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class PelotonSensorService {
|
||||
|
||||
private static final String TAG = "PelotonSensorService";
|
||||
|
||||
// Singleton instance for static access
|
||||
private static PelotonSensorService instance = null;
|
||||
private static Context staticContext = null;
|
||||
|
||||
// Peloton service action and permissions
|
||||
private static final String PELOTON_SENSOR_ACTION = "android.intent.action.peloton.SensorData";
|
||||
private static final String PELOTON_SENSOR_PERMISSION = "onepeloton.permission.ACCESS_SENSOR_SERVICE";
|
||||
|
||||
// Update interval for sensor reading
|
||||
private static final int SENSOR_UPDATE_INTERVAL_MS = 200;
|
||||
|
||||
// Threading components
|
||||
private Handler mainHandler;
|
||||
private ExecutorService executorService;
|
||||
private Runnable sensorUpdateRunnable;
|
||||
|
||||
// Service connection components
|
||||
private IBinder sensorBinder = null;
|
||||
private boolean isServiceConnected = false;
|
||||
private boolean isUpdating = false;
|
||||
|
||||
// Sensor components (similar to Grupetto's implementation)
|
||||
private PelotonPowerSensor powerSensor;
|
||||
private PelotonRpmSensor rpmSensor;
|
||||
private PelotonResistanceSensor resistanceSensor;
|
||||
|
||||
// Current sensor values
|
||||
private volatile float currentPower = 0.0f;
|
||||
private volatile float currentCadence = 0.0f;
|
||||
private volatile float currentResistance = 0.0f;
|
||||
private volatile float currentSpeed = 0.0f;
|
||||
|
||||
// Context for service binding
|
||||
private Context context;
|
||||
|
||||
public PelotonSensorService(Context context) {
|
||||
this.context = context;
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
private void initializeInstance() throws Exception {
|
||||
QLog.i(TAG, "Initializing Peloton sensor service connection...");
|
||||
|
||||
// Check if required permission is available
|
||||
if (context.checkSelfPermission(PELOTON_SENSOR_PERMISSION) !=
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
throw new Exception("Missing required permission: " + PELOTON_SENSOR_PERMISSION);
|
||||
}
|
||||
|
||||
// Connect to Peloton sensor service
|
||||
connectToSensorService();
|
||||
}
|
||||
|
||||
private void connectToSensorService() throws Exception {
|
||||
QLog.i(TAG, "Attempting to connect to Peloton sensor service...");
|
||||
|
||||
CompletableFuture<IBinder> binderFuture = new CompletableFuture<>();
|
||||
CountDownLatch connectionLatch = new CountDownLatch(1);
|
||||
|
||||
ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
QLog.i(TAG, "Connected to Peloton sensor service");
|
||||
sensorBinder = service;
|
||||
isServiceConnected = true;
|
||||
binderFuture.complete(service);
|
||||
connectionLatch.countDown();
|
||||
|
||||
// Initialize sensor components
|
||||
try {
|
||||
initializeSensors();
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to initialize sensors", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
QLog.w(TAG, "Disconnected from Peloton sensor service");
|
||||
sensorBinder = null;
|
||||
isServiceConnected = false;
|
||||
isUpdating = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
QLog.e(TAG, "Peloton sensor service binding died");
|
||||
sensorBinder = null;
|
||||
isServiceConnected = false;
|
||||
isUpdating = false;
|
||||
}
|
||||
};
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(PELOTON_SENSOR_ACTION);
|
||||
|
||||
boolean bound = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
if (!bound) {
|
||||
throw new Exception("Failed to bind to Peloton sensor service");
|
||||
}
|
||||
|
||||
// Wait for connection with timeout
|
||||
try {
|
||||
if (!connectionLatch.await(10, TimeUnit.SECONDS)) {
|
||||
throw new Exception("Timeout waiting for Peloton sensor service connection");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new Exception("Interrupted while waiting for service connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSensors() throws Exception {
|
||||
if (sensorBinder == null) {
|
||||
throw new Exception("Service binder not available");
|
||||
}
|
||||
|
||||
// Initialize individual sensor components (similar to Grupetto approach)
|
||||
powerSensor = new PelotonPowerSensor(sensorBinder);
|
||||
rpmSensor = new PelotonRpmSensor(sensorBinder);
|
||||
resistanceSensor = new PelotonResistanceSensor(sensorBinder);
|
||||
|
||||
QLog.i(TAG, "All sensors initialized successfully");
|
||||
}
|
||||
|
||||
private void startSensorUpdatesInstance() {
|
||||
if (isUpdating || !isServiceConnected) {
|
||||
QLog.w(TAG, "Cannot start sensor updates - service not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdating = true;
|
||||
|
||||
sensorUpdateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isUpdating || !isServiceConnected) return;
|
||||
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
// Read all sensor values
|
||||
if (powerSensor != null) {
|
||||
currentPower = powerSensor.readValue();
|
||||
}
|
||||
if (rpmSensor != null) {
|
||||
currentCadence = rpmSensor.readValue();
|
||||
}
|
||||
if (resistanceSensor != null) {
|
||||
currentResistance = resistanceSensor.readValue();
|
||||
}
|
||||
|
||||
// Calculate speed from power (similar to Grupetto approach)
|
||||
currentSpeed = calculateSpeedFromPower(currentPower);
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.w(TAG, "Error reading sensor values", e);
|
||||
}
|
||||
|
||||
if (isUpdating && isServiceConnected) {
|
||||
mainHandler.postDelayed(sensorUpdateRunnable, SENSOR_UPDATE_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mainHandler.post(sensorUpdateRunnable);
|
||||
QLog.i(TAG, "Started periodic sensor updates");
|
||||
}
|
||||
|
||||
private void stopSensorUpdatesInstance() {
|
||||
isUpdating = false;
|
||||
|
||||
if (sensorUpdateRunnable != null) {
|
||||
mainHandler.removeCallbacks(sensorUpdateRunnable);
|
||||
}
|
||||
|
||||
QLog.i(TAG, "Stopped periodic sensor updates");
|
||||
}
|
||||
|
||||
private float calculateSpeedFromPower(float power) {
|
||||
if (power < 0.1f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Use exact formula from Grupetto Peloton.kt
|
||||
double pwrSqrt = Math.sqrt(power);
|
||||
if (power < 26f) {
|
||||
return (float)(0.057f - (0.172f * pwrSqrt) + (0.759f * Math.pow(pwrSqrt, 2)) - (0.079f * Math.pow(pwrSqrt, 3)));
|
||||
} else {
|
||||
return (float)(-1.635f + (2.325f * pwrSqrt) - (0.064f * Math.pow(pwrSqrt, 2)) + (0.001f * Math.pow(pwrSqrt, 3)));
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownInstance() {
|
||||
stopSensorUpdates();
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e(TAG, "Error shutting down executor service", e);
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup sensors
|
||||
powerSensor = null;
|
||||
rpmSensor = null;
|
||||
resistanceSensor = null;
|
||||
|
||||
// Unbind from service
|
||||
if (isServiceConnected && context != null) {
|
||||
try {
|
||||
// Note: In real implementation, we'd need to properly unbind
|
||||
// context.unbindService(serviceConnection);
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Error unbinding service", e);
|
||||
}
|
||||
}
|
||||
|
||||
isServiceConnected = false;
|
||||
sensorBinder = null;
|
||||
}
|
||||
|
||||
// Static wrapper methods for JNI calls (similar to GrpcTreadmillService)
|
||||
public static void initialize() {
|
||||
try {
|
||||
if (staticContext == null) {
|
||||
QLog.e(TAG, "Context not set. Call setContext() first.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance == null) {
|
||||
instance = new PelotonSensorService(staticContext);
|
||||
}
|
||||
|
||||
instance.initializeInstance();
|
||||
QLog.i(TAG, "Static initialize completed");
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Static initialize failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
staticContext = context;
|
||||
}
|
||||
|
||||
public static void startSensorUpdates() {
|
||||
if (instance != null) {
|
||||
instance.startSensorUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopSensorUpdates() {
|
||||
if (instance != null) {
|
||||
instance.stopSensorUpdatesInstance();
|
||||
} else {
|
||||
QLog.e(TAG, "Service not initialized. Call initialize() first.");
|
||||
}
|
||||
}
|
||||
|
||||
// Getter methods for current sensor values
|
||||
public static float getCurrentPower() {
|
||||
if (instance != null) {
|
||||
return instance.currentPower;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentCadence() {
|
||||
if (instance != null) {
|
||||
return instance.currentCadence;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentResistance() {
|
||||
if (instance != null) {
|
||||
return instance.currentResistance;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float getCurrentSpeed() {
|
||||
if (instance != null) {
|
||||
return instance.currentSpeed;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static boolean isConnected() {
|
||||
if (instance != null) {
|
||||
return instance.isServiceConnected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
if (instance != null) {
|
||||
instance.shutdownInstance();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Inner classes for individual sensors (simplified versions based on Grupetto)
|
||||
private static class PelotonPowerSensor {
|
||||
private IBinder binder;
|
||||
|
||||
public PelotonPowerSensor(IBinder binder) {
|
||||
this.binder = binder;
|
||||
}
|
||||
|
||||
public float readValue() throws RemoteException {
|
||||
// Implementation would call into Peloton service via binder
|
||||
// This is a simplified version - actual implementation would need
|
||||
// proper AIDL interface definitions
|
||||
|
||||
// For now, return mock data or attempt basic binder calls
|
||||
// In real implementation, this would use proper service calls
|
||||
return 0.0f; // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
private static class PelotonRpmSensor {
|
||||
private IBinder binder;
|
||||
|
||||
public PelotonRpmSensor(IBinder binder) {
|
||||
this.binder = binder;
|
||||
}
|
||||
|
||||
public float readValue() throws RemoteException {
|
||||
// Implementation would call into Peloton service via binder
|
||||
return 0.0f; // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
private static class PelotonResistanceSensor {
|
||||
private IBinder binder;
|
||||
|
||||
public PelotonResistanceSensor(IBinder binder) {
|
||||
this.binder = binder;
|
||||
}
|
||||
|
||||
public float readValue() throws RemoteException {
|
||||
// Implementation would call into Peloton service via binder
|
||||
return 0.0f; // Placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,8 +211,9 @@ public class SDMChannelController {
|
||||
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
|
||||
payload[2] = (byte) ((lastTime % 256000) / 1000);
|
||||
payload[3] = (byte) 0x00;
|
||||
payload[4] = (byte) speedM_s;
|
||||
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
|
||||
int speedFixed = (int) Math.round(speedM_s * 256.0);
|
||||
payload[4] = (byte) (speedFixed & 0xFF); // low byte
|
||||
payload[5] = (byte) ((speedFixed >> 8) & 0xFF); // high byte
|
||||
payload[6] = (byte) stride_count++; // bad but it works on zwift
|
||||
payload[7] = (byte) ((double)deltaTime * 0.03125);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
20
src/android/src/main/proto/activitylog/ActivityLog.proto
Normal file
20
src/android/src/main/proto/activitylog/ActivityLog.proto
Normal file
@@ -0,0 +1,20 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "activitylog/ActivityLogStats.proto";
|
||||
import "activitylog/ActivityLogSummary.proto";
|
||||
import "activitylog/ActivityLogMetadata.proto";
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ActivityLog {
|
||||
ActivityLogMetadata metadata = 1;
|
||||
string id = 2;
|
||||
int32 softwareNumber = 3;
|
||||
int64 startMsSinceEpoch = 4;
|
||||
int64 endMsSinceEpoch = 5;
|
||||
int32 durationMs = 6;
|
||||
ActivityLogStats stats = 7;
|
||||
ActivityLogSummary summary = 8;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogErrorProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum ActivityLogErrorCode {
|
||||
ACTIVITY_LOG_UNKNOWN_ERROR = 0;
|
||||
ACTIVITY_LOG_NOT_FOUND_ERROR = 1;
|
||||
ACTIVITY_LOG_INVALID_TYPE_ERROR = 2;
|
||||
ACTIVITY_LOG_INVALID_DURATION_ERROR = 3;
|
||||
}
|
||||
|
||||
message ActivityLogError {
|
||||
ActivityLogErrorCode errorCode = 1;
|
||||
string message = 2;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogEventProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum ActivityLogEventType {
|
||||
ACTIVITY_LOG_EVENT_WORKOUT_STARTED = 0;
|
||||
ACTIVITY_LOG_EVENT_WORKOUT_MINIMUMS_REACHED = 1;
|
||||
ACTIVITY_LOG_EVENT_UPLOAD_STARTED = 2;
|
||||
ACTIVITY_LOG_EVENT_UPLOAD_SUCCESSFUL = 3;
|
||||
ACTIVITY_LOG_EVENT_WORKOUT_COMPLETED_NOT_UPLOADING = 4;
|
||||
ACTIVITY_LOG_EVENT_UPLOAD_RECOVERABLE_ERROR = 5;
|
||||
ACTIVITY_LOG_EVENT_UPLOAD_TERMINAL_ERROR = 6;
|
||||
ACTIVITY_LOG_EVENT_UNDER_MINIMUM_DURATION_ERROR = 7;
|
||||
ACTIVITY_LOG_EVENT_UNDER_MINIMUM_DISTANCE_ERROR = 8;
|
||||
ACTIVITY_LOG_EVENT_METADATA_UPDATED = 9;
|
||||
ACTIVITY_LOG_EVENT_WORKOUT_COMPLETED_ANONYMOUSLY_NOT_UPLOADING = 10;
|
||||
}
|
||||
|
||||
message ActivityLogEvent {
|
||||
ActivityLogEventType eventType = 1;
|
||||
string workoutID = 2;
|
||||
string contentID = 3;
|
||||
bool shouldUploadLog = 4;
|
||||
string activityLogID = 5;
|
||||
string errorCode = 6;
|
||||
int32 minimumDurationSeconds = 7;
|
||||
int32 workoutDurationSeconds = 8;
|
||||
int32 minimumDistanceMeters = 9;
|
||||
int32 workoutDistanceMeters = 10;
|
||||
string workoutDriverFQN = 11;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "activitylog/ActivityLogUtils.proto";
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogMetadataProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ActivityLogMetadata {
|
||||
string workoutId = 1;
|
||||
bool shouldUploadLog = 2;
|
||||
string contentId = 3;
|
||||
string title = 4;
|
||||
string heroImageUrl = 5;
|
||||
string socialImageUrl = 6;
|
||||
string programId = 7;
|
||||
string videoId = 8;
|
||||
string listWorkoutId = 9;
|
||||
string liveWorkoutId = 10;
|
||||
string liveWorkoutScheduleId = 11;
|
||||
ActivityLogOrigin origin = 12;
|
||||
ActivityLogContext context = 13;
|
||||
ActivityLogType type = 14;
|
||||
string typeDetail = 15;
|
||||
string externalType = 16;
|
||||
repeated string completedMovements = 17;
|
||||
bool redundant = 18;
|
||||
int32 sleepScore = 19;
|
||||
string seriesId = 20;
|
||||
string challengeId = 21;
|
||||
string workoutDriverFQN = 22;
|
||||
string thirdPartyContentId = 23;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
import "util/Util.proto";
|
||||
import "activitylog/ActivityLog.proto";
|
||||
import "activitylog/ActivityLogEvent.proto";
|
||||
import "activitylog/ActivityLogMetadata.proto";
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ActivityLogResult {
|
||||
oneof errorOrToken {
|
||||
IFitError error = 1;
|
||||
ActivityLog activityLog = 2;
|
||||
ActivityLogMetadata metadata = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ActivityLogID {
|
||||
string id = 1;
|
||||
}
|
||||
message ExternalUploadRequest {
|
||||
ActivityLog log = 1;
|
||||
string userId = 2;
|
||||
}
|
||||
|
||||
message ContentID {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message ActivityLogUploading {
|
||||
bool isUploading = 1;
|
||||
}
|
||||
|
||||
service ActivityLogService {
|
||||
rpc HasUnprocessedUploadEventsSubscription(Empty) returns (stream BooleanResponse) {}
|
||||
rpc PopMostRecentUploadEvent(Empty) returns (ActivityLogEvent) {}
|
||||
|
||||
rpc ActivityLogEventSubscription(Empty) returns (stream ActivityLogEvent) {}
|
||||
rpc ActivityLogUploadingSubscription(Empty) returns (stream ActivityLogUploading) {}
|
||||
|
||||
rpc GetActivityLogMetadataByWorkoutId(WorkoutID) returns (ActivityLogResult) {}
|
||||
rpc ChangeActivityLogMetadata(ActivityLogMetadata) returns (ActivityLogResult) {}
|
||||
|
||||
rpc GetActivityLogByWorkoutId(WorkoutID) returns (ActivityLogResult) {}
|
||||
rpc GetLatestActivityLogByContentId(ContentID) returns (ActivityLogResult) {}
|
||||
rpc GetActivityLogByActivityLogId(ActivityLogID) returns (ActivityLogResult) {}
|
||||
rpc DeleteActivityLogByActivityLogId(ActivityLogID) returns (ActivityLogResult) {}
|
||||
rpc UploadActivityLogFromExternalSource(ExternalUploadRequest) returns (ActivityLogResult) {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "activitylog/ActivityLogUtils.proto";
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogStatsProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ActivityLogStats {
|
||||
repeated ActivityOffsetValue bpm = 1;
|
||||
repeated ActivityOffsetValue calories = 2;
|
||||
repeated ActivityOffsetValue elevation = 3;
|
||||
repeated ActivityOffsetValue fiveHundredSplit = 4;
|
||||
repeated ActivityOffsetValue incline = 5;
|
||||
repeated ActivityOffsetValue meters = 6;
|
||||
repeated ActivityOffsetValue mps = 7;
|
||||
repeated ActivityOffsetValue resistance = 8;
|
||||
repeated ActivityOffsetValue rpm = 9;
|
||||
repeated ActivityOffsetValue watts = 10;
|
||||
repeated ActivityOffsetValue cadence = 11;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogSummaryProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ActivityLogSummary {
|
||||
int32 averageBpm = 1;
|
||||
int32 averageFiveHundredSplit = 2;
|
||||
int32 averageResistance = 3;
|
||||
int32 averageRespiration = 4;
|
||||
int32 averageSpm = 5;
|
||||
double averageWatts = 6;
|
||||
int32 maxBpm = 7;
|
||||
int32 maxFiveHundredSplit = 8;
|
||||
int32 maxSpm = 9;
|
||||
int32 maxWatts = 10;
|
||||
int32 minFiveHundredSplit = 11;
|
||||
float totalCalories = 12;
|
||||
float totalElevationGain = 13;
|
||||
float totalMeters = 14;
|
||||
int32 totalMovements = 15;
|
||||
int32 totalSteps = 16;
|
||||
int32 averageCadence = 17;
|
||||
int32 maxCadence = 18;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.activitylog";
|
||||
option java_outer_classname = "ActivityLogUtilsProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum ActivityLogContext {
|
||||
ACT_LOG_CONTEXT_ON_DEMAND = 0;
|
||||
ACT_LOG_CONTEXT_SCHEDULED_LIVE = 1;
|
||||
ACT_LOG_CONTEXT_SCHEDULED_PRE = 2;
|
||||
}
|
||||
|
||||
enum ActivityLogOrigin {
|
||||
ACT_LOG_ORIGIN_BIKE = 0;
|
||||
ACT_LOG_ORIGIN_DAILY = 1;
|
||||
ACT_LOG_ORIGIN_ELLIPTICAL = 2;
|
||||
ACT_LOG_ORIGIN_FUSION = 3;
|
||||
ACT_LOG_ORIGIN_GARMIN = 4;
|
||||
ACT_LOG_ORIGIN_GOOGLEFIT = 5;
|
||||
ACT_LOG_ORIGIN_HEALTHKIT = 6;
|
||||
ACT_LOG_ORIGIN_IFITAPP = 7;
|
||||
ACT_LOG_ORIGIN_ROWER = 8;
|
||||
ACT_LOG_ORIGIN_SLEEPSENSOR = 9;
|
||||
ACT_LOG_ORIGIN_STATIONARYBIKE = 10;
|
||||
ACT_LOG_ORIGIN_STRAVA = 11;
|
||||
ACT_LOG_ORIGIN_STRIDER = 12;
|
||||
ACT_LOG_ORIGIN_THIRDPARTY = 13;
|
||||
ACT_LOG_ORIGIN_TREADMILL = 14;
|
||||
ACT_LOG_ORIGIN_WEARABLE = 15;
|
||||
ACT_LOG_ORIGIN_WEBSITE = 16;
|
||||
ACT_LOG_ORIGIN_VALINOR = 17;
|
||||
}
|
||||
|
||||
enum ActivityLogType {
|
||||
ACT_LOG_TYPE_CARDIO = 0;
|
||||
ACT_LOG_TYPE_CYCLE = 1;
|
||||
ACT_LOG_TYPE_RUN = 2;
|
||||
ACT_LOG_TYPE_PULLEY = 3;
|
||||
ACT_LOG_TYPE_FUSION = 4;
|
||||
ACT_LOG_TYPE_ROW = 5;
|
||||
ACT_LOG_TYPE_DAILY_VIDEO = 6;
|
||||
ACT_LOG_TYPE_STRENGTH = 7;
|
||||
}
|
||||
|
||||
message ActivityOffsetValue {
|
||||
float offset = 1;
|
||||
float value = 2;
|
||||
}
|
||||
23
src/android/src/main/proto/antplus/AntPlusDevice.proto
Normal file
23
src/android/src/main/proto/antplus/AntPlusDevice.proto
Normal file
@@ -0,0 +1,23 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.antplus";
|
||||
option java_outer_classname = "AntPlusDeviceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message AntPlusDevice {
|
||||
int32 deviceType = 1;
|
||||
int32 deviceNumber = 2;
|
||||
int32 signalStrength = 3;
|
||||
int32 manufacturerID = 4;
|
||||
int32 serialNumberLSB = 5;
|
||||
int32 serialNumberMSB = 6;
|
||||
int32 hardwareVersion = 7;
|
||||
int32 softwareVersion = 8;
|
||||
int32 modelNumber = 9;
|
||||
int32 serialNumberCalculated = 10;
|
||||
}
|
||||
|
||||
message AntPlusDeviceList {
|
||||
repeated AntPlusDevice devices = 1;
|
||||
}
|
||||
18
src/android/src/main/proto/antplus/AntPlusService.proto
Normal file
18
src/android/src/main/proto/antplus/AntPlusService.proto
Normal file
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.antplus";
|
||||
option java_outer_classname = "AntPlusServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
import "antplus/AntPlusDevice.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
message AntPlusScanDurationMessage {
|
||||
int32 durationSeconds = 1;
|
||||
}
|
||||
|
||||
service AntPlusService {
|
||||
rpc ScanForDuration(AntPlusScanDurationMessage) returns (Empty) {}
|
||||
rpc FoundAntPlusDevicesSubscription(Empty) returns (stream AntPlusDeviceList) {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/Util.proto";
|
||||
import "appnavigation/ForegroundFqns.proto";
|
||||
import "appnavigation/TouchEvent.proto";
|
||||
import "appnavigation/ForegroundClasses.proto";
|
||||
import "appnavigation/ForegroundRequest.proto";
|
||||
option java_package = "com.ifit.glassos.appnavigation";
|
||||
option java_outer_classname = "AppNavigationServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
service AppNavigationService {
|
||||
rpc ForegroundFQNsFlowSubscription(Empty) returns (stream ForegroundFqns) {}
|
||||
rpc TouchFlowSubscription(Empty) returns (stream TouchEvent) {}
|
||||
rpc EnabledSubscription(Empty) returns (stream BooleanResponse) {}
|
||||
rpc KeyboardVisibleFlowSubscription(Empty) returns (stream BooleanResponse) {}
|
||||
rpc ForegroundClassNameFlowSubscription(Empty) returns (stream ListStringResponse) {}
|
||||
rpc ForegroundClassesFlowSubscription(Empty) returns (stream ForegroundClasses) {}
|
||||
|
||||
rpc PerformBackButton(Empty) returns (Empty) {}
|
||||
rpc GetForegroundFqns(Empty) returns (ForegroundFqns) {}
|
||||
rpc SetCurrentForegroundFQN(ForegroundFqnRequest) returns (Empty) {}
|
||||
rpc RemoveCurrentForegroundFQN(ForegroundFqnRequest) returns (Empty) {}
|
||||
rpc RemoveForegroundFQNFromHistory(ForegroundFqnRequest) returns (Empty) {}
|
||||
rpc SetCurrentForegroundClass(ForegroundClassNameRequest) returns (Empty) {}
|
||||
rpc NavigatedToThirdParty(ForegroundFqnRequest) returns (Empty) {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appnavigation";
|
||||
option java_outer_classname = "ForegroundClassesProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ForegroundClasses {
|
||||
ForegroundClass currentlyForegrounded = 1;
|
||||
repeated ForegroundClass foregroundHistory = 2;
|
||||
}
|
||||
|
||||
message ForegroundClass {
|
||||
string className = 1;
|
||||
int64 timestamp = 2;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appnavigation";
|
||||
option java_outer_classname = "ForegroundFqnsProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ForegroundFqns {
|
||||
string currentFqn = 1;
|
||||
repeated string historyFqns = 2;
|
||||
ForegroundFqn currentlyForegrounded = 3;
|
||||
repeated ForegroundFqn foregroundHistory = 4;
|
||||
}
|
||||
|
||||
message ForegroundFqn {
|
||||
string fqn = 1;
|
||||
int64 timestamp = 2;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appnavigation";
|
||||
option java_outer_classname = "ForegroundRequestProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ForegroundFqnRequest {
|
||||
string fqn = 1;
|
||||
}
|
||||
|
||||
message ForegroundClassNameRequest {
|
||||
string className = 1;
|
||||
}
|
||||
10
src/android/src/main/proto/appnavigation/TouchEvent.proto
Normal file
10
src/android/src/main/proto/appnavigation/TouchEvent.proto
Normal file
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appnavigation";
|
||||
option java_outer_classname = "TouchEventProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message TouchEvent {
|
||||
int64 timestamp = 1;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appstore";
|
||||
option java_outer_classname = "AppStoreActionRequestProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message AppStoreActionRequest {
|
||||
string fqn = 1;
|
||||
}
|
||||
21
src/android/src/main/proto/appstore/AppStoreApp.proto
Normal file
21
src/android/src/main/proto/appstore/AppStoreApp.proto
Normal file
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "appstore/AppStoreAppStatus.proto";
|
||||
option java_package = "com.ifit.glassos.appstore";
|
||||
option java_outer_classname = "AppStoreAppProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message AppStoreApp {
|
||||
string label = 1;
|
||||
string icon = 2;
|
||||
string fqn = 3;
|
||||
string category = 4;
|
||||
string version = 5;
|
||||
bool installed = 6;
|
||||
AppStoreAppStatus status = 7;
|
||||
}
|
||||
|
||||
message AppStoreAppList {
|
||||
repeated AppStoreApp appStoreApps = 1;
|
||||
}
|
||||
12
src/android/src/main/proto/appstore/AppStoreAppStatus.proto
Normal file
12
src/android/src/main/proto/appstore/AppStoreAppStatus.proto
Normal file
@@ -0,0 +1,12 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appstore";
|
||||
option java_outer_classname = "AppStoreAppStatusProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum AppStoreAppStatus {
|
||||
NOT_INSTALLED = 0;
|
||||
INSTALLED = 1;
|
||||
PENDING = 2;
|
||||
}
|
||||
23
src/android/src/main/proto/appstore/AppStoreService.proto
Normal file
23
src/android/src/main/proto/appstore/AppStoreService.proto
Normal file
@@ -0,0 +1,23 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/Util.proto";
|
||||
import "appstore/StorageStats.proto";
|
||||
import "appstore/AppStoreApp.proto";
|
||||
import "appstore/AppStoreActionRequest.proto";
|
||||
import "appstore/AppStoreState.proto";
|
||||
option java_package = "com.ifit.glassos.appstore";
|
||||
option java_outer_classname = "AppStoreServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
service AppStoreService {
|
||||
rpc AppStoreStateFlowSubscription(Empty) returns (stream AppStoreState) {}
|
||||
rpc AppsFlowSubscription(Empty) returns (stream AppStoreAppList) {}
|
||||
|
||||
rpc RequestAppInstall(AppStoreActionRequest) returns (Empty) {}
|
||||
rpc RequestAppUninstall(AppStoreActionRequest) returns (Empty) {}
|
||||
rpc GetApps(BooleanRequest) returns (Empty) {}
|
||||
rpc GoIdle(Empty) returns (Empty) {}
|
||||
|
||||
rpc GetStorageStats(Empty) returns (StorageStats) {}
|
||||
}
|
||||
43
src/android/src/main/proto/appstore/AppStoreState.proto
Normal file
43
src/android/src/main/proto/appstore/AppStoreState.proto
Normal file
@@ -0,0 +1,43 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appstore";
|
||||
option java_outer_classname = "AppStoreStateProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message AppStoreState {
|
||||
oneof state {
|
||||
Idle idle = 1;
|
||||
Loading loading = 2;
|
||||
Checking checking = 3;
|
||||
Uninstalling uninstalling = 4;
|
||||
Error error = 5;
|
||||
Downloading downloading = 6;
|
||||
Installing installing = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message Idle {}
|
||||
|
||||
message Loading {}
|
||||
|
||||
message Checking {}
|
||||
|
||||
message Uninstalling {
|
||||
string fqn = 1;
|
||||
}
|
||||
|
||||
message Error {
|
||||
int32 errorCode = 1;
|
||||
optional string fqn = 2;
|
||||
}
|
||||
|
||||
message Downloading {
|
||||
string fqn = 1;
|
||||
float progress = 2;
|
||||
}
|
||||
|
||||
message Installing {
|
||||
string fqn = 1;
|
||||
float progress = 2;
|
||||
}
|
||||
12
src/android/src/main/proto/appstore/StorageStats.proto
Normal file
12
src/android/src/main/proto/appstore/StorageStats.proto
Normal file
@@ -0,0 +1,12 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.appstore";
|
||||
option java_outer_classname = "StorageStatsProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message StorageStats {
|
||||
int64 totalBytes = 1;
|
||||
int64 allocatableBytes = 2;
|
||||
int64 reservedBytes = 3;
|
||||
}
|
||||
17
src/android/src/main/proto/auth/AuthError.proto
Normal file
17
src/android/src/main/proto/auth/AuthError.proto
Normal file
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.auth";
|
||||
option java_outer_classname = "AuthErrorCodeProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum AuthErrorCode {
|
||||
AUTH_FAILURE = 0;
|
||||
AUTH_LOGIN_REQUIRED = 1;
|
||||
AUTH_NETWORK_ERROR = 2;
|
||||
}
|
||||
|
||||
message AuthError {
|
||||
AuthErrorCode errorCode = 1;
|
||||
string message = 2;
|
||||
}
|
||||
116
src/android/src/main/proto/auth/AuthService.proto
Normal file
116
src/android/src/main/proto/auth/AuthService.proto
Normal file
@@ -0,0 +1,116 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
import "util/Util.proto";
|
||||
option java_package = "com.ifit.glassos.auth";
|
||||
option java_outer_classname = "AuthServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message Username {
|
||||
string username = 1;
|
||||
}
|
||||
|
||||
message UserCredentials {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message AuthToken {
|
||||
string username = 1;
|
||||
string accessToken = 2;
|
||||
int64 validUntilTimestampMs = 3;
|
||||
}
|
||||
|
||||
message AuthCredentials {
|
||||
string username = 1;
|
||||
string accessToken = 2;
|
||||
string refreshToken = 3;
|
||||
int64 expiresIn = 4;
|
||||
}
|
||||
|
||||
message GetCurrentTokenRequest {
|
||||
bool forceRefresh = 1;
|
||||
}
|
||||
|
||||
message AuthResult {
|
||||
oneof errorOrToken {
|
||||
IFitError error = 1;
|
||||
AuthToken token = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message MachineToken {
|
||||
string accessToken = 1;
|
||||
int64 validUntilTimestampMs = 2;
|
||||
}
|
||||
|
||||
message MachineTokenResult {
|
||||
oneof errorOrToken {
|
||||
IFitError error = 1;
|
||||
MachineToken token = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message AuthTokenList {
|
||||
repeated AuthToken tokens = 1;
|
||||
}
|
||||
|
||||
message AuthQRCodeData {
|
||||
string deviceCode = 1;
|
||||
string userCode = 2;
|
||||
string verificationUri = 3;
|
||||
string verificationUriComplete = 4;
|
||||
int64 expiresIn = 5;
|
||||
}
|
||||
|
||||
message AuthQRCodeResult {
|
||||
oneof errorOrData {
|
||||
IFitError error = 1;
|
||||
AuthQRCodeData data = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message AuthQRCodePollingState {
|
||||
oneof state {
|
||||
AUTH_QR_CODE_POLLING_IDLE pollingIdle = 1;
|
||||
AUTH_QR_CODE_POLLING_ACTIVE pollingActive = 2;
|
||||
AUTH_QR_CODE_POLLING_EXPIRED pollingExpired = 3;
|
||||
AUTH_QR_CODE_POLLING_USER_AUTHED pollingUserAuthed = 4;
|
||||
AUTH_QR_CODE_POLLING_ERROR pollingError = 5;
|
||||
AUTH_QR_CODE_POLLING_AUTH_ERROR pollingAuthError = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message AUTH_QR_CODE_POLLING_IDLE {}
|
||||
message AUTH_QR_CODE_POLLING_ACTIVE {}
|
||||
message AUTH_QR_CODE_POLLING_EXPIRED {}
|
||||
message AUTH_QR_CODE_POLLING_USER_AUTHED {
|
||||
AuthToken token = 1;
|
||||
}
|
||||
message AUTH_QR_CODE_POLLING_ERROR {
|
||||
int32 errorCode = 1;
|
||||
optional string errorMessage = 2;
|
||||
}
|
||||
message AUTH_QR_CODE_POLLING_AUTH_ERROR {
|
||||
int32 errorCode = 1;
|
||||
optional string errorMessage = 2;
|
||||
}
|
||||
|
||||
|
||||
service AuthService {
|
||||
rpc Login(UserCredentials) returns (AuthResult) {}
|
||||
rpc SwitchUser(Username) returns (AuthResult) {}
|
||||
rpc SetCredentials(AuthCredentials) returns (AuthResult) {}
|
||||
rpc Logout(Empty) returns (Empty) {}
|
||||
rpc GetQRCodeData(Empty) returns (AuthQRCodeResult) {}
|
||||
rpc StopPollingForQRAuthToken(Empty) returns (Empty) {}
|
||||
rpc QrCodePollingStateChanged(Empty) returns (stream AuthQRCodePollingState) {}
|
||||
|
||||
rpc TokenChanged(Empty) returns (stream AuthToken) {}
|
||||
rpc GetCurrentToken(GetCurrentTokenRequest) returns (AuthResult) {}
|
||||
rpc GetAllTokens(Empty) returns (AuthTokenList) {}
|
||||
|
||||
rpc MachineTokenChanged(Empty) returns (stream MachineToken) {}
|
||||
rpc GetMachineToken(Empty) returns (MachineTokenResult) {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.bluetooth";
|
||||
option java_outer_classname = "BluetoothConnectionStateProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum BluetoothConnectionState {
|
||||
BLE_DEVICE_CONNECTED = 0;
|
||||
BLE_DEVICE_CONNECTING = 1;
|
||||
BLE_DEVICE_DISCONNECTED = 2;
|
||||
}
|
||||
38
src/android/src/main/proto/bluetooth/BluetoothDevice.proto
Normal file
38
src/android/src/main/proto/bluetooth/BluetoothDevice.proto
Normal file
@@ -0,0 +1,38 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "bluetooth/BluetoothDeviceType.proto";
|
||||
import "bluetooth/BluetoothConnectionState.proto";
|
||||
option java_package = "com.ifit.glassos.bluetooth";
|
||||
option java_outer_classname = "BluetoothDeviceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message DeviceStreamRequest {
|
||||
string macAddress = 1;
|
||||
}
|
||||
|
||||
message DeviceConnectionStateResult {
|
||||
BluetoothConnectionState connectionState = 1;
|
||||
}
|
||||
|
||||
message DeviceRssiResult {
|
||||
int32 rssi = 1;
|
||||
}
|
||||
|
||||
message DeviceBatteryLevelResult {
|
||||
int32 batteryLevel = 1;
|
||||
}
|
||||
|
||||
message BluetoothDevice {
|
||||
string deviceName = 1;
|
||||
string macAddress = 2;
|
||||
int32 rssi = 3;
|
||||
int32 batteryLevel = 4;
|
||||
BluetoothDeviceType deviceType = 5;
|
||||
BluetoothConnectionState connectionState = 6;
|
||||
string pairKey = 7;
|
||||
}
|
||||
|
||||
message BluetoothDeviceList {
|
||||
repeated BluetoothDevice devices = 1;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.bluetooth";
|
||||
option java_outer_classname = "BluetoothDeviceTypeProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum BluetoothDeviceType {
|
||||
BLE_IFIT_CONSOLE = 0;
|
||||
BLE_HEART_RATE = 1;
|
||||
BLE_HEADPHONE = 2;
|
||||
BLE_OTHER = 3;
|
||||
BLE_IFIT_VIRTUAL_CONSOLE = 4;
|
||||
BLE_SMART_WATCH = 5;
|
||||
ARCX_RING = 6;
|
||||
BLE_PHONE_TABLET = 7;
|
||||
}
|
||||
66
src/android/src/main/proto/bluetooth/BluetoothService.proto
Normal file
66
src/android/src/main/proto/bluetooth/BluetoothService.proto
Normal file
@@ -0,0 +1,66 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
import "util/Util.proto";
|
||||
import "bluetooth/BluetoothDevice.proto";
|
||||
import "bluetooth/BluetoothDeviceType.proto";
|
||||
option java_package = "com.ifit.glassos.bluetooth";
|
||||
option java_outer_classname = "BluetoothServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message BluetoothResult {
|
||||
oneof errorOrSuccess {
|
||||
IFitError error = 1;
|
||||
bool success = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message StartScanRequest {
|
||||
int32 scanTimeoutSeconds = 1;
|
||||
repeated BluetoothDeviceType deviceTypes = 2;
|
||||
}
|
||||
|
||||
message BluetoothScanState {
|
||||
bool scanning = 1;
|
||||
}
|
||||
|
||||
message BluetoothServiceState {
|
||||
repeated BluetoothDevice connectedDevices = 1;
|
||||
}
|
||||
|
||||
message MACAddressConnectionRequest {
|
||||
string macAddress = 1;
|
||||
BluetoothDeviceType deviceType = 2;
|
||||
}
|
||||
|
||||
message MACAddressConnectionResult {
|
||||
oneof emptyOrDevice {
|
||||
BluetoothDevice device = 1;
|
||||
Empty empty = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message DeviceIdentifierRequest {
|
||||
string deviceIdentifier = 1;
|
||||
}
|
||||
|
||||
service BluetoothService {
|
||||
rpc ScanStateChanged(Empty) returns (stream BluetoothScanState) {}
|
||||
rpc BluetoothServiceStateChanged(Empty) returns (stream BluetoothServiceState) {}
|
||||
rpc FoundDevicesChanged(Empty) returns (stream BluetoothDevice) {}
|
||||
rpc StartScan(StartScanRequest) returns (BluetoothResult) {}
|
||||
rpc StopScan(Empty) returns (BluetoothResult) {}
|
||||
|
||||
rpc ConnectDevice(BluetoothDevice) returns (BluetoothResult) {}
|
||||
rpc ConnectWithMACAddress(MACAddressConnectionRequest) returns (MACAddressConnectionResult) {}
|
||||
rpc DisconnectDevice(BluetoothDevice) returns (BluetoothResult) {}
|
||||
rpc ConnectToHRM(DeviceIdentifierRequest) returns (BluetoothResult) {}
|
||||
rpc ConnectToRing(DeviceIdentifierRequest) returns (BluetoothResult) {}
|
||||
|
||||
rpc GetPairedDevices(Empty) returns (BluetoothDeviceList) {}
|
||||
|
||||
rpc BluetoothDeviceBatteryLevelChanged(DeviceStreamRequest) returns (stream DeviceBatteryLevelResult) {}
|
||||
rpc BluetoothDeviceConnectionStateChanged(DeviceStreamRequest) returns (stream DeviceConnectionStateResult) {}
|
||||
rpc BluetoothDeviceRSSIChanged(DeviceStreamRequest) returns (stream DeviceRssiResult) {}
|
||||
}
|
||||
95
src/android/src/main/proto/client_analytics.proto
Normal file
95
src/android/src/main/proto/client_analytics.proto
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2021 Google LLC.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package firebase.transport;
|
||||
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Top level metrics for all client analytics metrics.
|
||||
// These metrics should be sent as a part of every request that is uploaded to
|
||||
// FireLog server. In more detail, an additional LogRequest should be added to
|
||||
// the BatchedLogRequest, where the LogSource of the LogRequest should be
|
||||
// GDT_CLIENT_METRICS and the LogRequest should have a single LogEvent whose
|
||||
// payload is a ClientMetrics message.
|
||||
//
|
||||
// See go/firelog-client-analytics for more details.
|
||||
message ClientMetrics {
|
||||
// The window of time over which the metrics are evaluated.
|
||||
TimeWindow window = 1;
|
||||
|
||||
repeated LogSourceMetrics log_source_metrics = 2;
|
||||
|
||||
GlobalMetrics global_metrics = 3;
|
||||
|
||||
// The bundle ID on Apple platforms (e.g., iOS) or the package name on Android
|
||||
string app_namespace = 4;
|
||||
}
|
||||
|
||||
// Represents an arbitrary window of time.
|
||||
message TimeWindow {
|
||||
// The time that the window first starts.
|
||||
// start_ms is the number of milliseconds since the UNIX epoch
|
||||
// (January 1, 1970 00:00:00 UTC)
|
||||
int64 start_ms = 1;
|
||||
|
||||
// The time that the window ends.
|
||||
// end_ms is the number of milliseconds since the UNIX epoch
|
||||
// (January 1, 1970 00:00:00 UTC)
|
||||
int64 end_ms = 2;
|
||||
}
|
||||
|
||||
// Metrics per app, not per log source
|
||||
message GlobalMetrics {
|
||||
StorageMetrics storage_metrics = 1;
|
||||
}
|
||||
|
||||
message StorageMetrics {
|
||||
// The number of bytes of storage the event cache was consuming on the client
|
||||
// at the time the request was sent.
|
||||
int64 current_cache_size_bytes = 1;
|
||||
|
||||
// The maximum number of bytes to which the event cache is allowed to grow.
|
||||
int64 max_cache_size_bytes = 2;
|
||||
}
|
||||
|
||||
// Metrics per log source.
|
||||
message LogSourceMetrics {
|
||||
// A LogSource uniquely identifies a logging configuration. log_source should
|
||||
// contains a string value of the LogSource from
|
||||
// google3/wireless/android/play/playlog/proto/clientanalytics.proto
|
||||
string log_source = 1;
|
||||
|
||||
repeated LogEventDropped log_event_dropped = 2;
|
||||
}
|
||||
|
||||
message LogEventDropped {
|
||||
// A count of how many log event have been dropped on the client.
|
||||
int64 events_dropped_count = 1;
|
||||
|
||||
// The reason why log events have been dropped on the client.
|
||||
enum Reason {
|
||||
REASON_UNKNOWN = 0;
|
||||
MESSAGE_TOO_OLD = 1;
|
||||
CACHE_FULL = 2;
|
||||
PAYLOAD_TOO_BIG = 3;
|
||||
MAX_RETRIES_REACHED = 4;
|
||||
INVALID_PAYLOD = 5;
|
||||
SERVER_ERROR = 6;
|
||||
}
|
||||
|
||||
Reason reason = 3;
|
||||
}
|
||||
84
src/android/src/main/proto/club/ClubSettingsService.proto
Normal file
84
src/android/src/main/proto/club/ClubSettingsService.proto
Normal file
@@ -0,0 +1,84 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos.club;
|
||||
option java_package = "com.ifit.glassos.club";
|
||||
option java_outer_classname = "ClubSettingsServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
import "settings/SystemUnitsService.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
// Enum for UserRole
|
||||
enum UserRole {
|
||||
HOME_USER = 0;
|
||||
CLUB_ADMIN = 1;
|
||||
CLUB_GUEST = 2;
|
||||
CLUB_USER = 3;
|
||||
}
|
||||
|
||||
// Response message for getting club code
|
||||
message GetClubCodeResponse {
|
||||
oneof errorOrClubCode {
|
||||
IFitError error = 1;
|
||||
string clubCode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Response message for getting video screensaver setting
|
||||
message GetUseVideoScreensaverResponse {
|
||||
oneof errorOrUseVideoScreensaver {
|
||||
IFitError error = 1;
|
||||
bool useVideoScreensaver = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message UserRoleResponse {
|
||||
UserRole role = 1;
|
||||
}
|
||||
|
||||
// Request message for changing user role
|
||||
message ChangeUserRoleRequest {
|
||||
UserRole newRole = 1;
|
||||
}
|
||||
|
||||
// Request message for saving club code
|
||||
message SaveClubCodeRequest {
|
||||
string clubCode = 1;
|
||||
}
|
||||
|
||||
// Request message for saving video screensaver setting
|
||||
message SaveUseVideoScreensaverRequest {
|
||||
bool useVideoScreensaver = 1;
|
||||
}
|
||||
|
||||
// Request message for saving default language
|
||||
message SaveDefaultLanguageRequest {
|
||||
string language = 1;
|
||||
}
|
||||
|
||||
// Request message for saving default language
|
||||
message IsEgymEnabledRequest {
|
||||
bool featureFlagOnly = 1;
|
||||
bool adminOnly = 2;
|
||||
}
|
||||
|
||||
// Service definition for IFitClubSettingsService
|
||||
service IFitClubSettingsService {
|
||||
rpc ChangeUserRole(ChangeUserRoleRequest) returns (Empty) {}
|
||||
rpc CurrentUserRole(Empty) returns (stream UserRoleResponse) {}
|
||||
rpc RestoreClubOwnerDefaultSettings(Empty) returns (Empty) {}
|
||||
rpc GetClubCode(Empty) returns (GetClubCodeResponse) {}
|
||||
rpc SaveClubCode(SaveClubCodeRequest) returns (Empty) {}
|
||||
rpc GetUseVideoScreensaver(Empty) returns (GetUseVideoScreensaverResponse) {}
|
||||
rpc SaveUseVideoScreensaver(SaveUseVideoScreensaverRequest) returns (Empty) {}
|
||||
rpc SaveDefaultSystemUnits(SystemUnitsMessage) returns (Empty) {}
|
||||
rpc SaveDefaultLanguage(SaveDefaultLanguageRequest) returns (Empty) {}
|
||||
rpc GetCurrentUserRole(Empty) returns (UserRoleResponse) {}
|
||||
rpc SaveAdminEgymEnabledState(BooleanRequest) returns (Empty) {}
|
||||
rpc IsClub(Empty) returns (BooleanResponse) {}
|
||||
rpc IsClubUser(Empty) returns (BooleanResponse) {}
|
||||
rpc IsClubFreeUser(Empty) returns (BooleanResponse) {}
|
||||
rpc IsClubGuest(Empty) returns (BooleanResponse) {}
|
||||
rpc IsClubPremiumUser(Empty) returns (BooleanResponse) {}
|
||||
rpc IsEgymEnabled(IsEgymEnabledRequest) returns (BooleanResponse) {}
|
||||
}
|
||||
87
src/android/src/main/proto/compile_protos.bat
Normal file
87
src/android/src/main/proto/compile_protos.bat
Normal file
@@ -0,0 +1,87 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Percorso al tuo protoc specifico
|
||||
set PROTOC_EXE=C:\Users\violarob\Downloads\protoc-3.25.8-windows-x86_64.exe
|
||||
|
||||
REM Verifica che protoc esista
|
||||
if not exist "%PROTOC_EXE%" (
|
||||
echo ERRORE: protoc non trovato in: %PROTOC_EXE%
|
||||
echo Verifica che il file esista e il percorso sia corretto.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Directory di output
|
||||
set OUTPUT_DIR=..\java
|
||||
|
||||
REM Crea directory di output
|
||||
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
|
||||
|
||||
echo ===============================
|
||||
echo COMPILAZIONE PROTOBUF
|
||||
echo ===============================
|
||||
echo Protoc: %PROTOC_EXE%
|
||||
echo Directory corrente: %CD%
|
||||
echo Output in: %OUTPUT_DIR%
|
||||
echo.
|
||||
|
||||
REM Verifica versione protoc
|
||||
echo Versione protoc:
|
||||
"%PROTOC_EXE%" --version
|
||||
echo.
|
||||
|
||||
REM Contatori
|
||||
set /a success_count=0
|
||||
set /a error_count=0
|
||||
|
||||
REM Compila file .proto nella directory corrente
|
||||
for %%f in (*.proto) do (
|
||||
echo [INFO] Compilando: %%f
|
||||
"%PROTOC_EXE%" --java_out=lite:"%OUTPUT_DIR%" --proto_path=. "%%f"
|
||||
if errorlevel 1 (
|
||||
echo [ERRORE] Fallito: %%f
|
||||
set /a error_count+=1
|
||||
) else (
|
||||
echo [OK] Successo: %%f
|
||||
set /a success_count+=1
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
REM Compila file .proto nelle sottocartelle
|
||||
for /d %%d in (*) do (
|
||||
if exist "%%d\*.proto" (
|
||||
echo [INFO] Sottocartella trovata: %%d
|
||||
for %%f in (%%d\*.proto) do (
|
||||
echo [INFO] Compilando: %%f
|
||||
"%PROTOC_EXE%" --java_out=lite:"%OUTPUT_DIR%" --proto_path=. "%%f"
|
||||
if errorlevel 1 (
|
||||
echo [ERRORE] Fallito: %%f
|
||||
set /a error_count+=1
|
||||
) else (
|
||||
echo [OK] Successo: %%f
|
||||
set /a success_count+=1
|
||||
)
|
||||
)
|
||||
echo.
|
||||
)
|
||||
)
|
||||
|
||||
REM Riepilogo finale
|
||||
echo ===============================
|
||||
echo RIEPILOGO COMPILAZIONE:
|
||||
echo File compilati con successo: %success_count%
|
||||
echo File con errori: %error_count%
|
||||
echo Directory output: %OUTPUT_DIR%
|
||||
echo ===============================
|
||||
|
||||
if %error_count% gtr 0 (
|
||||
echo ATTENZIONE: Compilazione completata con %error_count% errori!
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
echo SUCCESSO: Tutti i file compilati correttamente!
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
17
src/android/src/main/proto/console/ConsoleError.proto
Normal file
17
src/android/src/main/proto/console/ConsoleError.proto
Normal file
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console";
|
||||
option java_outer_classname = "ConsoleErrorCodeProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum ConsoleErrorCode {
|
||||
FITNESS_VALUE_UNSUPPORTED = 0;
|
||||
VIRTUAL_CONSOLE_REQUIRED = 1;
|
||||
NO_VALUE_SET = 2;
|
||||
}
|
||||
|
||||
message ConsoleError {
|
||||
ConsoleErrorCode errorCode = 1;
|
||||
string message = 2;
|
||||
}
|
||||
62
src/android/src/main/proto/console/ConsoleInfo.proto
Normal file
62
src/android/src/main/proto/console/ConsoleInfo.proto
Normal file
@@ -0,0 +1,62 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "console/ConsoleType.proto";
|
||||
import "settings/SystemUnitsService.proto";
|
||||
option java_package = "com.ifit.glassos.console";
|
||||
option java_outer_classname = "ConsoleInfoProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ConsoleInfo {
|
||||
int32 modelNumber = 1;
|
||||
int32 partNumber = 2;
|
||||
int32 softwareVersion = 3;
|
||||
int32 hardwareVersion = 4;
|
||||
string firmwareVersion = 5;
|
||||
int32 serialNumber = 6;
|
||||
ConsoleType machineType = 7;
|
||||
string name = 8;
|
||||
string brainboardSerialNumber = 9;
|
||||
int32 masterLibraryVersion = 10;
|
||||
int32 masterLibraryBuild = 11;
|
||||
SystemUnits systemUnits = 12;
|
||||
double maxKph = 13;
|
||||
double minKph = 14;
|
||||
double maxInclinePercent = 15;
|
||||
double minInclinePercent = 16;
|
||||
double minResistance = 17;
|
||||
double maxResistance = 18;
|
||||
int32 minGear = 19;
|
||||
int32 maxGear = 20;
|
||||
double maxWeightKg = 21;
|
||||
bool canSetSpeed = 22;
|
||||
bool canSetIncline = 23;
|
||||
bool canSetResistance = 24;
|
||||
bool canSetGear = 25;
|
||||
bool canSetActivationLock = 26;
|
||||
bool supportsVerticalGain = 27;
|
||||
bool supportsVerticalNet = 28;
|
||||
bool supportsStartRequested = 29;
|
||||
bool supportsRequireStartRequested = 30;
|
||||
bool supportsKeyPressObserved = 31;
|
||||
bool supportsPulse = 32;
|
||||
double totalTimeSeconds = 33;
|
||||
double warmUpTimeoutSeconds = 34;
|
||||
double coolDownTimeoutSeconds = 35;
|
||||
double pauseTimeoutSeconds = 36;
|
||||
double totalDistanceKm = 37;
|
||||
bool isClubUnit = 38;
|
||||
double weightKg = 39;
|
||||
bool supportsConstantWatts = 40;
|
||||
string antPlusBootloaderVersion = 41;
|
||||
string antPlusSerialNumber = 42;
|
||||
string antPlusDeviceNumber = 43;
|
||||
string antPlusRelaySoftwareVersion = 44;
|
||||
string productSerialNumber = 45;
|
||||
string controller1SoftwareVersion = 46;
|
||||
string controller1SoftwarePartNumber = 47;
|
||||
string controller4SoftwareVersion = 48;
|
||||
string controller4SoftwarePartNumber = 49;
|
||||
string controller40SoftwareVersion = 50;
|
||||
string controller40SoftwarePartNumber = 51;
|
||||
}
|
||||
35
src/android/src/main/proto/console/ConsoleService.proto
Normal file
35
src/android/src/main/proto/console/ConsoleService.proto
Normal file
@@ -0,0 +1,35 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
import "util/Util.proto";
|
||||
import "console/ConsoleState.proto";
|
||||
import "console/ConsoleInfo.proto";
|
||||
option java_package = "com.ifit.glassos.console";
|
||||
option java_outer_classname = "ConsoleServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message ConnectionResult {
|
||||
oneof errorOrState {
|
||||
IFitError error = 1;
|
||||
ConsoleState consoleState = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ConsoleStateMessage {
|
||||
ConsoleState consoleState = 1;
|
||||
}
|
||||
|
||||
service ConsoleService {
|
||||
rpc Connect(Empty) returns (ConnectionResult) {}
|
||||
rpc Disconnect(Empty) returns (Empty) {}
|
||||
|
||||
rpc GetConsole(Empty) returns (ConsoleInfo) {}
|
||||
rpc ConsoleChanged(Empty) returns (stream ConsoleInfo) {}
|
||||
|
||||
rpc GetConsoleState(Empty) returns (ConsoleStateMessage) {}
|
||||
rpc ConsoleStateChanged(Empty) returns (stream ConsoleStateMessage) {}
|
||||
|
||||
rpc GetKnownConsoleInfo(Empty) returns (ConsoleInfo) {}
|
||||
rpc RefreshKnownConsoleInfo(Empty) returns (ConsoleInfo) {}
|
||||
}
|
||||
23
src/android/src/main/proto/console/ConsoleState.proto
Normal file
23
src/android/src/main/proto/console/ConsoleState.proto
Normal file
@@ -0,0 +1,23 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console";
|
||||
option java_outer_classname = "ConsoleStateProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum ConsoleState {
|
||||
DISCONNECTED = 0;
|
||||
CONSOLE_STATE_UNKNOWN = 1;
|
||||
IDLE = 2;
|
||||
WORKOUT = 3;
|
||||
PAUSED = 4;
|
||||
WORKOUT_RESULTS = 5;
|
||||
SAFETY_KEY_REMOVED = 6;
|
||||
WARM_UP = 7;
|
||||
COOL_DOWN = 8;
|
||||
RESUME = 9;
|
||||
LOCKED = 10;
|
||||
DEMO = 11;
|
||||
SLEEP = 12;
|
||||
ERROR = 13;
|
||||
}
|
||||
22
src/android/src/main/proto/console/ConsoleType.proto
Normal file
22
src/android/src/main/proto/console/ConsoleType.proto
Normal file
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console";
|
||||
option java_outer_classname = "ConsoleTypeProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum ConsoleType {
|
||||
CONSOLE_TYPE_UNKNOWN = 0;
|
||||
TREADMILL = 1;
|
||||
INCLINE_TRAINER = 2;
|
||||
ELLIPTICAL = 3;
|
||||
BIKE = 4;
|
||||
STRIDER = 5;
|
||||
FREE_STRIDER = 6;
|
||||
VERTICAL_ELLIPTICAL = 7;
|
||||
SPIN_BIKE = 8;
|
||||
ROWER = 9;
|
||||
EQUIPMENTLESS = 10;
|
||||
MIRROR = 11;
|
||||
VIBRATION = 12;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.calibration";
|
||||
option java_outer_classname = "InclineCalibrationProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
enum CalibrateInclineState {
|
||||
CALIBRATE_INCLINE_STATE_DONE = 0;
|
||||
CALIBRATE_INCLINE_STATE_FAILED = 1;
|
||||
CALIBRATE_INCLINE_STATE_IN_PROGRESS = 2;
|
||||
CALIBRATE_INCLINE_STATE_WAITING = 3;
|
||||
}
|
||||
|
||||
message InclineCalibrationStateResult {
|
||||
CalibrateInclineState state = 1;
|
||||
}
|
||||
|
||||
message InclineCalibrationStartedResult {
|
||||
oneof errorOrBool {
|
||||
IFitError error = 1;
|
||||
bool calibrationStarted = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.calibration";
|
||||
option java_outer_classname = "InclineCalibrationServiceProto";
|
||||
option java_multiple_files = true;
|
||||
import "console/calibration/InclineCalibration.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service InclineCalibrationService {
|
||||
rpc CalibrateIncline(Empty) returns (Empty) {}
|
||||
rpc InclineCalibrationStateChanged(Empty) returns (stream InclineCalibrationStateResult) {}
|
||||
rpc InclineCalibrationStartedChanged(Empty) returns (stream InclineCalibrationStartedResult) {}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.calibration";
|
||||
option java_outer_classname = "ThrottleCalibrationProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
enum ThrottleCalibrationState {
|
||||
CALIBRATE_THROTTLE_STATE_IDLE = 0;
|
||||
CALIBRATE_THROTTLE_STATE_FAILED = 1;
|
||||
CALIBRATE_THROTTLE_STATE_WAITING_FOR_NEUTRAL = 2;
|
||||
CALIBRATE_THROTTLE_STATE_WAITING_FOR_GRADE_FORWARD = 3;
|
||||
CALIBRATE_THROTTLE_STATE_WAITING_FOR_GRADE_BACKWARD = 4;
|
||||
CALIBRATE_THROTTLE_STATE_WAITING_FOR_SPEED_FORWARD = 5;
|
||||
CALIBRATE_THROTTLE_STATE_WAITING_FOR_SPEED_BACKWARD = 6;
|
||||
CALIBRATE_THROTTLE_STATE_DONE = 7;
|
||||
}
|
||||
|
||||
message ThrottleCalibrationValues {
|
||||
int32 rawGradeReading = 1;
|
||||
int32 rawSpeedReading = 2;
|
||||
int32 gradeTopThreshold = 3;
|
||||
int32 gradeHighThreshold = 4;
|
||||
int32 gradeLowThreshold = 5;
|
||||
int32 gradeBottomThreshold = 6;
|
||||
int32 gradeFilterConstant = 7;
|
||||
int32 speedTopThreshold = 8;
|
||||
int32 speedHighThreshold = 9;
|
||||
int32 speedLowThreshold = 10;
|
||||
int32 speedBottomThreshold = 11;
|
||||
int32 speedFilterConstant = 12;
|
||||
}
|
||||
|
||||
message ThrottleCalibrationStateResult {
|
||||
ThrottleCalibrationState state = 1;
|
||||
optional string errorMessage = 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.calibration";
|
||||
option java_outer_classname = "ThrottleCalibrationServiceProto";
|
||||
option java_multiple_files = true;
|
||||
import "util/Util.proto";
|
||||
import "console/calibration/ThrottleCalibration.proto";
|
||||
|
||||
service ThrottleCalibrationService {
|
||||
rpc IsThrottleCalibrationAvailable(Empty) returns (AvailabilityResponse) {}
|
||||
|
||||
rpc CalibrateThrottles(Empty) returns (Empty) {}
|
||||
rpc ConfirmThrottleState(Empty) returns (Empty) {}
|
||||
rpc AbortCalibrateThrottles(Empty) returns (Empty) {}
|
||||
|
||||
rpc ThrottleCalibrationStateChanged(Empty) returns (stream ThrottleCalibrationStateResult) {}
|
||||
|
||||
rpc GetThrottleCalibrationValues(Empty) returns (ThrottleCalibrationValues) {}
|
||||
rpc ThrottleCalibrationValuesChanged(Empty) returns (stream ThrottleCalibrationValues) {}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.constantwatts";
|
||||
option java_outer_classname = "ConstantWattsServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/Util.proto";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
message ConstantWattsMessage {
|
||||
int32 watts = 1;
|
||||
}
|
||||
|
||||
enum ConstantWattsState {
|
||||
CONSTANT_WATTS_STATE_DISABLED = 0;
|
||||
CONSTANT_WATTS_STATE_ENABLED = 1;
|
||||
CONSTANT_WATTS_STATE_PAUSED = 2;
|
||||
}
|
||||
|
||||
message ConstantWattsStateMessage {
|
||||
oneof errorOrState {
|
||||
IFitError error = 1;
|
||||
ConstantWattsState state = 2;
|
||||
}
|
||||
}
|
||||
|
||||
service ConstantWattsService {
|
||||
rpc CanRead(Empty) returns (AvailabilityResponse) {}
|
||||
rpc CanWrite(Empty) returns (AvailabilityResponse) {}
|
||||
rpc IsSupported(Empty) returns (AvailabilityResponse) {}
|
||||
rpc GetConstantWatts(Empty) returns (ConstantWattsMessage) {}
|
||||
rpc SetConstantWatts(ConstantWattsMessage) returns (AvailabilityResponse) {}
|
||||
rpc GetState(Empty) returns (ConstantWattsStateMessage){}
|
||||
rpc IsEquipmentSupported(Empty) returns (AvailabilityResponse){}
|
||||
rpc IsWorkoutSupported(Empty) returns (AvailabilityResponse){}
|
||||
rpc IsUserSupported(Empty) returns (AvailabilityResponse){}
|
||||
rpc Pause(Empty) returns (Empty){}
|
||||
rpc Resume(Empty) returns (Empty){}
|
||||
rpc Enable(Empty) returns (Empty){}
|
||||
rpc Disable(Empty) returns (Empty){}
|
||||
rpc Increment(Empty) returns (Empty){}
|
||||
rpc Decrement(Empty) returns (Empty){}
|
||||
rpc OnStateChanged(Empty) returns (stream ConstantWattsStateMessage) {}
|
||||
rpc ConstantWattsSubscription(Empty) returns (stream ConstantWattsMessage) {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.idlelockout";
|
||||
option java_outer_classname = "IdleModeLockoutProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
enum IdleModeLockoutState {
|
||||
LOCK_STATE_UNKNOWN = 0;
|
||||
LOCK_STATE_UNLOCKED = 1;
|
||||
LOCK_STATE_LOCKED = 2;
|
||||
}
|
||||
|
||||
message IdleModeLockoutMessage {
|
||||
IdleModeLockoutState state = 1;
|
||||
}
|
||||
|
||||
message IdleModeLockoutResult {
|
||||
oneof errorOrIdleModeLockoutState {
|
||||
IFitError error = 1;
|
||||
IdleModeLockoutState idleModeLockoutState = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.idlelockout";
|
||||
option java_outer_classname = "IdleModeLockoutServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "console/idlelockout/IdleModeLockout.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service IdleModeLockoutService {
|
||||
rpc CanRead(Empty) returns (AvailabilityResponse) {}
|
||||
rpc CanWrite(Empty) returns (AvailabilityResponse) {}
|
||||
rpc GetIdleModeLockout(Empty) returns (IdleModeLockoutResult) {}
|
||||
rpc SetIdleModeLockout(IdleModeLockoutMessage) returns (IdleModeLockoutResult) {}
|
||||
rpc IdleModeLockoutSubscription(Empty) returns (stream IdleModeLockoutMessage) {}
|
||||
}
|
||||
297
src/android/src/main/proto/console/keypress/KeyCode.proto
Normal file
297
src/android/src/main/proto/console/keypress/KeyCode.proto
Normal file
@@ -0,0 +1,297 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.keypress";
|
||||
option java_outer_classname = "KeyCodeProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum KeyCode {
|
||||
NO_KEY = 0;
|
||||
STOP = 1;
|
||||
START = 2;
|
||||
SPEED_UP = 3;
|
||||
SPEED_DOWN = 4;
|
||||
INCLINE_UP = 5;
|
||||
INCLINE_DOWN = 6;
|
||||
RESISTANCE_UP = 7;
|
||||
RESISTANCE_DOWN = 8;
|
||||
GEAR_UP = 9;
|
||||
GEAR_DOWN = 10;
|
||||
WEIGHT_UP = 11;
|
||||
WEIGHT_DOWN = 12;
|
||||
AGE_UP = 13;
|
||||
AGE_DOWN = 14;
|
||||
SPEED_RESUME = 15;
|
||||
INCLINE_RESUME = 16;
|
||||
BLE_KEY = 17;
|
||||
ON_RESET = 18;
|
||||
PRIORITY_DISPLAY = 19;
|
||||
BURN_RATE_UP = 20;
|
||||
BURN_RATE_DOWN = 21;
|
||||
RECOVERY = 22;
|
||||
WORK = 23;
|
||||
START_STOP = 24;
|
||||
POWER_ON_OFF = 25;
|
||||
FAN_UP = 50;
|
||||
FAN_DOWN = 51;
|
||||
FAN_OFF = 52;
|
||||
FAN_MANUAL = 53;
|
||||
FAN_AUTO = 54;
|
||||
FAN_1 = 55;
|
||||
FAN_2 = 56;
|
||||
FAN_3 = 57;
|
||||
FAN_4 = 58;
|
||||
FAN_5 = 59;
|
||||
PC_BACK = 100;
|
||||
PC_MENU = 101;
|
||||
PC_HOME = 102;
|
||||
KEYPAD = 103;
|
||||
DISPLAY = 104;
|
||||
ENTER = 105;
|
||||
UP = 106;
|
||||
DOWN = 107;
|
||||
LEFT = 108;
|
||||
RIGHT = 109;
|
||||
TV_POWER = 120;
|
||||
TV_CHANNEL_UP = 121;
|
||||
TV_CHANNEL_DOWN = 122;
|
||||
TV_RECALL = 123;
|
||||
TV_MENU = 124;
|
||||
TV_SOURCE = 125;
|
||||
TV_SEEK = 126;
|
||||
TV_CLOSE_CAPTION = 127;
|
||||
TV_VOLUME_UP = 128;
|
||||
TV_VOLUME_DOWN = 129;
|
||||
TV_MUTE = 130;
|
||||
RIGHT_GEAR_UP = 150;
|
||||
RIGHT_GEAR_DOWN = 151;
|
||||
LEFT_GEAR_UP = 152;
|
||||
LEFT_GEAR_DOWN = 153;
|
||||
AUDIO_VOLUME_UP = 200;
|
||||
AUDIO_VOLUME_DOWN = 201;
|
||||
AUDIO_MUTE = 202;
|
||||
AUDIO_EQUALIZER = 203;
|
||||
AUDIO_SOURCE = 204;
|
||||
NUMBER_PAD_0 = 300;
|
||||
NUMBER_PAD_1 = 301;
|
||||
NUMBER_PAD_2 = 302;
|
||||
NUMBER_PAD_3 = 303;
|
||||
NUMBER_PAD_4 = 304;
|
||||
NUMBER_PAD_5 = 305;
|
||||
NUMBER_PAD_6 = 306;
|
||||
NUMBER_PAD_7 = 307;
|
||||
NUMBER_PAD_8 = 308;
|
||||
NUMBER_PAD_9 = 309;
|
||||
NUMBER_PAD_STAR = 310;
|
||||
NUMBER_PAD_DOT = 311;
|
||||
NUMBER_PAD_HASH = 312;
|
||||
NUMBER_PAD_OK = 313;
|
||||
NUMBER_PAD_ENTER = 314;
|
||||
ERGOFIT_TILT_FORWARD = 400;
|
||||
ERGOFIT_TILT_BACK = 401;
|
||||
ERGOFIT_UPRIGHT_UP = 402;
|
||||
ERGOFIT_UPRIGHT_DOWN = 403;
|
||||
ERGOFIT_MEMORY = 404;
|
||||
ERGOFIT_USER_1 = 405;
|
||||
ERGOFIT_USER_2 = 406;
|
||||
ERGOFIT_USER_3 = 407;
|
||||
ERGOFIT_USER_4 = 408;
|
||||
SET_TO_SHIP = 500;
|
||||
DEBUG_MODE = 501;
|
||||
LOG_MODE = 502;
|
||||
SETTINGS = 503;
|
||||
INCLINE_DISPLAY = 600;
|
||||
PULSE_DISPLAY = 601;
|
||||
WATTS_DISPLAY = 602;
|
||||
SPEED_DISPLAY = 603;
|
||||
TIME_DISPLAY = 604;
|
||||
PACE_DISPLAY = 605;
|
||||
CALORIES_DISPLAY = 606;
|
||||
DISTANCE_DISPLAY = 607;
|
||||
SCAN_DISPLAY = 608;
|
||||
MPH_1 = 1000;
|
||||
MPH_2 = 1001;
|
||||
MPH_3 = 1002;
|
||||
MPH_4 = 1003;
|
||||
MPH_5 = 1004;
|
||||
MPH_6 = 1005;
|
||||
MPH_7 = 1006;
|
||||
MPH_8 = 1007;
|
||||
MPH_9 = 1008;
|
||||
MPH_10 = 1009;
|
||||
MPH_11 = 1010;
|
||||
MPH_12 = 1011;
|
||||
MPH_13 = 1012;
|
||||
MPH_14 = 1013;
|
||||
MPH_15 = 1014;
|
||||
KPH_1 = 1100;
|
||||
KPH_2 = 1101;
|
||||
KPH_3 = 1102;
|
||||
KPH_4 = 1103;
|
||||
KPH_5 = 1104;
|
||||
KPH_6 = 1105;
|
||||
KPH_7 = 1106;
|
||||
KPH_8 = 1107;
|
||||
KPH_9 = 1108;
|
||||
KPH_10 = 1109;
|
||||
KPH_11 = 1110;
|
||||
KPH_12 = 1111;
|
||||
KPH_13 = 1112;
|
||||
KPH_14 = 1113;
|
||||
KPH_15 = 1114;
|
||||
KPH_16 = 1115;
|
||||
KPH_17 = 1116;
|
||||
KPH_18 = 1117;
|
||||
KPH_19 = 1118;
|
||||
KPH_20 = 1119;
|
||||
KPH_21 = 1120;
|
||||
KPH_22 = 1121;
|
||||
KPH_23 = 1122;
|
||||
KPH_24 = 1123;
|
||||
INCLINE_NEG_30 = 1200;
|
||||
INCLINE_NEG_29 = 1201;
|
||||
INCLINE_NEG_28 = 1202;
|
||||
INCLINE_NEG_27 = 1203;
|
||||
INCLINE_NEG_26 = 1204;
|
||||
INCLINE_NEG_25 = 1205;
|
||||
INCLINE_NEG_24 = 1206;
|
||||
INCLINE_NEG_23 = 1207;
|
||||
INCLINE_NEG_22 = 1208;
|
||||
INCLINE_NEG_21 = 1209;
|
||||
INCLINE_NEG_20 = 1210;
|
||||
INCLINE_NEG_19 = 1211;
|
||||
INCLINE_NEG_18 = 1212;
|
||||
INCLINE_NEG_17 = 1213;
|
||||
INCLINE_NEG_16 = 1214;
|
||||
INCLINE_NEG_15 = 1215;
|
||||
INCLINE_NEG_14 = 1216;
|
||||
INCLINE_NEG_13 = 1217;
|
||||
INCLINE_NEG_12 = 1218;
|
||||
INCLINE_NEG_11 = 1219;
|
||||
INCLINE_NEG_10 = 1220;
|
||||
INCLINE_NEG_9 = 1221;
|
||||
INCLINE_NEG_8 = 1222;
|
||||
INCLINE_NEG_7 = 1223;
|
||||
INCLINE_NEG_6 = 1224;
|
||||
INCLINE_NEG_5 = 1225;
|
||||
INCLINE_NEG_4 = 1226;
|
||||
INCLINE_NEG_3 = 1227;
|
||||
INCLINE_NEG_2 = 1228;
|
||||
INCLINE_NEG_1 = 1229;
|
||||
INCLINE_0 = 1230;
|
||||
INCLINE_1 = 1231;
|
||||
INCLINE_2 = 1232;
|
||||
INCLINE_3 = 1233;
|
||||
INCLINE_4 = 1234;
|
||||
INCLINE_5 = 1235;
|
||||
INCLINE_6 = 1236;
|
||||
INCLINE_7 = 1237;
|
||||
INCLINE_8 = 1238;
|
||||
INCLINE_9 = 1239;
|
||||
INCLINE_10 = 1240;
|
||||
INCLINE_11 = 1241;
|
||||
INCLINE_12 = 1242;
|
||||
INCLINE_13 = 1243;
|
||||
INCLINE_14 = 1244;
|
||||
INCLINE_15 = 1245;
|
||||
INCLINE_16 = 1246;
|
||||
INCLINE_17 = 1247;
|
||||
INCLINE_18 = 1248;
|
||||
INCLINE_19 = 1249;
|
||||
INCLINE_20 = 1250;
|
||||
INCLINE_21 = 1251;
|
||||
INCLINE_22 = 1252;
|
||||
INCLINE_23 = 1253;
|
||||
INCLINE_24 = 1254;
|
||||
INCLINE_25 = 1255;
|
||||
INCLINE_26 = 1256;
|
||||
INCLINE_27 = 1257;
|
||||
INCLINE_28 = 1258;
|
||||
INCLINE_29 = 1259;
|
||||
INCLINE_30 = 1260;
|
||||
INCLINE_31 = 1261;
|
||||
INCLINE_32 = 1262;
|
||||
INCLINE_33 = 1263;
|
||||
INCLINE_34 = 1264;
|
||||
INCLINE_35 = 1265;
|
||||
INCLINE_36 = 1266;
|
||||
INCLINE_37 = 1267;
|
||||
INCLINE_38 = 1268;
|
||||
INCLINE_39 = 1269;
|
||||
INCLINE_40 = 1270;
|
||||
INCLINE_41 = 1271;
|
||||
INCLINE_42 = 1272;
|
||||
INCLINE_43 = 1273;
|
||||
INCLINE_44 = 1274;
|
||||
INCLINE_45 = 1275;
|
||||
INCLINE_46 = 1276;
|
||||
INCLINE_47 = 1277;
|
||||
INCLINE_48 = 1278;
|
||||
INCLINE_49 = 1279;
|
||||
INCLINE_50 = 1280;
|
||||
RESISTANCE_0 = 1300;
|
||||
RESISTANCE_1 = 1301;
|
||||
RESISTANCE_2 = 1302;
|
||||
RESISTANCE_3 = 1303;
|
||||
RESISTANCE_4 = 1304;
|
||||
RESISTANCE_5 = 1305;
|
||||
RESISTANCE_6 = 1306;
|
||||
RESISTANCE_7 = 1307;
|
||||
RESISTANCE_8 = 1308;
|
||||
RESISTANCE_9 = 1309;
|
||||
RESISTANCE_10 = 1310;
|
||||
RESISTANCE_11 = 1311;
|
||||
RESISTANCE_12 = 1312;
|
||||
RESISTANCE_13 = 1313;
|
||||
RESISTANCE_14 = 1314;
|
||||
RESISTANCE_15 = 1315;
|
||||
RESISTANCE_16 = 1316;
|
||||
RESISTANCE_17 = 1317;
|
||||
RESISTANCE_18 = 1318;
|
||||
RESISTANCE_19 = 1319;
|
||||
RESISTANCE_20 = 1320;
|
||||
RESISTANCE_21 = 1321;
|
||||
RESISTANCE_22 = 1322;
|
||||
RESISTANCE_23 = 1323;
|
||||
RESISTANCE_24 = 1324;
|
||||
RESISTANCE_25 = 1325;
|
||||
RESISTANCE_26 = 1326;
|
||||
RESISTANCE_27 = 1327;
|
||||
RESISTANCE_28 = 1328;
|
||||
RESISTANCE_29 = 1329;
|
||||
RESISTANCE_30 = 1330;
|
||||
MANUAL_WORKOUT = 11000;
|
||||
MAP_WORKOUT = 11001;
|
||||
TRAIN_WORKOUT = 11002;
|
||||
COMPETE_WORKOUT = 11003;
|
||||
TRACK_WORKOUT = 11004;
|
||||
SET_A_GOAL_WORKOUT = 11005;
|
||||
VIDEO_WORKOUT = 11006;
|
||||
LOSE_WT_WORKOUT = 11007;
|
||||
CALORIES_WORKOUT = 11008;
|
||||
INTENSITY_WORKOUT = 11009;
|
||||
INCLINE_WORKOUT = 11010;
|
||||
SPEED_WORKOUT = 11011;
|
||||
PULSE_WORKOUT = 11012;
|
||||
PERFORMANCE_WORKOUT = 11013;
|
||||
DAY_WORKOUT = 11014;
|
||||
WEEK_WORKOUT = 11015;
|
||||
MONTH_WORKOUT = 11016;
|
||||
INTERVAL_WORKOUT = 11017;
|
||||
TEMP_WORKOUT = 11018;
|
||||
DUMMY_WORKOUT_1 = 11100;
|
||||
DUMMY_WORKOUT_2 = 11101;
|
||||
DUMMY_WORKOUT_3 = 11102;
|
||||
DUMMY_WORKOUT_4 = 11103;
|
||||
DUMMY_WORKOUT_5 = 11104;
|
||||
DUMMY_WORKOUT_6 = 11105;
|
||||
DUMMY_WORKOUT_7 = 11106;
|
||||
DUMMY_WORKOUT_8 = 11107;
|
||||
DUMMY_WORKOUT_9 = 11108;
|
||||
DUMMY_WORKOUT_10 = 11109;
|
||||
CALORIES_WORKOUT_0 = 12000;
|
||||
CALORIES_WORKOUT_999 = 12999;
|
||||
TIME_WORKOUT_0 = 13000;
|
||||
TIME_WORKOUT_99 = 13099;
|
||||
DUMMY = 9999;
|
||||
}
|
||||
21
src/android/src/main/proto/console/keypress/KeyPress.proto
Normal file
21
src/android/src/main/proto/console/keypress/KeyPress.proto
Normal file
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.keypress";
|
||||
option java_outer_classname = "KeyPressProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "console/keypress/KeyCode.proto";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
message KeyPress {
|
||||
KeyCode code = 1;
|
||||
int32 timePressed = 2;
|
||||
int32 durationHeld = 3;
|
||||
}
|
||||
|
||||
message KeyPressResult {
|
||||
oneof errorOrKeyPress {
|
||||
IFitError error = 1;
|
||||
KeyPress keyPress = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.keypress";
|
||||
option java_outer_classname = "KeyPressServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "console/keypress/KeyPress.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service KeyPressService {
|
||||
rpc CanRead(Empty) returns (AvailabilityResponse) {}
|
||||
rpc CanWriteVirtual(Empty) returns (AvailabilityResponse) {}
|
||||
rpc GetKeyPress(Empty) returns (KeyPressResult) {}
|
||||
rpc KeyPressSubscription(Empty) returns (stream KeyPress) {}
|
||||
rpc SetVirtualKeyPress(KeyPress) returns (KeyPressResult) {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/Util.proto";
|
||||
option java_package = "com.ifit.glassos.console.proximity";
|
||||
option java_outer_classname = "ProximitySensingServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
service ProximitySensingService {
|
||||
|
||||
// check whether Movement Detection (PIR) is Readable
|
||||
rpc CanReadMovementDetect(Empty) returns (AvailabilityResponse) {}
|
||||
|
||||
// check whether User Distance (LIDAR) is Readable
|
||||
rpc CanReadUserDistance(Empty) returns (AvailabilityResponse) {}
|
||||
|
||||
// get the current Movement Detection (PIR) state
|
||||
rpc GetMovementDetect(Empty) returns (BooleanResponse) {}
|
||||
|
||||
// subscribe to Movement Detection (PIR) updates
|
||||
rpc MovementDetectSubscription(Empty) returns (stream BooleanResponse) {}
|
||||
|
||||
// get the current User Distance (LIDAR) in centimeters
|
||||
rpc GetUserDistance(Empty) returns (FloatResponse) {}
|
||||
|
||||
// subscribe to User Distance (LIDAR) updates
|
||||
rpc UserDistanceSubscription(Empty) returns (stream FloatResponse) {}
|
||||
}
|
||||
25
src/android/src/main/proto/console/sleep/SleepState.proto
Normal file
25
src/android/src/main/proto/console/sleep/SleepState.proto
Normal file
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.sleep";
|
||||
option java_outer_classname = "SleepStateProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
enum SleepState {
|
||||
SLEEP_STATE_UNKNOWN = 0;
|
||||
SLEEP_STATE_AWAKE = 1;
|
||||
SLEEP_STATE_INITIATE_SLEEP = 2;
|
||||
SLEEP_STATE_SLEEPING = 3;
|
||||
}
|
||||
|
||||
message SleepStateMessage {
|
||||
SleepState state = 1;
|
||||
}
|
||||
|
||||
message SleepStateResult {
|
||||
oneof errorOrSleepState {
|
||||
IFitError error = 1;
|
||||
SleepState sleepState = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.sleep";
|
||||
option java_outer_classname = "SleepStateServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "console/sleep/SleepState.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service SleepStateService {
|
||||
rpc CanRead(Empty) returns (AvailabilityResponse) {}
|
||||
rpc CanWrite(Empty) returns (AvailabilityResponse) {}
|
||||
rpc GetSleepState(Empty) returns (SleepStateResult) {}
|
||||
rpc SetSleepState(SleepStateMessage) returns (SleepStateResult) {}
|
||||
rpc SleepStateSubscription(Empty) returns (stream SleepStateMessage) {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "console/spoofing/SpoofPartNumberResult.proto";
|
||||
import "util/Util.proto";
|
||||
option java_package = "com.ifit.glassos.console.spoofing";
|
||||
option java_outer_classname = "ConsoleSpoofingServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
service ConsoleSpoofingService {
|
||||
rpc SetSpoofedPartNumber(IntRequest) returns (SpoofPartNumberResult) {}
|
||||
rpc GetSpoofedPartNumber(Empty) returns (SpoofPartNumberResult) {}
|
||||
rpc ClearSpoofedPartNumber(Empty) returns (SpoofPartNumberResult) {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
option java_package = "com.ifit.glassos.console.spoofing";
|
||||
option java_outer_classname = "SpoofPartNumberResultProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message SpoofPartNumberResult {
|
||||
oneof errorOrPartNumber {
|
||||
IFitError error = 1;
|
||||
int32 partNumber = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
option java_package = "com.ifit.glassos.console.tdf";
|
||||
option java_outer_classname = "TDFChainRingConfigProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum TDFChainRingConfig {
|
||||
INVALID_CHAIN_RING = 0;
|
||||
COMPACT_34_50 = 1;
|
||||
SUB_COMPACT_36_52 = 2;
|
||||
STANDARD_39_53 = 3;
|
||||
TRIPLE_30_39_53 = 4;
|
||||
}
|
||||
|
||||
message TDFChainRingConfigList {
|
||||
repeated TDFChainRingConfig chainRingConfigs = 1;
|
||||
}
|
||||
|
||||
message TDFChainRingConfigsResult {
|
||||
oneof errorOrChainRingConfigs {
|
||||
IFitError error = 1;
|
||||
TDFChainRingConfigList chainRingConfigs = 2;
|
||||
}
|
||||
}
|
||||
19
src/android/src/main/proto/console/tdf/TDFGear.proto
Normal file
19
src/android/src/main/proto/console/tdf/TDFGear.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
option java_package = "com.ifit.glassos.console.tdf";
|
||||
option java_outer_classname = "TDFGearProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message TDFGear {
|
||||
int32 frontGear = 1;
|
||||
int32 rearGear = 2;
|
||||
}
|
||||
|
||||
message TDFGearResult {
|
||||
oneof errorOrGear {
|
||||
IFitError error = 1;
|
||||
TDFGear gear = 2;
|
||||
}
|
||||
}
|
||||
21
src/android/src/main/proto/console/tdf/TDFGearConfig.proto
Normal file
21
src/android/src/main/proto/console/tdf/TDFGearConfig.proto
Normal file
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "console/tdf/TDFChainRingConfig.proto";
|
||||
import "console/tdf/TDFRearCassetteConfig.proto";
|
||||
import "util/IFitError.proto";
|
||||
option java_package = "com.ifit.glassos.console.tdf";
|
||||
option java_outer_classname = "TDFGearConfigProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message TDFGearConfig {
|
||||
TDFChainRingConfig frontGearConfig = 1;
|
||||
TDFRearCassetteConfig rearGearConfig = 2;
|
||||
}
|
||||
|
||||
message TDFGearConfigResult {
|
||||
oneof errorOrGearConfig {
|
||||
IFitError error = 1;
|
||||
TDFGearConfig gearConfig = 2;
|
||||
}
|
||||
}
|
||||
29
src/android/src/main/proto/console/tdf/TDFGearService.proto
Normal file
29
src/android/src/main/proto/console/tdf/TDFGearService.proto
Normal file
@@ -0,0 +1,29 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "console/tdf/TDFChainRingConfig.proto";
|
||||
import "console/tdf/TDFGearConfig.proto";
|
||||
import "console/tdf/TDFGear.proto";
|
||||
import "console/tdf/TDFRearCassetteConfig.proto";
|
||||
import "util/Util.proto";
|
||||
option java_package = "com.ifit.glassos.console.tdf";
|
||||
option java_outer_classname = "TDFGearServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
service TDFGearService {
|
||||
rpc IsSupported(Empty) returns (BooleanResponse) {}
|
||||
rpc ResetGearConfig(Empty) returns (TDFGearConfigResult) {}
|
||||
rpc ResetGearAndGearConfig(Empty) returns (BooleanResponse) {}
|
||||
rpc SetGearConfig(TDFGearConfig) returns (TDFGearConfigResult) {}
|
||||
rpc GetGearConfig(Empty) returns (TDFGearConfigResult) {}
|
||||
rpc ListFrontGearConfigs(Empty) returns (TDFChainRingConfigsResult) {}
|
||||
rpc ListRearGearConfigs(Empty) returns (TDFRearCassetteConfigsResult) {}
|
||||
rpc GearConfigChangedSubscription(Empty) returns (stream TDFGearConfig) {}
|
||||
|
||||
rpc SetGear(TDFGear) returns (TDFGearResult) {}
|
||||
rpc GetCurrentGear(Empty) returns (TDFGearResult) {}
|
||||
rpc GearChangedSubscription(Empty) returns (stream TDFGear) {}
|
||||
|
||||
rpc GetGearRatio(Empty) returns (FloatResponse) {}
|
||||
rpc GearRatioChangedSubscription(Empty) returns (stream FloatResponse) {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "util/IFitError.proto";
|
||||
option java_package = "com.ifit.glassos.console.tdf";
|
||||
option java_outer_classname = "TDFRearCassetteConfigProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
message TDFRearCassetteConfig {
|
||||
int32 minTeeth = 1;
|
||||
int32 maxTeeth = 2;
|
||||
int32 speeds = 3;
|
||||
repeated int32 teethAtGear = 4;
|
||||
}
|
||||
|
||||
message TDFRearCassetteConfigList {
|
||||
repeated TDFRearCassetteConfig cassetteConfigs = 1;
|
||||
}
|
||||
|
||||
message TDFRearCassetteConfigsResult {
|
||||
oneof errorOrCassetteConfigs {
|
||||
IFitError error = 1;
|
||||
TDFRearCassetteConfigList cassetteConfigs = 2;
|
||||
}
|
||||
}
|
||||
14
src/android/src/main/proto/console/update/FirmwareType.proto
Normal file
14
src/android/src/main/proto/console/update/FirmwareType.proto
Normal file
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.update";
|
||||
option java_outer_classname = "FirmwareTypeProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum FirmwareType {
|
||||
FIRMWARE_TYPE_UNKNOWN = 0;
|
||||
FIRMWARE_TYPE_BRAINBOARD = 1;
|
||||
FIRMWARE_TYPE_ANT_PLUS_APPLICATION = 2;
|
||||
FIRMWARE_TYPE_ANT_PLUS_BOOTLOADER = 3;
|
||||
FIRMWARE_TYPE_MOTOR_CONTROLLER = 4;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.update";
|
||||
option java_outer_classname = "FirmwareUpdateFileProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
import "console/update/FirmwareType.proto";
|
||||
|
||||
message FirmwareUpdateFile {
|
||||
FirmwareType updateType = 1;
|
||||
string filePath = 2;
|
||||
string fileName = 3;
|
||||
string version = 4;
|
||||
int32 partNumber = 5;
|
||||
bool forceUpdate = 6;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.update";
|
||||
option java_outer_classname = "FirmwareUpdateServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
import "console/update/FirmwareUpdateFile.proto";
|
||||
import "console/update/FirmwareUpdateState.proto";
|
||||
import "console/update/FirmwareUpdateStatus.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service FirmwareUpdateService {
|
||||
rpc GetFirmwareUpdateStatus(Empty) returns (FirmwareUpdateStatus) {}
|
||||
rpc FirmwareUpdateStatusChangedSubscription(Empty) returns (stream FirmwareUpdateStatus) {}
|
||||
rpc StartFirmwareUpdate(FirmwareUpdateFile) returns (FirmwareUpdateStatus) {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.update";
|
||||
option java_outer_classname = "FirmwareUpdateStateProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum FirmwareUpdateState {
|
||||
FIRMWARE_UPDATE_STATE_UNKNOWN = 0;
|
||||
FIRMWARE_UPDATE_STATE_IDLE = 1;
|
||||
FIRMWARE_UPDATE_STATE_PREPARING = 2;
|
||||
FIRMWARE_UPDATE_STATE_UPDATING = 3;
|
||||
FIRMWARE_UPDATE_STATE_VERIFYING = 4;
|
||||
FIRMWARE_UPDATE_STATE_SUCCESSFUL = 5;
|
||||
FIRMWARE_UPDATE_STATE_FAILED = 6;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.update";
|
||||
option java_outer_classname = "FirmwareUpdateStatusProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "console/update/FirmwareUpdateFile.proto";
|
||||
import "console/update/FirmwareUpdateState.proto";
|
||||
|
||||
message FirmwareUpdateStatus {
|
||||
FirmwareUpdateState state = 1;
|
||||
string updateSessionId = 2;
|
||||
FirmwareUpdateFile updateFile = 3;
|
||||
google.protobuf.Timestamp startTime = 4;
|
||||
google.protobuf.Timestamp endTime = 5;
|
||||
string resultMessage = 6;
|
||||
float percentComplete = 7;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.useractivity";
|
||||
option java_outer_classname = "UserActivityProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
|
||||
message DurationResult {
|
||||
google.protobuf.Duration duration = 1;
|
||||
}
|
||||
|
||||
message UserActivityOverrideMessage {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message SetDurationRequest {
|
||||
google.protobuf.Duration duration = 1;
|
||||
}
|
||||
|
||||
message UserActivityServiceResult {
|
||||
oneof errorOrSuccess {
|
||||
IFitError error = 1;
|
||||
bool success = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.useractivity";
|
||||
option java_outer_classname = "UserActivityServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "console/useractivity/UserActivity.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service UserActivityService {
|
||||
rpc DurationSinceLastScreenTap(Empty) returns (stream DurationResult) {}
|
||||
rpc StartUserActivityOverride(Empty) returns (UserActivityOverrideMessage) {}
|
||||
rpc CompleteUserActivityOverride(UserActivityOverrideMessage) returns (UserActivityServiceResult) {}
|
||||
rpc SetDurationSinceLastScreenTap(SetDurationRequest) returns (UserActivityServiceResult) {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
option java_package = "com.ifit.glassos.console.virtualdmk";
|
||||
option java_outer_classname = "VirtualDMKServiceProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
import "util/IFitError.proto";
|
||||
import "util/Util.proto";
|
||||
|
||||
service VirtualDMKService {
|
||||
rpc GetDMKOverride(Empty) returns (BooleanResponse) {}
|
||||
rpc SetDMKOverride(BooleanRequest) returns (IFitError) {}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
syntax = "proto3";
|
||||
package com.ifit.glassos;
|
||||
import "user/UserTier.proto";
|
||||
import "console/ConsoleType.proto";
|
||||
import "club/ClubSettingsService.proto";
|
||||
option java_package = "com.ifit.glassos.featuregates";
|
||||
option java_outer_classname = "FeatureGateFacetProto";
|
||||
option java_multiple_files = true;
|
||||
option swift_prefix = "IFit";
|
||||
|
||||
enum FeatureGateFacet {
|
||||
FEATURE_FACET_UNKNOWN = 0;
|
||||
FEATURE_FACET_CHINA = 1;
|
||||
FEATURE_FACET_CLUB_CONSOLE = 2;
|
||||
FEATURE_FACET_CLUB_USER_ROLE = 3;
|
||||
FEATURE_FACET_DEMO_MODE = 4;
|
||||
FEATURE_FACET_ENTIRE_FEATURE = 5;
|
||||
FEATURE_FACET_MOBILE = 6;
|
||||
FEATURE_FACET_MOBILE_FORM_FACTOR = 7;
|
||||
FEATURE_FACET_MODALITY = 8;
|
||||
FEATURE_FACET_SOFTWARE_NUMBER = 9;
|
||||
FEATURE_FACET_USER_TIER = 10;
|
||||
}
|
||||
|
||||
message FacetMessage {
|
||||
FeatureGateFacet featureGateFacet = 1;
|
||||
oneof payload {
|
||||
BooleanFacetPayload booleanFacetPayload = 2;
|
||||
EnumeratedUserRoleFacetMessage enumeratedUserRoleFacetMessage = 3;
|
||||
EnumeratedStringFacetMessage enumeratedStringFacetMessage = 4;
|
||||
EnumeratedConsoleTypeFacetMessage enumeratedConsoleTypeFacetMessage = 5;
|
||||
EnumeratedIntFacetMessage enumeratedIntFacetMessage = 6;
|
||||
EnumeratedUserTierFacetMessage enumeratedUserTierFacetMessage = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message BooleanFacetPayload {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message EnumeratedUserRoleFacetMessage {
|
||||
repeated club.UserRole allowedValues = 1;
|
||||
repeated club.UserRole disallowedValues = 2;
|
||||
}
|
||||
|
||||
message EnumeratedStringFacetMessage {
|
||||
repeated string allowedValues = 1;
|
||||
repeated string disallowedValues = 2;
|
||||
}
|
||||
|
||||
message EnumeratedConsoleTypeFacetMessage {
|
||||
repeated ConsoleType allowedValues = 1;
|
||||
repeated ConsoleType disallowedValues = 2;
|
||||
}
|
||||
|
||||
message EnumeratedIntFacetMessage {
|
||||
repeated int32 allowedValues = 1;
|
||||
repeated int32 disallowedValues = 2;
|
||||
}
|
||||
|
||||
message EnumeratedUserTierFacetMessage {
|
||||
repeated UserTier allowedValues = 1;
|
||||
repeated UserTier disallowedValues = 2;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user