Compare commits
1246
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad74cf089 | ||
|
|
6073d37892 | ||
|
|
c04937553f | ||
|
|
e08bdfa3c7 | ||
|
|
d282678b19 | ||
|
|
9ff4243350 | ||
|
|
6c79b33752 | ||
|
|
861b5da7a3 | ||
|
|
7dd3d50653 | ||
|
|
ba8e04c641 | ||
|
|
cece245a55 | ||
|
|
fab4d2810e | ||
|
|
5bff3ca929 | ||
|
|
afe1d3b80e | ||
|
|
6c8005dba9 | ||
|
|
9f399ca6f3 | ||
|
|
a7574dd348 | ||
|
|
b39f3597c8 | ||
|
|
bb9a86d52e | ||
|
|
e5329e40c6 | ||
|
|
319611f69d | ||
|
|
4b16253c8c | ||
|
|
b60423fde6 | ||
|
|
5d3399c72e | ||
|
|
d247bafcbb | ||
|
|
5227aad4c6 | ||
|
|
da4c54a99e | ||
|
|
8d42bd93f7 | ||
|
|
37e467272b | ||
|
|
ed0b9cd5ae | ||
|
|
8045f799f0 | ||
|
|
ca6ea16f89 | ||
|
|
452614abf1 | ||
|
|
f63c797cd1 | ||
|
|
de791f6ea8 | ||
|
|
024f751ef1 | ||
|
|
d4c82bfd7d | ||
|
|
a49f19133d | ||
|
|
a94009514c | ||
|
|
d1d3724f52 | ||
|
|
9f4177aff9 | ||
|
|
e323fd76f8 | ||
|
|
b0d24d8502 | ||
|
|
8ae20c2aae | ||
|
|
89843514b8 | ||
|
|
1a9e39a49e | ||
|
|
f4d5e74c59 | ||
|
|
eb1301df99 | ||
|
|
2885fa91cc | ||
|
|
d89e4681db | ||
|
|
dedcd76241 | ||
|
|
e987dd07f9 | ||
|
|
6747433100 | ||
|
|
3ffd67918c | ||
|
|
3fe6556037 | ||
|
|
5ccc65241d | ||
|
|
be92fd0665 | ||
|
|
8a6d4d1385 | ||
|
|
13117a5d94 | ||
|
|
6b74c7ec7a | ||
|
|
e8b561f722 | ||
|
|
75ee5af180 | ||
|
|
29f924f6cc | ||
|
|
6e058c8e95 | ||
|
|
6822da80cf | ||
|
|
37e3302f61 | ||
|
|
9b369d71ee | ||
|
|
cf58f0db6f | ||
|
|
299f23c796 | ||
|
|
b58e4333a0 | ||
|
|
f76cb9c381 | ||
|
|
e93e10d14d | ||
|
|
025464ebff | ||
|
|
ae8f77f070 | ||
|
|
97811a12d1 | ||
|
|
1f7e01820a | ||
|
|
2f73771bbd | ||
|
|
503ac18e7b | ||
|
|
6a99b21c15 | ||
|
|
170667f2b4 | ||
|
|
314b7f2484 | ||
|
|
9353b12707 | ||
|
|
063810f906 | ||
|
|
ac7ce126e6 | ||
|
|
a920830dcf | ||
|
|
15d74fcee0 | ||
|
|
c598e3fc9f | ||
|
|
0dba0f2774 | ||
|
|
e2cdb6a94c | ||
|
|
5dd2011e78 | ||
|
|
7bc142dc83 | ||
|
|
1931f00806 | ||
|
|
253336b02c | ||
|
|
b0c3a97f7f | ||
|
|
22887d4f7a | ||
|
|
00dbc90c85 | ||
|
|
9ade1d8993 | ||
|
|
b2fb803033 | ||
|
|
988ed5b959 | ||
|
|
2e62f99724 | ||
|
|
530327b1e8 | ||
|
|
2f75b31fa9 | ||
|
|
ce8f0a8c4b | ||
|
|
321aae5b86 | ||
|
|
7712ea2d1f | ||
|
|
175f64f784 | ||
|
|
9357960463 | ||
|
|
4d043906bc | ||
|
|
b99b7f796b | ||
|
|
a7b0414613 | ||
|
|
13c608b11a | ||
|
|
d14fb91736 | ||
|
|
6beaa444af | ||
|
|
3b552558d4 | ||
|
|
9cc63bf8fb | ||
|
|
70ce7625b2 | ||
|
|
9936764125 | ||
|
|
a0f09e1671 | ||
|
|
1100b880f9 | ||
|
|
6224f33ba5 | ||
|
|
b0a9b5a6d2 | ||
|
|
ac7247c87a | ||
|
|
2ea459f3be | ||
|
|
25250f640f | ||
|
|
57a0cc8af4 | ||
|
|
5ebb978eae | ||
|
|
d647ee6ebe | ||
|
|
fea203ae7a | ||
|
|
b5eb413d37 | ||
|
|
aa1e5c4f39 | ||
|
|
ee91934bf7 | ||
|
|
1fa04eae9a | ||
|
|
98e1269f6f | ||
|
|
8f640fcb21 | ||
|
|
6c120b7fe9 | ||
|
|
033b93b9da | ||
|
|
6092358e87 | ||
|
|
71de39e871 | ||
|
|
54ffa193c3 | ||
|
|
03164f1ceb | ||
|
|
f67a46ab4c | ||
|
|
610790a778 | ||
|
|
6eaf451ad4 | ||
|
|
022e0adae3 | ||
|
|
6e6d4b31e7 | ||
|
|
0dafad3fb5 | ||
|
|
534de55239 | ||
|
|
d9f92bcdf2 | ||
|
|
1570299e38 | ||
|
|
c95f6f3bd9 | ||
|
|
9fcde2a234 | ||
|
|
a2ffc41e05 | ||
|
|
ff13baf257 | ||
|
|
8f5df07c19 | ||
|
|
299b38515a | ||
|
|
2b073aeb76 | ||
|
|
7a6f92f8a4 | ||
|
|
2e283ea95f | ||
|
|
829bb5ea1c | ||
|
|
0be18aaed0 | ||
|
|
ffe0f09061 | ||
|
|
730d61a18d | ||
|
|
fc07525976 | ||
|
|
c48953aa6b | ||
|
|
0c948b436d | ||
|
|
1b3f92dd1c | ||
|
|
c29c549a1c | ||
|
|
17622cacde | ||
|
|
0b423454ec | ||
|
|
81ec98f670 | ||
|
|
1aed425544 | ||
|
|
da080ace68 | ||
|
|
ded8dd2d2d | ||
|
|
edd83ba559 | ||
|
|
7d1d8559a5 | ||
|
|
f0bb9bfed1 | ||
|
|
c6ea057736 | ||
|
|
90a00198a7 | ||
|
|
f8241547fe | ||
|
|
1cb5d51c13 | ||
|
|
c42cdf8db1 | ||
|
|
69014ef8c6 | ||
|
|
5faab29a2b | ||
|
|
9a8b16ca69 | ||
|
|
53f3c66214 | ||
|
|
432bf0d6ba | ||
|
|
f3990f35f1 | ||
|
|
6f17412363 | ||
|
|
68907b9750 | ||
|
|
e7007e1980 | ||
|
|
3bcd96d73c | ||
|
|
8a77c62a5e | ||
|
|
93add82ab5 | ||
|
|
690b1f7ab3 | ||
|
|
3c07b94e3a | ||
|
|
3b63b55859 | ||
|
|
f87982c465 | ||
|
|
b03cedb55c | ||
|
|
f5934efa6c | ||
|
|
186619cd48 | ||
|
|
583d4c1a4d | ||
|
|
63bee54948 | ||
|
|
1523d64ed7 | ||
|
|
48fdde61dc | ||
|
|
7bc700ac5f | ||
|
|
bdd6147f92 | ||
|
|
f7f65bdaaa | ||
|
|
232b0782ca | ||
|
|
c3c36a26b6 | ||
|
|
7f22bdfbf2 | ||
|
|
ff4c579f7b | ||
|
|
9683882775 | ||
|
|
893e01532d | ||
|
|
713f78fa4e | ||
|
|
f444347fb9 | ||
|
|
ea03c0f755 | ||
|
|
e283a0f164 | ||
|
|
6820206d78 | ||
|
|
5069ee425c | ||
|
|
f93f342508 | ||
|
|
c6d3563101 | ||
|
|
f2f893c917 | ||
|
|
bbbca4085d | ||
|
|
4fdf461520 | ||
|
|
dcfc6728ba | ||
|
|
2673a92f50 | ||
|
|
4d72185ba9 | ||
|
|
ade4f5cd2b | ||
|
|
cf1ebf2b3d | ||
|
|
6b5cb7d87e | ||
|
|
4894bbc231 | ||
|
|
721c5a99ee | ||
|
|
36fccc80cb | ||
|
|
e14fe86306 | ||
|
|
95c9f99961 | ||
|
|
546189b9c8 | ||
|
|
dfbc2a65c0 | ||
|
|
2038d79e18 | ||
|
|
188caf397b | ||
|
|
13138b1d7d | ||
|
|
5466efd8c3 | ||
|
|
61f0dc92d1 | ||
|
|
073c174203 | ||
|
|
1d0f7231e5 | ||
|
|
9401c81581 | ||
|
|
936e54f3e2 | ||
|
|
0c02fc1ce0 | ||
|
|
c723f1df00 | ||
|
|
c65a3f7fb1 | ||
|
|
a0c59f9bb6 | ||
|
|
069dcd65bb | ||
|
|
5331d39c1d | ||
|
|
2c928e6158 | ||
|
|
3ae17479cb | ||
|
|
b3589755f5 | ||
|
|
e7f97ff4d4 | ||
|
|
7b4a3b52b3 | ||
|
|
310fd99730 | ||
|
|
26473f5596 | ||
|
|
ead8a02b6a | ||
|
|
b2ca9cf792 | ||
|
|
301c254446 | ||
|
|
769091d271 | ||
|
|
1bc7907ba0 | ||
|
|
8260c606dc | ||
|
|
c0b7e563c5 | ||
|
|
9062e15712 | ||
|
|
134da6fad3 | ||
|
|
4cec6ef711 | ||
|
|
36d64c85fb | ||
|
|
8e00019023 | ||
|
|
575d4d2d26 | ||
|
|
19e9ebc20f | ||
|
|
dd4476b786 | ||
|
|
fc283a64a7 | ||
|
|
b6dc1d35c6 | ||
|
|
4f542510d6 | ||
|
|
0ca334decc | ||
|
|
239a926301 | ||
|
|
9f4c7fbedd | ||
|
|
6a9de48bb7 | ||
|
|
d9fc22399a | ||
|
|
f41b0dc0c2 | ||
|
|
14233c0aa1 | ||
|
|
58e56c4226 | ||
|
|
8ee184475e | ||
|
|
be9c5b1be5 | ||
|
|
8411f8f2b9 | ||
|
|
5441a7a126 | ||
|
|
b2425de273 | ||
|
|
82d5e73129 | ||
|
|
31189ebdef | ||
|
|
ee1720fa95 | ||
|
|
72dc0634f3 | ||
|
|
13f5125daa | ||
|
|
2b6904b145 | ||
|
|
f776a4ba19 | ||
|
|
a7c8cb218f | ||
|
|
3cb2d20df9 | ||
|
|
ee4b5b15d7 | ||
|
|
299e6314b8 | ||
|
|
01170e1a1b | ||
|
|
6fe2e498aa | ||
|
|
fc7262f8f0 | ||
|
|
880d2ddcb0 | ||
|
|
e9ea378ef0 | ||
|
|
145ad7ed2c | ||
|
|
d5ce65c868 | ||
|
|
1bc76dfe09 | ||
|
|
17e82cf5ea | ||
|
|
926650802b | ||
|
|
fba7c0375d | ||
|
|
27be5b8ec4 | ||
|
|
d036246b2a | ||
|
|
59a9b15632 | ||
|
|
c09e061390 | ||
|
|
02f6dffaff | ||
|
|
509d4c8734 | ||
|
|
084fdef754 | ||
|
|
a2f21da355 | ||
|
|
b722502f5b | ||
|
|
ab45407763 | ||
|
|
dd4fbbdafd | ||
|
|
c8c609d88b | ||
|
|
92a4905b44 | ||
|
|
1c2aea7cb1 | ||
|
|
44127bec42 | ||
|
|
f48b97dd24 | ||
|
|
74c107c032 | ||
|
|
c4263dbe84 | ||
|
|
a6b7ec914d | ||
|
|
56c095a10c | ||
|
|
f486aaa31b | ||
|
|
c4f2f96c2e | ||
|
|
99988c0445 | ||
|
|
9c5191a6ae | ||
|
|
2f66715738 | ||
|
|
bcfd579a4b | ||
|
|
bef13832d6 | ||
|
|
130ebf1681 | ||
|
|
587c8c8fdb | ||
|
|
1ad9896be4 | ||
|
|
a6cdcf893d | ||
|
|
37ca881ed7 | ||
|
|
c38d7c0056 | ||
|
|
99b9a8710c | ||
|
|
a859426b06 | ||
|
|
479908bf59 | ||
|
|
3bc6522095 | ||
|
|
6fcce62b04 | ||
|
|
060a7b6dbc | ||
|
|
73521ad80a | ||
|
|
5c3897db9b | ||
|
|
e659ebbfd5 | ||
|
|
3e98d08a0f | ||
|
|
12069c0cd2 | ||
|
|
a62d492a0a | ||
|
|
2b65d2ddd5 | ||
|
|
9bb16d5cc5 | ||
|
|
9b8076b357 | ||
|
|
c86d34d881 | ||
|
|
6dd031df24 | ||
|
|
40b33bf28d | ||
|
|
9a93c7bbd9 | ||
|
|
24ef2b1c6c | ||
|
|
37359b4058 | ||
|
|
ce19f515d7 | ||
|
|
1359e0a33d | ||
|
|
3b234df7e7 | ||
|
|
b820a433df | ||
|
|
9bf061bf6d | ||
|
|
84f1fa123f | ||
|
|
91660fd1ec | ||
|
|
28c4a801a6 | ||
|
|
22af6e61af | ||
|
|
794a309d71 | ||
|
|
273d695d0d | ||
|
|
76d5ae613a | ||
|
|
14cca9e8cd | ||
|
|
8f5ac3706f | ||
|
|
d928f02f2b | ||
|
|
77c099fa8a | ||
|
|
bf9e5e2a3c | ||
|
|
b1ccbf3585 | ||
|
|
1f1050f115 | ||
|
|
df0f86ce7e | ||
|
|
007def4a01 | ||
|
|
3737cc4292 | ||
|
|
8723eb1e55 | ||
|
|
1192fc28a3 | ||
|
|
23af0a82ec | ||
|
|
a2ee1d91d9 | ||
|
|
eb9830f8a7 | ||
|
|
3f92b11864 | ||
|
|
96785e8876 | ||
|
|
d36925f616 | ||
|
|
bdc8d7bbd0 | ||
|
|
268685858d | ||
|
|
eeafd93e3e | ||
|
|
8fa6213f9c | ||
|
|
f8b186ec5d | ||
|
|
4f6a1a0493 | ||
|
|
8ee9b233f6 | ||
|
|
4f6f148c70 | ||
|
|
3ea8f38139 | ||
|
|
3657bbf53e | ||
|
|
92b098c240 | ||
|
|
7350eeb354 | ||
|
|
67e2b33db7 | ||
|
|
d9eb49c37c | ||
|
|
dba7ce7494 | ||
|
|
50d16feb3d | ||
|
|
437f94659c | ||
|
|
53efecc636 | ||
|
|
c2371d095e | ||
|
|
cacde7868c | ||
|
|
7cf06e6a9d | ||
|
|
83688daaf4 | ||
|
|
f55a6c6569 | ||
|
|
e42cc64f5d | ||
|
|
5c0a9f3bb6 | ||
|
|
b719731926 | ||
|
|
5b4d81fec2 | ||
|
|
4f50395eb9 | ||
|
|
30a7134db9 | ||
|
|
474d64c2a2 | ||
|
|
0293654803 | ||
|
|
80ca509998 | ||
|
|
a84157bd94 | ||
|
|
0007f14296 | ||
|
|
307ad265a9 | ||
|
|
41f36fdf2e | ||
|
|
86c28fef8a | ||
|
|
c01c09548c | ||
|
|
8f76bb88e7 | ||
|
|
ea24c2f85c | ||
|
|
806603c583 | ||
|
|
4b9a788be4 | ||
|
|
2d91d34e9a | ||
|
|
7e50cce009 | ||
|
|
2f642a11d0 | ||
|
|
6cb027b12e | ||
|
|
58903a885d | ||
|
|
eda4b7a9b5 | ||
|
|
2f48b4093f | ||
|
|
676f19ea0d | ||
|
|
0252cd5ef3 | ||
|
|
ac248e31a1 | ||
|
|
58e43f4fc8 | ||
|
|
3845926b97 | ||
|
|
78c99580bf | ||
|
|
3526711f4d | ||
|
|
5ab97839c6 | ||
|
|
c92455226f | ||
|
|
3b91162fd3 | ||
|
|
c3da8bb357 | ||
|
|
70dd9d2072 | ||
|
|
2a87e17909 | ||
|
|
ac95e23e18 | ||
|
|
5e606e485f | ||
|
|
a35f65b8d0 | ||
|
|
587f7f6c88 | ||
|
|
eedf71b933 | ||
|
|
671c36e9c1 | ||
|
|
f59b1f4096 | ||
|
|
138c5614d1 | ||
|
|
b0e1a09d61 | ||
|
|
29a8a5281b | ||
|
|
92cb25749a | ||
|
|
0b26dfe9de | ||
|
|
a1117ad09d | ||
|
|
d39bd0b7fa | ||
|
|
51db45c341 | ||
|
|
c74fabb1f0 | ||
|
|
afc9b2a135 | ||
|
|
e3cfb6d0a0 | ||
|
|
30a521f27d | ||
|
|
bba2759c1a | ||
|
|
4c1f870eaa | ||
|
|
5d317a9841 | ||
|
|
4c750ec3be | ||
|
|
da531208e5 | ||
|
|
554560681b | ||
|
|
05198ee7e9 | ||
|
|
a3b9f1b505 | ||
|
|
08578bf856 | ||
|
|
ea65f3cd2c | ||
|
|
4df8815bfa | ||
|
|
5f645c3d3a | ||
|
|
c774ace112 | ||
|
|
64a6ab0410 | ||
|
|
def9c22b64 | ||
|
|
6d6f90bf9f | ||
|
|
12dc482e91 | ||
|
|
b52c047cdf | ||
|
|
fa75e91ce6 | ||
|
|
c52d4d464d | ||
|
|
9719b5251c | ||
|
|
ea796bbea6 | ||
|
|
594a8730a4 | ||
|
|
03ae4aeefd | ||
|
|
57f6e50e0e | ||
|
|
fcebb2d0f2 | ||
|
|
56ebc80aab | ||
|
|
6d8aba1577 | ||
|
|
ebaf033b36 | ||
|
|
71f314cf75 | ||
|
|
f520c759ce | ||
|
|
b8d0b1a630 | ||
|
|
11deedb75d | ||
|
|
50a61f9e21 | ||
|
|
2df5a06930 | ||
|
|
0ae30ba8fb | ||
|
|
2b80d6c806 | ||
|
|
fd2ff77b62 | ||
|
|
46e699b745 | ||
|
|
5e0dbf9d7e | ||
|
|
eefc9b3834 | ||
|
|
bbc9783961 | ||
|
|
3c998d2f5c | ||
|
|
52cd1ce050 | ||
|
|
5140410006 | ||
|
|
852beb5a06 | ||
|
|
2cf150a541 | ||
|
|
4f2f1b227c | ||
|
|
7c1f77ff7b | ||
|
|
8b2b769435 | ||
|
|
5350ca8f53 | ||
|
|
14f4ac1248 | ||
|
|
548ecf5723 | ||
|
|
99e74759b5 | ||
|
|
ec177906c8 | ||
|
|
afdf38eaf2 | ||
|
|
d8d509812b | ||
|
|
3eeeadab5c | ||
|
|
657c3cff85 | ||
|
|
7ea7721ea3 | ||
|
|
b5ccf10128 | ||
|
|
a5a0b1a5e5 | ||
|
|
1e8ef812e9 | ||
|
|
4965e73b0c | ||
|
|
6db2b91052 | ||
|
|
a930d058c2 | ||
|
|
5b7acbc7f3 | ||
|
|
344d319d12 | ||
|
|
d5534e0967 | ||
|
|
680cb71dde | ||
|
|
a26ccb0a1f | ||
|
|
9fa037a9f9 | ||
|
|
f07dcbf8f8 | ||
|
|
783ee38a3a | ||
|
|
4a7be41dfe | ||
|
|
8b6960ae64 | ||
|
|
cf0b71bb69 | ||
|
|
e5608d061f | ||
|
|
7eba9523d4 | ||
|
|
5ad85b1a8c | ||
|
|
064a904975 | ||
|
|
ccbab4afba | ||
|
|
79870d995a | ||
|
|
0d95f1c44f | ||
|
|
62aabaa36a | ||
|
|
b57b746379 | ||
|
|
d1dbe7550d | ||
|
|
38d2364ac4 | ||
|
|
581b4f7773 | ||
|
|
0550ecec7b | ||
|
|
3071482a87 | ||
|
|
0c926a5acd | ||
|
|
59387eb214 | ||
|
|
9693af313f | ||
|
|
df1beabfa8 | ||
|
|
9630142921 | ||
|
|
fcef922ab1 | ||
|
|
edd8c83402 | ||
|
|
09d9801482 | ||
|
|
f50997ec23 | ||
|
|
5524e9f10a | ||
|
|
7e73e2f879 | ||
|
|
96719dae15 | ||
|
|
e78e9ba9e3 | ||
|
|
a730ed3228 | ||
|
|
c3b0a8643d | ||
|
|
422d98bc7c | ||
|
|
f1b61dad9b | ||
|
|
c97768975b | ||
|
|
0b882e8bde | ||
|
|
6752b503bc | ||
|
|
847963fb1c | ||
|
|
4441b60d4a | ||
|
|
cb74f4fb6a | ||
|
|
fae9c7151c | ||
|
|
7aa36c0361 | ||
|
|
eb4b4e948d | ||
|
|
13af890e29 | ||
|
|
d366f52127 | ||
|
|
8f8535b6a3 | ||
|
|
83fa27c7be | ||
|
|
8295496671 | ||
|
|
9277521dbd | ||
|
|
3797fc6469 | ||
|
|
9700e47358 | ||
|
|
8ee1c63e8d | ||
|
|
dae3def188 | ||
|
|
fa3dc0aa8c | ||
|
|
69fcdb969d | ||
|
|
9b03b4d909 | ||
|
|
5e9fb93a21 | ||
|
|
b6842a71fb | ||
|
|
f89645a516 | ||
|
|
d928a4ba81 | ||
|
|
c457b21e64 | ||
|
|
c63be3688b | ||
|
|
21c40ac7ad | ||
|
|
0938f68d8d | ||
|
|
87e903781b | ||
|
|
6753f92128 | ||
|
|
59353ff06f | ||
|
|
938bbc97a6 | ||
|
|
bb02eeb2b0 | ||
|
|
7bc91d73a6 | ||
|
|
59783bc2c3 | ||
|
|
d93ff842ab | ||
|
|
78417edcb8 | ||
|
|
f0f1c2c69e | ||
|
|
b4043d1a75 | ||
|
|
3614f812b9 | ||
|
|
26d3af414f | ||
|
|
ec8eb6a835 | ||
|
|
69f0f73087 | ||
|
|
0b3e4c8c96 | ||
|
|
b8777ee4da | ||
|
|
0bc415271e | ||
|
|
efa32e91ee | ||
|
|
f0927513cd | ||
|
|
3599c26c54 | ||
|
|
aa97091b98 | ||
|
|
ea62de3fdc | ||
|
|
b42089e992 | ||
|
|
ebffbb69ec | ||
|
|
c48b5b2292 | ||
|
|
8abd6558d1 | ||
|
|
dc0b4fada3 | ||
|
|
92cacbc6c8 | ||
|
|
4bad318ce7 | ||
|
|
0eeb7f372e | ||
|
|
1a72f3f8bb | ||
|
|
522ace8a4f | ||
|
|
7e776dc360 | ||
|
|
6487ddc6d2 | ||
|
|
4ebd09d2eb | ||
|
|
330b000b6e | ||
|
|
7a817945ee | ||
|
|
47b1b7fc82 | ||
|
|
7f2ea72ad3 | ||
|
|
62553d1a2e | ||
|
|
cff28876ee | ||
|
|
56032ed59c | ||
|
|
73245a31c6 | ||
|
|
f423ac2a14 | ||
|
|
82377c2c25 | ||
|
|
08bd2c8cee | ||
|
|
7205fdea22 | ||
|
|
c1c420ce8f | ||
|
|
576e80cc39 | ||
|
|
a51a871b70 | ||
|
|
f686420e1e | ||
|
|
2237a84d88 | ||
|
|
5c00d28396 | ||
|
|
2b0cff358e | ||
|
|
c3b7f9d050 | ||
|
|
a7fce331a0 | ||
|
|
6dd8573584 | ||
|
|
124418dc7a | ||
|
|
59e8ddbf82 | ||
|
|
366f58f072 | ||
|
|
7a12950c22 | ||
|
|
668000adf9 | ||
|
|
f2a2fee8f6 | ||
|
|
b412fb9d4e | ||
|
|
73de9f2e4c | ||
|
|
f4e5570ad5 | ||
|
|
2d1da2bbf5 | ||
|
|
23cb7ce992 | ||
|
|
941a1edb0c | ||
|
|
19af948deb | ||
|
|
63ea9a0f7d | ||
|
|
784e0f8652 | ||
|
|
e5f6f3f8e6 | ||
|
|
7c47ef0dee | ||
|
|
2a668d8602 | ||
|
|
efe18e9d81 | ||
|
|
cf20bb57b9 | ||
|
|
284f427da0 | ||
|
|
9f55171286 | ||
|
|
d427f50037 | ||
|
|
5bfaf8178f | ||
|
|
d57866e1a6 | ||
|
|
401f1bf7db | ||
|
|
a46dcbd0ae | ||
|
|
d2fea5f23b | ||
|
|
11b78638ec | ||
|
|
d6b6f76b55 | ||
|
|
bfba0b1d62 | ||
|
|
3b93e6ddc6 | ||
|
|
2a0ba7f72a | ||
|
|
ff9fbe6a6e | ||
|
|
7c712faff9 | ||
|
|
5a4ca867a8 | ||
|
|
88633798e7 | ||
|
|
86df807b01 | ||
|
|
3ae04f5547 | ||
|
|
c1ad22266d | ||
|
|
ec4f2517f6 | ||
|
|
4c73710665 | ||
|
|
fcea61979f | ||
|
|
bc68644468 | ||
|
|
652af28c76 | ||
|
|
31d6c5c4b9 | ||
|
|
28fdbd8bce | ||
|
|
314f5b3eed | ||
|
|
8983a67390 | ||
|
|
74d67cc261 | ||
|
|
2c4c64c0b7 | ||
|
|
758816815e | ||
|
|
bf52030506 | ||
|
|
29cf495dd0 | ||
|
|
500917937b | ||
|
|
812080341c | ||
|
|
221fa7edb3 | ||
|
|
fd58786524 | ||
|
|
a3398cbe34 | ||
|
|
b4cb5916bd | ||
|
|
b7e9c3c1eb | ||
|
|
c291c532ec | ||
|
|
edded9e5a6 | ||
|
|
eb2f0f34a4 | ||
|
|
a1fca74d66 | ||
|
|
36089c3e75 | ||
|
|
d27eecd54c | ||
|
|
183166d8d5 | ||
|
|
e01a8268aa | ||
|
|
d4d1969ec3 | ||
|
|
3dc371ce68 | ||
|
|
fba882adb8 | ||
|
|
e8ca268f1c | ||
|
|
3a0840d758 | ||
|
|
409460736d | ||
|
|
879de6ffcc | ||
|
|
d513f1ad84 | ||
|
|
98b65fadb6 | ||
|
|
832bafa217 | ||
|
|
769dc6220f | ||
|
|
a15af859d4 | ||
|
|
9a4d1e288d | ||
|
|
e20791aa6b | ||
|
|
19c4a48150 | ||
|
|
fba5c5c4f3 | ||
|
|
1f05edc79f | ||
|
|
f79c7ba58d | ||
|
|
5e7a6f1523 | ||
|
|
cc41a2e696 | ||
|
|
32ba1b0f75 | ||
|
|
d38b7e8051 | ||
|
|
353be9a4f8 | ||
|
|
ff14618ec0 | ||
|
|
208306cba5 | ||
|
|
7ef2c95b32 | ||
|
|
a41c822618 | ||
|
|
69a3cdbef3 | ||
|
|
ff51018d35 | ||
|
|
a228906eed | ||
|
|
a238fc9246 | ||
|
|
bf3ee15562 | ||
|
|
48fac7f86c | ||
|
|
82a7e2fbfa | ||
|
|
23b2fa5faf | ||
|
|
1d78bacbe8 | ||
|
|
043e781049 | ||
|
|
2d23d9db2f | ||
|
|
c260403370 | ||
|
|
8ac9b73bd5 | ||
|
|
609cce0a42 | ||
|
|
ec304649bd | ||
|
|
217edd7db0 | ||
|
|
5d441b2b9a | ||
|
|
157989316e | ||
|
|
7fc5a35b6a | ||
|
|
a93c863688 | ||
|
|
7735a76b1f | ||
|
|
58b0412c68 | ||
|
|
7ade900f46 | ||
|
|
ad678e9c67 | ||
|
|
6455a8b6f1 | ||
|
|
b1b26fb5ae | ||
|
|
d68be75be1 | ||
|
|
f40a5ac3af | ||
|
|
0c2cf6f460 | ||
|
|
b5a989b7fe | ||
|
|
ab9fad2514 | ||
|
|
e3e7cbb20f | ||
|
|
7b1395b803 | ||
|
|
d67db133ea | ||
|
|
d74f3aad96 | ||
|
|
7f2b24f075 | ||
|
|
d609d41628 | ||
|
|
50c241dd59 | ||
|
|
1a6e7da996 | ||
|
|
3a0eaf53f3 | ||
|
|
16a952349f | ||
|
|
c40f256df5 | ||
|
|
8f0cc645ef | ||
|
|
1f436a6abe | ||
|
|
477b44c8bb | ||
|
|
4147528d0e | ||
|
|
2b533f2c57 | ||
|
|
afaf8c919e | ||
|
|
2e87efdb63 | ||
|
|
65fe3626ba | ||
|
|
36bae74613 | ||
|
|
2e34fa11f8 | ||
|
|
3410493c62 | ||
|
|
f13ad8fd0a | ||
|
|
8208762da5 | ||
|
|
1beb01da98 | ||
|
|
e3dcfaec3b | ||
|
|
efbf7bc350 | ||
|
|
6cf769891f | ||
|
|
840e4b8566 | ||
|
|
38341f6968 | ||
|
|
80e9726ba3 | ||
|
|
8afce68864 | ||
|
|
5f7b7544c3 | ||
|
|
27897182f9 | ||
|
|
2af1f0d79c | ||
|
|
4eeaebe355 | ||
|
|
1dc85828d9 | ||
|
|
bf5408cd18 | ||
|
|
d80b06256a | ||
|
|
aa0fb88fd2 | ||
|
|
fa85501578 | ||
|
|
500390e6dd | ||
|
|
bf73f996ca | ||
|
|
cb7fb902be | ||
|
|
8fabc46ad4 | ||
|
|
38a7da7ee2 | ||
|
|
bbae14a727 | ||
|
|
f4609bfb3c | ||
|
|
afa4663176 | ||
|
|
9b806ee8b3 | ||
|
|
4975d2f6a7 | ||
|
|
1a675db872 | ||
|
|
0230ff4f81 | ||
|
|
d8039156ae | ||
|
|
e7105334c4 | ||
|
|
ad04ea0a62 | ||
|
|
0128268719 | ||
|
|
7873975980 | ||
|
|
57883fcb7e | ||
|
|
5979d9497e | ||
|
|
5dade5a633 | ||
|
|
694b46afbe | ||
|
|
5f81b2df5b | ||
|
|
ff47ed3261 | ||
|
|
da84e2a562 | ||
|
|
a272fd57bb | ||
|
|
e2fb623cbb | ||
|
|
d797584637 | ||
|
|
4549d806c9 | ||
|
|
a22d1d0ff1 | ||
|
|
adde259aa0 | ||
|
|
2768609286 | ||
|
|
1973d8841d | ||
|
|
b337031272 | ||
|
|
507b7d507a | ||
|
|
2f231b203c | ||
|
|
6e7a528d6f | ||
|
|
43dd37e8e0 | ||
|
|
11e965ddeb | ||
|
|
eb08493fdc | ||
|
|
27b0386eac | ||
|
|
c4314a45dd | ||
|
|
02a436f053 | ||
|
|
fea487c74e | ||
|
|
7e317c4b23 | ||
|
|
2c02d98890 | ||
|
|
bdbad1dca2 | ||
|
|
202ae24eea | ||
|
|
38db16468e | ||
|
|
1f272f4bce | ||
|
|
ed2b94ce05 | ||
|
|
146365fae2 | ||
|
|
3bf9956c41 | ||
|
|
7d8f833063 | ||
|
|
fcdac437eb | ||
|
|
cb74dbc22b | ||
|
|
a749f98e81 | ||
|
|
634c00cdd1 | ||
|
|
5524be3ce0 | ||
|
|
77bdb2f92c | ||
|
|
163fae21eb | ||
|
|
f58226aa27 | ||
|
|
2742a296ef | ||
|
|
bb204d6efb | ||
|
|
3a7c99cbbd | ||
|
|
7796d20984 | ||
|
|
9b3223c014 | ||
|
|
a5c5ba4b87 | ||
|
|
b53fea1812 | ||
|
|
b71a3456ce | ||
|
|
81cb44ed2b | ||
|
|
8c1fe6df1f | ||
|
|
91fadd1c3d | ||
|
|
28660a0873 | ||
|
|
5171ef5ef4 | ||
|
|
0cfd7eec9f | ||
|
|
2ed2361fdf | ||
|
|
7c8b05241a | ||
|
|
80e6edc474 | ||
|
|
9f13765db6 | ||
|
|
465f8e9f07 | ||
|
|
ebbd4a5d20 | ||
|
|
727a87eb99 | ||
|
|
dd754bdb60 | ||
|
|
830a58874f | ||
|
|
2459d8236e | ||
|
|
35c38dc28c | ||
|
|
45d1bed990 | ||
|
|
6c4f8ac37c | ||
|
|
22d131e9c4 | ||
|
|
232e74c659 | ||
|
|
85f90676e9 | ||
|
|
6a46997f24 | ||
|
|
69b34adb7f | ||
|
|
f7546058bf | ||
|
|
daf45d5990 | ||
|
|
fa4db9c66d | ||
|
|
9ce47bdfd1 | ||
|
|
b0c43e9260 | ||
|
|
6f567a24b6 | ||
|
|
2680a59080 | ||
|
|
0b3479bc16 | ||
|
|
87112035fb | ||
|
|
fbcae62e25 | ||
|
|
a15d708138 | ||
|
|
d4794055e1 | ||
|
|
eb273f928f | ||
|
|
f497a343fb | ||
|
|
ecb27ea068 | ||
|
|
45c56e7d6e | ||
|
|
8c7a71a3f6 | ||
|
|
40b167757a | ||
|
|
83c405abb6 | ||
|
|
4ea2d3bee2 | ||
|
|
4ac533548d | ||
|
|
7b8525f795 | ||
|
|
a6c65bab22 | ||
|
|
eeb0733c63 | ||
|
|
62a10ace1c | ||
|
|
fec87f1ba3 | ||
|
|
b1cfe581f6 | ||
|
|
7993481d0f | ||
|
|
7c74cedf32 | ||
|
|
3ec3d0bbc7 | ||
|
|
1fca8821b5 | ||
|
|
59b0cde17f | ||
|
|
b2e1019222 | ||
|
|
66be2a143a | ||
|
|
1db27548fb | ||
|
|
cba84b93f0 | ||
|
|
8dbd6a3a91 | ||
|
|
0bab7e53a7 | ||
|
|
fa0611b1bb | ||
|
|
156e69eb0d | ||
|
|
626115b284 | ||
|
|
54c264d03f | ||
|
|
f1399332bb | ||
|
|
943f1081a8 | ||
|
|
db039c7ccc | ||
|
|
7f7984ebe4 | ||
|
|
3752a69c78 | ||
|
|
42867bf41c | ||
|
|
b3f06b4973 | ||
|
|
710c6cb2dd | ||
|
|
d0a98557d3 | ||
|
|
416475aaea | ||
|
|
b2c7f6e0b1 | ||
|
|
7e26bb787d | ||
|
|
e7d3581e94 | ||
|
|
7ca76accc4 | ||
|
|
2f41582e9d | ||
|
|
7b09d1b42f | ||
|
|
17056d61e9 | ||
|
|
49b25a017a | ||
|
|
2b9dc727ba | ||
|
|
229ac7cace | ||
|
|
73ec7ef2ab | ||
|
|
0558b9a125 | ||
|
|
bd373024f1 | ||
|
|
22eb39522b | ||
|
|
93c42078a9 | ||
|
|
29bf15a266 | ||
|
|
a3541e7906 | ||
|
|
fcfeb448d6 | ||
|
|
4d8c15df31 | ||
|
|
9ff63b7389 | ||
|
|
1df8504c4e | ||
|
|
886a5a2a2a | ||
|
|
34142b8063 | ||
|
|
c8eb44b697 | ||
|
|
f0dd0c5a74 | ||
|
|
a108384305 | ||
|
|
37a82b1bb8 | ||
|
|
df03e4c2a2 | ||
|
|
4b0321975b | ||
|
|
43d495db38 | ||
|
|
46aff0e080 | ||
|
|
a65ec0b920 | ||
|
|
a3ef0f5bbe | ||
|
|
d91d413aaf | ||
|
|
4e7cb845d6 | ||
|
|
677276a2b7 | ||
|
|
a846928ff5 | ||
|
|
f760d3567a | ||
|
|
e2ebaf69a0 | ||
|
|
0b160581b9 | ||
|
|
1a7fe8b93d | ||
|
|
b745524257 | ||
|
|
14cd458d60 | ||
|
|
224a3cb66f | ||
|
|
25be1678dc | ||
|
|
7a05ce93b5 | ||
|
|
d556f0225b | ||
|
|
4845036256 | ||
|
|
0cf761f529 | ||
|
|
3b414192e9 | ||
|
|
736f75f413 | ||
|
|
db5046f7e1 | ||
|
|
eb6758bf3b | ||
|
|
7a77d3ba73 | ||
|
|
eba54a4acb | ||
|
|
1049dd22b3 | ||
|
|
cee244da23 | ||
|
|
fcf6000e8d | ||
|
|
74ce4ad340 | ||
|
|
73c5de46a5 | ||
|
|
36bbd92ee1 | ||
|
|
30cab528f9 | ||
|
|
3dfe362983 | ||
|
|
a711542863 | ||
|
|
bc00962d8e | ||
|
|
0776506e30 | ||
|
|
c8fdfc79ee | ||
|
|
51b797f9dd | ||
|
|
6f50515144 | ||
|
|
1911604fc3 | ||
|
|
f507f27d13 | ||
|
|
68260bc379 | ||
|
|
b63adbb601 | ||
|
|
754533834b | ||
|
|
0d8fb2789f | ||
|
|
680ecd9483 | ||
|
|
92414e42b1 | ||
|
|
74fbb8ee4b | ||
|
|
0361e04e2e | ||
|
|
2587ebb63c | ||
|
|
8f3894f68e | ||
|
|
cd9b034ef2 | ||
|
|
5d79eaff5b | ||
|
|
e77827ef6a | ||
|
|
d00a195599 | ||
|
|
2b6c7be71d | ||
|
|
2c8f1e6989 | ||
|
|
17934bdf08 | ||
|
|
06f7759321 | ||
|
|
25075c2cfd | ||
|
|
0438a6e641 | ||
|
|
e92a0421f4 | ||
|
|
2a09a74703 | ||
|
|
7023fe76fc | ||
|
|
0f0e3258ce | ||
|
|
374a93b0f2 | ||
|
|
0019db3394 | ||
|
|
b58f300171 | ||
|
|
75d01146ac | ||
|
|
d231e30f20 | ||
|
|
484be08a60 | ||
|
|
326653dc71 | ||
|
|
82c005f2c3 | ||
|
|
c88a44aef7 | ||
|
|
400bfe6dfd | ||
|
|
9d11d9914d | ||
|
|
ed68f7a5eb | ||
|
|
0090d3decf | ||
|
|
019f7ddacd | ||
|
|
36c662a201 | ||
|
|
e7617d2ac2 | ||
|
|
9b721c19ba | ||
|
|
755b6fd69a | ||
|
|
317fdce027 | ||
|
|
90d81ee7df | ||
|
|
2241c7f996 | ||
|
|
170c46563b | ||
|
|
6d67ff9843 | ||
|
|
d5b2b1e76e | ||
|
|
a3cb746d15 | ||
|
|
4cb6d684f8 | ||
|
|
bcd131238a | ||
|
|
65d49b3d81 | ||
|
|
f2550897a1 | ||
|
|
e9de191593 | ||
|
|
e3047b3141 | ||
|
|
38b220e4c9 | ||
|
|
a64a2b812c | ||
|
|
c11a70a0b7 | ||
|
|
215857b372 | ||
|
|
89e028d3c4 | ||
|
|
b81b9ce6c0 | ||
|
|
4fa0d389b6 | ||
|
|
1090ce7f97 | ||
|
|
c8cf92bc73 | ||
|
|
2acd0f73d9 | ||
|
|
36462865f6 | ||
|
|
ffeb2b6494 | ||
|
|
339e461d2f | ||
|
|
4748b4c332 | ||
|
|
125efe0e63 | ||
|
|
7aa3222efd | ||
|
|
ee305b4254 | ||
|
|
007dab9ed9 | ||
|
|
18adf63937 | ||
|
|
2e538fdad5 | ||
|
|
58c1880488 | ||
|
|
7cca56e20a | ||
|
|
50ac55c2c1 | ||
|
|
926c8f7f4d | ||
|
|
ac037bf977 | ||
|
|
0c3d26a08b | ||
|
|
70e96a0fa2 | ||
|
|
22ec270561 | ||
|
|
e8d9c231dc | ||
|
|
45c708909b | ||
|
|
3e963d8c22 | ||
|
|
45038fc9c3 | ||
|
|
4ad8abb88f | ||
|
|
13610f6612 | ||
|
|
173861bbd5 | ||
|
|
4b952f55ab | ||
|
|
00af80f531 | ||
|
|
7caad4bb32 | ||
|
|
a0667f6bc7 | ||
|
|
1d28abe664 | ||
|
|
029edb2e54 | ||
|
|
27959f0411 | ||
|
|
cd52a99fe5 | ||
|
|
691f03e905 | ||
|
|
ae93075dc8 | ||
|
|
e6b99dbb51 | ||
|
|
3539193f46 | ||
|
|
9a140d803c | ||
|
|
3afa2d0cb6 | ||
|
|
757c529b18 | ||
|
|
e3e3839dbc | ||
|
|
f8fdcbcf39 | ||
|
|
0d3a9be546 | ||
|
|
7b5251e80f | ||
|
|
70b4a417ff | ||
|
|
5dada9a06c | ||
|
|
95758fe71d | ||
|
|
21fdc4dbab | ||
|
|
c80100c38b | ||
|
|
b358fe6ab1 | ||
|
|
80c7b3d0e7 | ||
|
|
9455d7fc47 | ||
|
|
27f7d8568f | ||
|
|
08b149a4d1 | ||
|
|
39148d6b18 | ||
|
|
aed1960ce4 | ||
|
|
07d765da48 | ||
|
|
24e33e504b | ||
|
|
93a8215a22 | ||
|
|
b7ea958034 | ||
|
|
95e16d13e0 | ||
|
|
7f958210fd | ||
|
|
998d0010af | ||
|
|
7a29b74d05 | ||
|
|
1020bd2438 | ||
|
|
6de0ba238d | ||
|
|
28d871f45e | ||
|
|
675aa0daf6 | ||
|
|
ce98e1b71b | ||
|
|
7887860a91 | ||
|
|
67ec75a09a | ||
|
|
eba6656cf2 | ||
|
|
fbe0d46b82 | ||
|
|
6cc70b97ba | ||
|
|
b527bbe11e | ||
|
|
eee13c0dcb | ||
|
|
e141111ad4 | ||
|
|
83f247f8e9 | ||
|
|
8d46ee0af8 | ||
|
|
a6b41bf530 | ||
|
|
a73eec2d5a | ||
|
|
aafb37324d | ||
|
|
cc78e84615 | ||
|
|
c8968c1d3d | ||
|
|
0062f420a4 | ||
|
|
ceb8c3ed16 | ||
|
|
f21858d54f | ||
|
|
cde791e01d | ||
|
|
9919439bf6 | ||
|
|
033986af9e | ||
|
|
02a7d30e35 | ||
|
|
9ebc0f352c | ||
|
|
8e23884679 | ||
|
|
c88b44f712 | ||
|
|
f1dbe23131 | ||
|
|
d2a80b07c2 | ||
|
|
74b98195da | ||
|
|
551161c690 | ||
|
|
34a914dd50 | ||
|
|
ad1657b283 | ||
|
|
fe37ea63a0 | ||
|
|
5dbaa01b7a | ||
|
|
f259b8cb06 | ||
|
|
19ccb67a27 | ||
|
|
48a1adb672 | ||
|
|
bc9f795129 | ||
|
|
f0b18f4b15 | ||
|
|
15445870e3 | ||
|
|
8d47ec48ad | ||
|
|
3cc6e3b465 | ||
|
|
7e7c70276f | ||
|
|
ed9d9d1425 | ||
|
|
7caa0eb117 | ||
|
|
cbe7421f9f | ||
|
|
056ad8e6fb | ||
|
|
03a258f583 | ||
|
|
af721b867c | ||
|
|
9957d512a5 | ||
|
|
0af5d9ef05 | ||
|
|
545888ecd6 | ||
|
|
52a9e7c1d7 | ||
|
|
a936b0e139 | ||
|
|
8d156f0af9 | ||
|
|
a2384d61e0 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
.png filter=lfs diff=lfs merge=lfs -text
|
||||
.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,29 @@
|
||||
This is a machine control software project.
|
||||
|
||||
## Repository structure
|
||||
|
||||
### Frontend
|
||||
|
||||
`/electron`: Frontend code for the control software built with React and Electron.
|
||||
|
||||
### Backend
|
||||
|
||||
`/server`: Implements machine setup & loop. Exposes a REST & Socket.io API for the electron frontend.
|
||||
`/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
|
||||
`/ethercat-hal-derive`: Macros for ethercat-hal
|
||||
`/control-core`: Generic logic for the server.
|
||||
|
||||
### HMI Panel Operating System
|
||||
|
||||
`/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
|
||||
|
||||
### Other
|
||||
|
||||
/docs: Documentation for the project.
|
||||
|
||||
## Key Guidelines
|
||||
|
||||
1. Follow Rust & Typescript best practices and idiomatic patterns
|
||||
2. Maintain existing code structure and organization
|
||||
3. Write unit tests for new functionality where applicable.
|
||||
4. Document public APIs and complex logic in code. Suggest changes to the `docs/` folder when appropriate
|
||||
+14
-11
@@ -1,16 +1,19 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/electron" # Location of package manifests
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/electron"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "monthly"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
|
||||
- package-ecosystem: "cargo" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "monthly"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-minor"
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Electron
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build-electron:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18" # or '20' for latest LTS
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
node_modules/
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Install
|
||||
run: npm i
|
||||
|
||||
- name: Build
|
||||
run: tsc
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Package
|
||||
run: npm run package
|
||||
@@ -16,9 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install libudev development package
|
||||
run: sudo apt-get update && sudo apt-get install -y libudev-dev
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set Rust version
|
||||
run: rustup override set ${{ env.RUST_VERSION }}
|
||||
- name: Install Formatter
|
||||
run: rustup component add rustfmt
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -33,7 +37,12 @@ jobs:
|
||||
${{ runner.os }}-${{ env.RUST_VERSION }}-cargo-
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --check --verbose
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
+3
-1
@@ -2,4 +2,6 @@ target/
|
||||
dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Canonical identities on the left, aliases on the right.
|
||||
# Format options:
|
||||
# Canon Name <canon@example.com> <alias@example.com>
|
||||
# Canon Name <canon@example.com> Alias Name <alias@example.com>
|
||||
|
||||
# Icedsoda -> Philipp
|
||||
Philipp <29167146+1cedsoda@users.noreply.github.com> <29167146+1cedsoda@users.noreply.github.com>
|
||||
|
||||
# kraemr / Robin Krämer -> Robin
|
||||
Robin <93094049+kraemr@users.noreply.github.com> <93094049+kraemr@users.noreply.github.com>
|
||||
Robin <93094049+kraemr@users.noreply.github.com> Robin Krämer <93094049+kraemr@users.noreply.github.com>
|
||||
|
||||
# Oshgnacknak -> Josh
|
||||
Josh <osh@oshgnacknak.de> <osh@oshgnacknak.de>
|
||||
|
||||
# Milan
|
||||
Milan <61654753+MilanVDB@users.noreply.github.com>
|
||||
|
||||
# dependabot stays as bot
|
||||
Dependabot <49699333+dependabot[bot]@users.noreply.github.com> <49699333+dependabot[bot]@users.noreply.github.com>
|
||||
|
||||
# Medformatik / Florian -> Florian
|
||||
Florian <dev@fwcr.de> <dev@fwcr.de>
|
||||
Florian <dev@fwcr.de> Medformatik <dev@fwcr.de>
|
||||
|
||||
# Anatomik5 -> Alisher (merge noreply + gmail under Alisher)
|
||||
Alisher <115714693+Anatomik5@users.noreply.github.com> <115714693+Anatomik5@users.noreply.github.com>
|
||||
Alisher <115714693+Anatomik5@users.noreply.github.com> Anatomik5 <darmenovalisher@gmail.com>
|
||||
Alisher <115714693+Anatomik5@users.noreply.github.com> Darmenov Alisher <115714693+Anatomik5@users.noreply.github.com>
|
||||
|
||||
# snacj -> Melvin
|
||||
Melvin <0xsnacj@proton.me> <0xsnacj@proton.me>
|
||||
Melvin <0xsnacj@proton.me> snacj <bbmelvin@gmx.de>
|
||||
Melvin <0xsnacj@proton.me> Melvin Spieß <0xsnacj@proton.me>
|
||||
|
||||
# Janhegerq -> Jan
|
||||
Jan <219994445+Janhegerq@users.noreply.github.com> <219994445+Janhegerq@users.noreply.github.com>
|
||||
Jan <219994445+Janhegerq@users.noreply.github.com> <19994445+Janhegerq@users.noreply.github.com>
|
||||
|
||||
# Christian / TheBest6337 -> christian
|
||||
Christian <christian.zschoegner@outlook.de> <christian.zschoegner@outlook.de>
|
||||
Christian <christian.zschoegner@outlook.de> TheBest6337 <christian.zschoegner@outlook.de>
|
||||
Christian <christian.zschoegner@outlook.de> Christian <80114847+TheBest6337@users.noreply.github.com>
|
||||
|
||||
# nixos-install.sh
|
||||
nixos-install.sh <nixos-install.sh@localhost> <nixos-install.sh@localhost>
|
||||
|
||||
# copilot-swe-agent bot
|
||||
Copilot <198982749+Copilot@users.noreply.github.com> <198982749+Copilot@users.noreply.github.com>
|
||||
Vendored
+3
-2
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"typescript.inlayHints.enumMemberValues.enabled": false,
|
||||
"git.detectSubmodules": true,
|
||||
"git.detectSubmodulesLimit": 10,
|
||||
"git.detectSubmodules": true,
|
||||
"git.detectSubmodulesLimit": 10,
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
}
|
||||
+245
-1
@@ -1 +1,245 @@
|
||||
# Unreleased
|
||||
# `2.12.0`
|
||||
_17.11.2025_
|
||||
|
||||
## General
|
||||
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
|
||||
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
|
||||
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
|
||||
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
|
||||
|
||||
## Laser
|
||||
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
|
||||
|
||||
## Extruder V2
|
||||
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
|
||||
|
||||
|
||||
# `2.11.0`
|
||||
_21.10.2025_
|
||||
|
||||
## General
|
||||
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
|
||||
|
||||
## Winder V2
|
||||
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
|
||||
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
|
||||
|
||||
## Extruder V2
|
||||
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
|
||||
|
||||
|
||||
# `2.10.0`
|
||||
_16.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
|
||||
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
|
||||
|
||||
## General
|
||||
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
|
||||
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
|
||||
|
||||
|
||||
|
||||
# `2.9.1`
|
||||
_10.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#796](https://github.com/qitechgmbh/control/pull/796) Winding in adaptive mode, while puller is in reverse mode is now possible
|
||||
|
||||
## General
|
||||
- [#780](https://github.com/qitechgmbh/control/pull/780) Updates are faster now
|
||||
- [#746](https://github.com/qitechgmbh/control/pull/746) Improved scrollbar styling for manuals
|
||||
|
||||
# `2.9.0`
|
||||
_06.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#778](https://github.com/qitechgmbh/control/pull/778) Increased Winder Traverse Step size limit
|
||||
|
||||
## General
|
||||
- [#725](https://github.com/qitechgmbh/control/pull/725) Improved troubleshooting and update page
|
||||
- [#625](https://github.com/qitechgmbh/control/pull/625) Improved stability of Machine-Connections
|
||||
|
||||
|
||||
# `2.8.0`
|
||||
_30.09.2025_
|
||||
|
||||
|
||||
## Laser V1
|
||||
- [#741](https://github.com/qitechgmbh/control/pull/741) Added roundness measurement to two-axis Laser
|
||||
|
||||
|
||||
## Winder V2
|
||||
- [#745](https://github.com/qitechgmbh/control/pull/745), [#756](https://github.com/qitechgmbh/control/pull/756) improved Winder Frontend
|
||||
|
||||
## General
|
||||
- [#447](https://github.com/qitechgmbh/control/pull/447) added AquaPath Machine
|
||||
|
||||
|
||||
|
||||
|
||||
# `2.7.0`
|
||||
|
||||
_17.09.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#695](https://github.com/qitechgmbh/control/pull/695) Pressure and Rpm Regulation properly switch again
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#727](https://github.com/qitechgmbh/control/pull/727) Removed estimated diameter
|
||||
|
||||
## Laser V1
|
||||
|
||||
- [#731](https://github.com/qitechgmbh/control/pull/731) Added Laser with two-axis measurements
|
||||
|
||||
## General
|
||||
|
||||
- [#679](https://github.com/qitechgmbh/control/pull/679),[#677](https://github.com/qitechgmbh/control/pull/677),[#698](https://github.com/qitechgmbh/control/pull/698), [#684](https://github.com/qitechgmbh/control/pull/684),[#706](https://github.com/qitechgmbh/control/pull/706) Greatly improved overall system latency and stability
|
||||
|
||||
- [#720](https://github.com/qitechgmbh/control/pull/720), [#686](https://github.com/qitechgmbh/control/pull/686) Improved User Interface
|
||||
|
||||
# `2.6.0`
|
||||
|
||||
_03.09.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#658](https://github.com/qitechgmbh/control/pull/658) Added manual
|
||||
- [#639](https://github.com/qitechgmbh/control/pull/639) Swapped order of screw speed regulation buttons
|
||||
|
||||
## Laser V1
|
||||
|
||||
- [#642](https://github.com/qitechgmbh/control/pull/642) Improved stability of laser connection
|
||||
|
||||
## General
|
||||
|
||||
- [#657](https://github.com/qitechgmbh/control/pull/657) Time frame for graphs is now remembered
|
||||
- [#641](https://github.com/qitechgmbh/control/pull/641) Removed duplicate scrollbar from manuals
|
||||
|
||||
# `2.5.0`
|
||||
|
||||
_27.08.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#595](https://github.com/qitechgmbh/control/pull/600) Simplified power graphs
|
||||
- [#593](https://github.com/qitechgmbh/control/pull/599) Added identifiers for temperature sensors in temperature graph
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#598](https://github.com/qitechgmbh/control/pull/601) Images now load properly in the manual
|
||||
|
||||
## General
|
||||
|
||||
- [#540](https://github.com/qitechgmbh/control/pull/592) Improved readability of the presets
|
||||
- [#591](https://github.com/qitechgmbh/control/pull/626) Significantly improved user interface performance and stability
|
||||
|
||||
# `2.4.0`
|
||||
|
||||
_14.08.2025_
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
|
||||
|
||||
## General
|
||||
|
||||
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
|
||||
- [#588](https://github.com/qitechgmbh/control/pull/589) Improved frontend performance
|
||||
|
||||
# `2.3.0`
|
||||
|
||||
_06.08.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#520](https://github.com/qitechgmbh/control/pull/523) added warning message incase inverter is overloaded
|
||||
- [#501](https://github.com/qitechgmbh/control/pull/508) raised pressure limit of pressured PID mode for extruder
|
||||
- [#519](https://github.com/qitechgmbh/control/pull/549) added total power consumption monitor and graphs for voltage,current and wattage of the motor
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#64](https://github.com/qitechgmbh/control/pull/504) added winder handbook
|
||||
|
||||
## Laser V1
|
||||
|
||||
- [#497](https://github.com/qitechgmbh/control/pull/498) improved diameter visualisation
|
||||
|
||||
## General
|
||||
|
||||
- [#62](https://github.com/qitechgmbh/control/pull/514),[#524](https://github.com/qitechgmbh/control/pull/528) added machine presets to load configurations
|
||||
- [#526](https://github.com/qitechgmbh/control/pull/541) added import and export of presets
|
||||
|
||||
# `2.2.0`
|
||||
|
||||
_15.07.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#455](https://github.com/qitechgmbh/control/pull/458), Greatly improved stability and precision of pressure-based regulation for the motor
|
||||
- [#467](https://github.com/qitechgmbh/control/pull/468), [#448](https://github.com/qitechgmbh/control/pull/449), [#485](https://github.com/qitechgmbh/control/pull/486) Improved the Responsiveness for motor control and monitoring
|
||||
- [#138](https://github.com/qitechgmbh/control/pull/495), Added a Warning in case the Pressure sensor is disconnected or incorrectly wired
|
||||
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
|
||||
|
||||
## General
|
||||
|
||||
- [#121](https://github.com/qitechgmbh/control/pull/121), Use Linux realtime threads for EtherCAT and control loop (PREEMPT_RT)
|
||||
- [#138](https://github.com/qitechgmbh/control/pull/495), Updated Quick Start Guide
|
||||
|
||||
# `2.1.0`
|
||||
|
||||
_30.06.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- Intial Stable Release of **Extruder V2**
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#420](https://github.com/qitechgmbh/control/pull/429), [#401](https://github.com/qitechgmbh/control/pull/401) Add _Adaptive_ spool speed algorithm, the old algorithm is now called _Minmax_ ans tillavailable.
|
||||
- [#427](https://github.com/qitechgmbh/control/pull/427) Add _Estimated Spool Diameter_ graph
|
||||
- [#419](https://github.com/qitechgmbh/control/pull/419) Fix Issue where traverse won't work at low spool speed
|
||||
|
||||
## Laser V1
|
||||
|
||||
- Initial Stable Release of **Laser V1**
|
||||
|
||||
## HMI Panel
|
||||
|
||||
- [#339](https://github.com/qitechgmbh/control/pull/338) Don't go into sleep Mmde
|
||||
- [#410](https://github.com/qitechgmbh/control/pull/410) Ship with Wireshark
|
||||
- [#424](https://github.com/qitechgmbh/control/pull/424) Updates can be canceled
|
||||
|
||||
## General
|
||||
|
||||
- [#269](https://github.com/qitechgmbh/control/pull/269), [#370](https://github.com/qitechgmbh/control/pull/370) Improved performace of live values
|
||||
- [#358](https://github.com/qitechgmbh/control/pull/358), [#343](https://github.com/qitechgmbh/control/pull/343), [#324](https://github.com/qitechgmbh/control/pull/324), [#390](https://github.com/qitechgmbh/control/pull/390), [#299](https://github.com/qitechgmbh/control/pull/299) Improved functionality for graphs
|
||||
- [#405](https://github.com/qitechgmbh/control/pull/405) Remove weird color behaviour on tocuhscreens
|
||||
- [#369](https://github.com/qitechgmbh/control/pull/369) More failsafe EtherCAT loop
|
||||
|
||||
# `2.0.1`
|
||||
|
||||
_10.06.2025_
|
||||
|
||||
# `2.0.0`
|
||||
|
||||
_10.06.2025_
|
||||
|
||||
## General
|
||||
|
||||
- Initial stable release of QiTech Control.
|
||||
|
||||
## Winder V2
|
||||
|
||||
- Initial stable release of **Winder V2**
|
||||
|
||||
## Features
|
||||
|
||||
- Stabilized **Winder V2**
|
||||
Generated
+878
-292
File diff suppressed because it is too large.
Load diff
+21
-1
@@ -4,7 +4,27 @@ members = [
|
||||
"ethercat-hal",
|
||||
"ethercat-hal-derive",
|
||||
"control-core",
|
||||
"ethercat-eeprom-dump",
|
||||
"machines",
|
||||
#"ethercat-eeprom-dump",
|
||||
"control-core-derive",
|
||||
"units",
|
||||
"utils",
|
||||
]
|
||||
exclude = []
|
||||
resolver = "2"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Individual lints (priority 0)
|
||||
|
||||
# don't lint poissbilities for let chains because they were introduces after Rust 1.86
|
||||
collapsible_if = "allow"
|
||||
|
||||
# Lint groups (priority -1)
|
||||
correctness = { level = "warn", priority = -1 }
|
||||
suspicious = { level = "warn", priority = -1 }
|
||||
complexity = { level = "warn", priority = -1 }
|
||||
perf = { level = "warn", priority = -1 }
|
||||
style = { level = "warn", priority = -1 }
|
||||
pendantic = { level = "warn", priority = -1 }
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
|
||||
|
||||
[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
|
||||
*Click here to watch a video demo of our software.*
|
||||
|
||||
[](https://youtu.be/55egCAkQgyM)
|
||||
*Click here to watch a full explainer Video of our Software.*
|
||||
|
||||
# Repo Structure
|
||||
|
||||
Frontend
|
||||
@@ -29,7 +35,7 @@ Other
|
||||
|
||||
## Backend
|
||||
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
|
||||
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
|
||||
|
||||
@@ -41,6 +47,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
|
||||
# Dev Setup
|
||||
|
||||
[Developer Documentation](./docs/developer-docs/)
|
||||
|
||||
## Backend
|
||||
|
||||
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
|
||||
@@ -59,10 +67,12 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
|
||||
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
|
||||
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | In Progress | [](./docs/machines/winder-1.md) |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
|
||||
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
|
||||
|
||||
# More Docs
|
||||
|
||||
@@ -87,7 +97,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
- [X] [Threading](./docs/control-loop.md)
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
|
||||
+25
-5
@@ -1,13 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $1 == "release" ]; then
|
||||
set -e
|
||||
|
||||
FEATURE=""
|
||||
|
||||
# Check if "mock-machine" was passed as an argument
|
||||
if [[ "$1" == "mock-machine" ]]; then
|
||||
echo "Building Debug Code with mock-machine feature"
|
||||
FEATURE="--features mock-machine"
|
||||
fi
|
||||
|
||||
if [[ "$1" == "release" ]]; then
|
||||
echo "building Release Code"
|
||||
cargo build --release
|
||||
else
|
||||
echo "building Debug Code"
|
||||
cargo build
|
||||
echo "Building Debug Code"
|
||||
cargo build $FEATURE --features development-build
|
||||
fi
|
||||
# set cap for server executable
|
||||
sudo setcap cap_net_raw=eip ./target/debug/server
|
||||
|
||||
# Only setcap if not building with mock-machine
|
||||
if [ "$1" != "mock-machine" ]; then
|
||||
echo "Setting capabilities for server executable"
|
||||
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
|
||||
|
||||
if compgen -G "/dev/ttyUSB*" > /dev/null; then
|
||||
echo "Setting permissions for serial ports"
|
||||
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
|
||||
fi
|
||||
fi
|
||||
|
||||
# run
|
||||
./target/debug/server
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Nix store uses sqlite ... SO HAHAHAHAHH NO CONCURRENT ACCESS AHAHAHHAHAHAHHAHAHAHAH
|
||||
time nix build .#packages.x86_64-linux.server --impure --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/server-result
|
||||
serverstore=$(readlink -f /tmp/server-result)
|
||||
|
||||
time nix build .#packages.x86_64-linux.electron --impure --option sandbox false --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/electron-result
|
||||
electronstore=$(readlink -f /tmp/electron-result)
|
||||
|
||||
|
||||
nix copy --to "file:///opt/dev-cache" \
|
||||
$(readlink -f /tmp/server-result) \
|
||||
$(readlink -f /tmp/electron-result) --extra-experimental-features nix-command
|
||||
|
||||
nix store sign \
|
||||
--key-file /opt/dev-cache/cache-private-key.pem \
|
||||
--store "file:///opt/dev-cache" \
|
||||
--extra-experimental-features nix-command \
|
||||
"$serverstore" "$electronstore"
|
||||
|
||||
|
||||
nix path-info "$serverstore" --extra-experimental-features nix-command --extra-experimental-features flakes
|
||||
|
||||
|
||||
# nix verify --store "file:///opt/dev-cache" \
|
||||
# --trusted-public-keys "$(cat /opt/dev-cache/cache-public-key.pem)" --extra-experimental-features nix-command --extra-experimental-features flakes \
|
||||
# "$FIRST_PATH" || echo "Verification failed!"
|
||||
|
||||
|
||||
#time nix build .#packages.x86_64-linux.electron --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/electron-result
|
||||
#electronstore=$(readlink -f /tmp/electron-result)
|
||||
|
||||
#nix-store --generate-binary-cache-key cache.key cache.pem --extra-experimental-features nix-command
|
||||
#nix store sign --key-file cache.key "$serverstore" --extra-experimental-features nix-command
|
||||
#nix path-info --json "$serverstore" --extra-experimental-features nix-command --extra-experimental-features flakes
|
||||
#export
|
||||
#nix-store --export /nix/store/<hash>-server > nix-cache/server.nar
|
||||
#nix-store --export /nix/store/<hash>-electron > nix-cache/electron.nar
|
||||
|
||||
# Any http server
|
||||
# cd ~/nix-cache
|
||||
# python3 -m http.server 8080
|
||||
#
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "control_core_derive"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,65 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{DeriveInput, Error};
|
||||
|
||||
#[proc_macro_derive(BuildEvent)]
|
||||
pub fn build_event_derive(item: TokenStream) -> TokenStream {
|
||||
build_event_derive2(item.into()).unwrap().into()
|
||||
}
|
||||
|
||||
fn build_event_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
|
||||
// Parse the input tokens into a syntax tree
|
||||
let ast: DeriveInput = syn::parse2(item)?;
|
||||
|
||||
let ident = &ast.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
// Generate the BuildEvent impl
|
||||
let expanded = quote! {
|
||||
impl #impl_generics control_core::socketio::event::BuildEvent for #ident #ty_generics #where_clause {
|
||||
#[doc = "Implemented by the BuildEvent derive macro"]
|
||||
fn build(&self) -> control_core::socketio::event::Event<Self> {
|
||||
|
||||
control_core::socketio::event::Event::new(stringify!(#ident), self.clone())
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Machine)]
|
||||
pub fn machine_derive(item: TokenStream) -> TokenStream {
|
||||
machine_derive2(item.into()).unwrap().into()
|
||||
}
|
||||
|
||||
fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
|
||||
// Parse the input tokens into a syntax tree
|
||||
let ast: DeriveInput = syn::parse2(item)?;
|
||||
|
||||
let ident = &ast.ident;
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {
|
||||
fn get_machine_identification_unique(&self) -> control_core::machines::identification::MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(expanded)
|
||||
}
|
||||
+31
-7
@@ -3,19 +3,43 @@ name = "control_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
axum = "0.8.4"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
axum = "0.8.6"
|
||||
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
units = { path = "../units" }
|
||||
ethercrab = "0.6"
|
||||
interfaces = "0.0.9"
|
||||
log = "0.4.27"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serialport = "4.7.3"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.0"
|
||||
uom = "0.36.0"
|
||||
socketioxide = "0.17.2"
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.2.1"
|
||||
crc = "3.3.0"
|
||||
tracing = "0.1.41"
|
||||
erased-serde = "0.4.8"
|
||||
core_affinity = "0.8.3"
|
||||
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
|
||||
tokio = { version = "1.45.1", features = ["sync"], optional = true }
|
||||
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.176"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
textplots = "0.8"
|
||||
tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
video-streaming = ["dep:tokio"]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["axum","bitvec","control_core_derive"]
|
||||
@@ -1,91 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::analog_output::AnalogOutput;
|
||||
use std::{
|
||||
f64::consts::PI,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use uom::si::{angle::radian, f64::Angle, ratio::ratio};
|
||||
pub type AnalogFunction = Box<dyn Fn(Duration) -> f64 + Send + Sync>;
|
||||
|
||||
/// Can module analog output with a function
|
||||
pub struct AnalogFunctionGenerator {
|
||||
output: AnalogOutput,
|
||||
function: AnalogFunction,
|
||||
start_ts: Instant,
|
||||
}
|
||||
|
||||
impl AnalogFunctionGenerator {
|
||||
pub fn new(output: AnalogOutput, function: AnalogFunction) -> Self {
|
||||
Self {
|
||||
output,
|
||||
function,
|
||||
start_ts: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AnalogFunctionGenerator {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let diff = now - self.start_ts;
|
||||
let value = (self.function)(diff);
|
||||
(self.output.write)((value as f32).into()).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn analog_sine(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
// Convert to seconds as f64 for calculation
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let angle = Angle::new::<radian>(2.0 * PI * phase_secs / period_secs);
|
||||
let value = angle.sin().get::<ratio>();
|
||||
value * (amplitude / 2.0) + normal + (amplitude / 2.0)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn analog_multiply<const N: usize>(functions: [AnalogFunction; N]) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| functions.iter().fold(1.0, |acc, func| acc * func(dt)))
|
||||
}
|
||||
|
||||
pub fn analog_sawtooth(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let value = phase_secs / period_secs;
|
||||
value * amplitude + normal
|
||||
})
|
||||
}
|
||||
|
||||
pub fn analog_square(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let value = if phase_secs < period_secs / 2.0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
value * amplitude + normal
|
||||
})
|
||||
}
|
||||
|
||||
pub fn analog_triangle(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let value = if phase_secs < period_secs / 2.0 {
|
||||
phase_secs / period_secs * 2.0
|
||||
} else {
|
||||
2.0 - phase_secs / period_secs * 2.0
|
||||
};
|
||||
value * amplitude + normal
|
||||
})
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a analog input
|
||||
#[derive(Debug)]
|
||||
pub struct AnalogInputGetter {
|
||||
input: AnalogInput,
|
||||
normalized: Option<f32>,
|
||||
}
|
||||
|
||||
impl AnalogInputGetter {
|
||||
pub fn new(input: AnalogInput) -> Self {
|
||||
Self {
|
||||
input,
|
||||
normalized: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Value from -1.0 to 1.0
|
||||
pub fn get_normalized(&self) -> Option<f32> {
|
||||
self.normalized
|
||||
}
|
||||
|
||||
pub fn get_physical(&self) -> Option<AnalogInputValue> {
|
||||
match self.get_normalized() {
|
||||
Some(normalized) => Some(self.input.range.normalized_to_physical(normalized)),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AnalogInputGetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)().await;
|
||||
self.normalized = Some(state.input.normalized);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a digital input
|
||||
pub struct DigitalInputLogger {
|
||||
input: DigitalInput,
|
||||
}
|
||||
|
||||
impl DigitalInputLogger {
|
||||
pub fn new(input: DigitalInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalInputLogger {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)()
|
||||
.await
|
||||
.expect("Failed to get digital input state");
|
||||
println!("DigitalInputLogger: {}", state.input.value);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
|
||||
pub struct DigitalOutputBlinker {
|
||||
last_toggle: Instant,
|
||||
output: DigitalOutput,
|
||||
interval: Duration,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl DigitalOutputBlinker {
|
||||
pub fn new(output: DigitalOutput, interval: Duration) -> Self {
|
||||
Self {
|
||||
last_toggle: Instant::now(),
|
||||
output,
|
||||
interval,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
pub fn set_interval(&mut self, interval: Duration) {
|
||||
self.interval = interval;
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalOutputBlinker {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
{
|
||||
let state = (self.output.state)().await;
|
||||
if now - self.last_toggle > self.interval {
|
||||
match state.output.into() {
|
||||
true => (self.output.write)(false.into()).await,
|
||||
false => (self.output.write)(true.into()).await,
|
||||
}
|
||||
self.last_toggle = now;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Set a series of digital outputs high in sequence with a given interval
|
||||
pub struct DigitalOutputBlinkers {
|
||||
last_toggle: Instant,
|
||||
outputs: Vec<Option<DigitalOutput>>,
|
||||
index: usize,
|
||||
interval: Duration,
|
||||
enabled: bool,
|
||||
amount: usize,
|
||||
}
|
||||
|
||||
impl DigitalOutputBlinkers {
|
||||
pub fn new(outputs: Vec<Option<DigitalOutput>>, interval: Duration, amount: usize) -> Self {
|
||||
Self {
|
||||
last_toggle: Instant::now(),
|
||||
outputs,
|
||||
interval,
|
||||
enabled: true,
|
||||
index: 0,
|
||||
amount,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_interval(&mut self, interval: Duration) {
|
||||
self.interval = interval;
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalOutputBlinkers {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if now - self.last_toggle > self.interval {
|
||||
if let Some(output) = &self.outputs[self.index] {
|
||||
(output.write)(true.into()).await;
|
||||
}
|
||||
let index_end =
|
||||
(self.index + self.outputs.len() - self.amount) % self.outputs.len();
|
||||
if let Some(output) = &self.outputs[index_end] {
|
||||
(output.write)(false.into()).await;
|
||||
}
|
||||
self.last_toggle = now;
|
||||
self.index = (self.index + 1) % self.outputs.len();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalOutputSetter {
|
||||
output: DigitalOutput,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl DigitalOutputSetter {
|
||||
pub fn new(output: DigitalOutput) -> Self {
|
||||
Self {
|
||||
output,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
pub fn set(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
pub fn get(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
pub fn toggle(&mut self) {
|
||||
self.enabled = !self.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalOutputSetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
{
|
||||
(self.output.write)(self.enabled.into()).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// In this file create all Requests that are needed to int the inverter
|
||||
@@ -1,536 +0,0 @@
|
||||
use super::Actor;
|
||||
use crate::{
|
||||
converters::motor_converter::MotorConverter,
|
||||
modbus::{ModbusFunctionCode, ModbusRequest, ModbusResponse, calculate_modbus_rtu_timeout},
|
||||
};
|
||||
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
/// WaitingForResponse is set after sending a request through the serial_interface
|
||||
WaitingForResponse,
|
||||
/// After Sending a Resuest we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has transmit accepted toggled
|
||||
/// Then we can set state = ReadyToSend
|
||||
WaitingForRequestAccept,
|
||||
/// After Receiving a Response we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has received accepted toggled
|
||||
WaitingForReceiveAccept,
|
||||
/// ReadyToSend is set after receiving the response from the serial_interface
|
||||
ReadyToSend,
|
||||
/// Initial State
|
||||
Uninitialized,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OperationMode {
|
||||
PU,
|
||||
EXT,
|
||||
NET,
|
||||
}
|
||||
|
||||
impl MitsubishiInverterRS485Actor {
|
||||
pub fn convert_hz_float_to_word(&mut self, value: f32, little_endian: bool) -> u16 {
|
||||
let scaled = value * 100.0; // Convert Hz to 0.01 Hz units
|
||||
scaled.round() as u16
|
||||
}
|
||||
|
||||
pub fn set_running_rpm_target(&mut self, rpm: f32) {
|
||||
let mut request: MitsubishiModbusRequest =
|
||||
MitsubishiControlRequests::WriteRunningFrequency.into();
|
||||
|
||||
let hz = MotorConverter::rpm_to_hz(rpm); // convert rpm to hz
|
||||
let result: u16 = self.convert_hz_float_to_word(hz, true); // convert hz float to short
|
||||
request.request.data[2] = result.to_le_bytes()[1];
|
||||
request.request.data[3] = result.to_le_bytes()[0];
|
||||
self.add_request(request);
|
||||
}
|
||||
|
||||
pub fn read_running_frequency() {
|
||||
// Check if the current element pushed to front is freq event
|
||||
}
|
||||
|
||||
pub fn switch_operation_mode() {}
|
||||
}
|
||||
|
||||
/// Specifies all System environmet Variables
|
||||
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> address:0x1
|
||||
enum MitsubishiSystemRegister {
|
||||
/// Register 40002
|
||||
InverterReset,
|
||||
/// Register 40003
|
||||
ParameterClear,
|
||||
/// Register 40004
|
||||
AllParameterClear,
|
||||
/// Register 40006
|
||||
ParamClearNonCommunication,
|
||||
/// Register 40007
|
||||
AllParameterClearNonCommunication,
|
||||
/// Register 40009
|
||||
InverterStatusAndControl,
|
||||
/// Register 40010
|
||||
OperationModeAndSetting,
|
||||
/// Register 40014
|
||||
RunningFrequencyRAM,
|
||||
/// Register 40015
|
||||
RunningFrequencyEEPROM,
|
||||
/// Register 40201
|
||||
MotorFrequency,
|
||||
}
|
||||
|
||||
impl MitsubishiControlRequests {
|
||||
fn get_system_register(value: MitsubishiSystemRegister) -> u16 {
|
||||
match value {
|
||||
MitsubishiSystemRegister::InverterReset => 0x1,
|
||||
MitsubishiSystemRegister::ParameterClear => 0x2,
|
||||
MitsubishiSystemRegister::AllParameterClear => 0x3,
|
||||
MitsubishiSystemRegister::ParamClearNonCommunication => 0x5,
|
||||
MitsubishiSystemRegister::AllParameterClearNonCommunication => 0x6,
|
||||
MitsubishiSystemRegister::InverterStatusAndControl => 0x8,
|
||||
MitsubishiSystemRegister::OperationModeAndSetting => 0x9,
|
||||
MitsubishiSystemRegister::RunningFrequencyRAM => 0x0d,
|
||||
MitsubishiSystemRegister::RunningFrequencyEEPROM => 0x0e,
|
||||
MitsubishiSystemRegister::MotorFrequency => 0x00C8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MitsubishiControlRequests> for MitsubishiModbusRequest {
|
||||
fn from(request: MitsubishiControlRequests) -> Self {
|
||||
match request {
|
||||
MitsubishiControlRequests::WriteRunningFrequency => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::RunningFrequencyRAM,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x0],
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::WriteFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ReadInverterStatus => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::ReadHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterStatus,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::StopMotor => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Value 1 to stop
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterControl,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::StartForwardRotation => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000010], // Value 2 for forward rotation
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterControl,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::StartReverseRotation => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000100], // Value 4 for reverse rotation
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterControl,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ReadRunningFrequency => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::RunningFrequencyRAM,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::ReadHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::ReadFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ReadMotorFrequency => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::MotorFrequency,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::ReadHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x1],
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::ReadMotorFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ResetInverter => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0b00000001],
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::ReadMotorFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ClearAllParameters => todo!(),
|
||||
MitsubishiControlRequests::ClearNonCommunicationParameter => todo!(),
|
||||
MitsubishiControlRequests::ClearNonCommunicationParameters => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// These Requests Serve as Templates for controling the inverter
|
||||
#[derive(Debug)]
|
||||
pub enum MitsubishiControlRequests {
|
||||
/// Register 40002, Reset/Restart the Inverter
|
||||
ResetInverter,
|
||||
/// Register 40004, Clear ALL parameters
|
||||
ClearAllParameters,
|
||||
/// Register 40006, Clear a non communication parameter
|
||||
ClearNonCommunicationParameter,
|
||||
/// Register 40007, Clear all Non Communication related Parameters
|
||||
ClearNonCommunicationParameters,
|
||||
/// Register 40009, Read Inverter Status
|
||||
ReadInverterStatus,
|
||||
/// Register 40009, Stops the Motor
|
||||
StopMotor,
|
||||
/// Register 40009, Starts the Motor in Forward Rotation
|
||||
StartForwardRotation,
|
||||
/// Register 40009, Starts the Motor in Reverse Rotation
|
||||
StartReverseRotation,
|
||||
/// Register 40014, Read the current frequency the motor runs at (RAM)
|
||||
ReadRunningFrequency,
|
||||
/// Register 40014, Write the frequency
|
||||
WriteRunningFrequency,
|
||||
/// Read Register 40201, This contains the actual output frequency
|
||||
ReadMotorFrequency,
|
||||
}
|
||||
|
||||
// We need to know from the request queue which events are of what operation type, so that the correct timeout can be used
|
||||
#[derive(Debug)]
|
||||
pub struct MitsubishiModbusRequest {
|
||||
request: ModbusRequest,
|
||||
request_type: RequestType,
|
||||
expected_response_type: ResponseType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RotationDirection {
|
||||
Forward,
|
||||
Backwards,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResponseType {
|
||||
NoResponse,
|
||||
ReadFrequency,
|
||||
ReadMotorFrequency, // Motor Frequency is the actual frequency, that the motor is running at right now
|
||||
ReadInverterStatus,
|
||||
WriteFrequency,
|
||||
InverterStatus,
|
||||
InverterControl,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MitsubishiInverterRS485Actor {
|
||||
pub serial_interface: SerialInterface,
|
||||
pub last_ts: Instant,
|
||||
pub last_message_size: usize,
|
||||
pub last_request_type: RequestType,
|
||||
pub state: State,
|
||||
|
||||
pub baudrate: Option<u32>,
|
||||
pub encoding: Option<SerialEncoding>,
|
||||
|
||||
pub request_queue: VecDeque<MitsubishiModbusRequest>,
|
||||
pub response_queue: VecDeque<ModbusResponse>,
|
||||
|
||||
pub forward_rotation: bool,
|
||||
pub next_response_type: ResponseType,
|
||||
|
||||
pub current_freq: f32,
|
||||
pub current_rpm: f32,
|
||||
}
|
||||
|
||||
impl MitsubishiInverterRS485Actor {
|
||||
pub fn new(serial_interface: SerialInterface) -> Self {
|
||||
Self {
|
||||
serial_interface,
|
||||
last_ts: Instant::now(),
|
||||
state: State::Uninitialized,
|
||||
request_queue: VecDeque::new(),
|
||||
response_queue: VecDeque::new(),
|
||||
forward_rotation: true,
|
||||
next_response_type: ResponseType::ReadMotorFrequency,
|
||||
last_request_type: RequestType::OperationCommand,
|
||||
last_message_size: 0,
|
||||
baudrate: None,
|
||||
encoding: None,
|
||||
current_freq: 0.0,
|
||||
current_rpm: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// This would get called by the api to add a new request to the inverter
|
||||
pub fn add_request(&mut self, request: MitsubishiModbusRequest) {
|
||||
self.request_queue.push_front(request);
|
||||
}
|
||||
|
||||
/// This is used by the Api to pop off the Response of our Request
|
||||
pub fn get_response(&mut self) -> Option<ModbusResponse> {
|
||||
if self.response_queue.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
self.response_queue.pop_back()
|
||||
}
|
||||
|
||||
/// This is used internally to read the receive buffer of the el6021
|
||||
fn read_modbus_response(
|
||||
&mut self,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ModbusResponse, anyhow::Error>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if !(self.serial_interface.has_message)().await {
|
||||
// return;
|
||||
}
|
||||
|
||||
let res: Option<Vec<u8>> = (self.serial_interface.read_message)().await;
|
||||
let raw_response = match res {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
log::error!("ERROR: No Modbus Response");
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let response: Result<ModbusResponse, _> =
|
||||
ModbusResponse::try_from(raw_response.clone());
|
||||
|
||||
match response {
|
||||
Ok(result) => {
|
||||
self.last_message_size = result.clone().data.len() + 4;
|
||||
self.state = State::ReadyToSend;
|
||||
Ok(result)
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Error Parsing ModbusResponse!");
|
||||
self.state = State::ReadyToSend;
|
||||
Err(anyhow::anyhow!("error"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// This is used internally to fill the write buffer of the el6021
|
||||
fn send_modbus_request(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if self.request_queue.len() == 0 {
|
||||
return;
|
||||
};
|
||||
let request: MitsubishiModbusRequest = self.request_queue.pop_back().unwrap();
|
||||
self.next_response_type = request.expected_response_type;
|
||||
self.last_request_type = self.last_request_type;
|
||||
let modbus_request: Vec<u8> = request.request.into();
|
||||
let res = (self.serial_interface.write_message)(modbus_request.clone()).await;
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(_) => log::error!("ERROR: serial_interface.write_message has failed"),
|
||||
}
|
||||
self.state = State::WaitingForRequestAccept;
|
||||
self.last_message_size = modbus_request.len();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestType {
|
||||
/// Monitoring, Operation (start,stop etc) command, frequency setting (RAM), less than 12 milliseconds timeout for Response
|
||||
OperationCommand,
|
||||
/// Parameter Read/Write and Frequency (EEPROM), Less than 30 milliseconds timeout for Response
|
||||
ReadWrite,
|
||||
/// Less than 5 seconds timeout for Response
|
||||
ParamClear,
|
||||
/// no Timeout for Response
|
||||
Reset,
|
||||
}
|
||||
|
||||
impl RequestType {
|
||||
fn timeout_duration(self) -> Duration {
|
||||
match self {
|
||||
RequestType::OperationCommand => Duration::from_millis(12),
|
||||
RequestType::ReadWrite => Duration::from_millis(30),
|
||||
RequestType::ParamClear => Duration::from_millis(5000),
|
||||
RequestType::Reset => Duration::from_millis(30),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MitsubishiModbusExceptionCode {
|
||||
IllegalFunction,
|
||||
IllegalDataAddress,
|
||||
IllegalDataValue,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<MitsubishiModbusExceptionCode> for u8 {
|
||||
fn from(value: MitsubishiModbusExceptionCode) -> Self {
|
||||
match value {
|
||||
MitsubishiModbusExceptionCode::None => 0,
|
||||
MitsubishiModbusExceptionCode::IllegalFunction => 1,
|
||||
MitsubishiModbusExceptionCode::IllegalDataAddress => 2,
|
||||
MitsubishiModbusExceptionCode::IllegalDataValue => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for MitsubishiModbusExceptionCode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => MitsubishiModbusExceptionCode::IllegalFunction,
|
||||
2 => MitsubishiModbusExceptionCode::IllegalDataAddress,
|
||||
3 => MitsubishiModbusExceptionCode::IllegalDataValue,
|
||||
_ => MitsubishiModbusExceptionCode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different Response types
|
||||
impl MitsubishiInverterRS485Actor {
|
||||
// When we get respone from Pr. 40014 (Running Frequency) Convert to rpm and save it
|
||||
fn handle_motor_frequency(&mut self, resp: ModbusResponse) {
|
||||
if resp.data.len() < 4 {
|
||||
//return;
|
||||
}
|
||||
let freq_bytes = &resp.data[1..3]; // bytes 1 and 2 are needed
|
||||
self.current_freq = u16::from_be_bytes([freq_bytes[0], freq_bytes[1]]) as f32 / 100.0;
|
||||
self.current_rpm = MotorConverter::hz_to_rpm(self.current_freq);
|
||||
}
|
||||
|
||||
fn handle_inverter_status(&mut self, resp: ModbusResponse) {}
|
||||
|
||||
fn handle_inverter_frequency(&mut self, resp: ModbusResponse) {}
|
||||
|
||||
fn handle_response(&mut self, resp: ModbusResponse) {
|
||||
match self.next_response_type {
|
||||
ResponseType::ReadFrequency => self.handle_inverter_frequency(resp),
|
||||
ResponseType::ReadInverterStatus => (),
|
||||
ResponseType::WriteFrequency => (),
|
||||
ResponseType::ReadMotorFrequency => self.handle_motor_frequency(resp),
|
||||
ResponseType::InverterStatus => self.handle_inverter_status(resp),
|
||||
ResponseType::InverterControl => (),
|
||||
ResponseType::NoResponse => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for MitsubishiInverterRS485Actor {
|
||||
fn act(&mut self, now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let elapsed: Duration = now_ts.duration_since(self.last_ts);
|
||||
// State is uninitialized until serial interface init returns true, which takes a few cycles on the el6021
|
||||
if let State::Uninitialized = self.state {
|
||||
let res = (self.serial_interface.initialize)().await;
|
||||
if res == true {
|
||||
self.state = State::ReadyToSend;
|
||||
// every time when our inverter is "Uninitialzed" reset it first to clear any error states it may have
|
||||
self.add_request(MitsubishiControlRequests::ResetInverter.into());
|
||||
self.baudrate = (self.serial_interface.get_baudrate)().await;
|
||||
self.encoding = (self.serial_interface.get_serial_encoding)().await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let encoding = match self.encoding {
|
||||
Some(encoding) => encoding,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let baudrate = match self.baudrate {
|
||||
Some(baudrate) => baudrate,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let timeout = calculate_modbus_rtu_timeout(
|
||||
encoding.total_bits(),
|
||||
self.last_request_type.timeout_duration(),
|
||||
baudrate,
|
||||
self.last_message_size,
|
||||
);
|
||||
|
||||
// if we have no requests add ReadMotorFrequency
|
||||
if self.request_queue.is_empty() {
|
||||
self.add_request(MitsubishiControlRequests::ReadMotorFrequency.into());
|
||||
}
|
||||
|
||||
if elapsed < timeout {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_ts = now_ts;
|
||||
match self.state {
|
||||
State::WaitingForResponse => {
|
||||
let ret = self.read_modbus_response().await;
|
||||
match ret {
|
||||
Ok(ret) => self.handle_response(ret),
|
||||
Err(_) => (), // Do nothing for now
|
||||
}
|
||||
}
|
||||
State::ReadyToSend => self.send_modbus_request().await,
|
||||
State::WaitingForReceiveAccept => self.state = State::ReadyToSend,
|
||||
State::WaitingForRequestAccept => self.state = State::WaitingForResponse,
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
pub mod analog_function_generator;
|
||||
pub mod analog_input_getter;
|
||||
pub mod digital_input_logger;
|
||||
pub mod digital_output_blinker;
|
||||
pub mod digital_output_blinkers;
|
||||
pub mod digital_output_setter;
|
||||
pub mod mitsubishi_inverter_rs485;
|
||||
pub mod stepper_driver_el70x1;
|
||||
pub mod stepper_driver_max_speed;
|
||||
pub mod stepper_driver_pulse_train;
|
||||
pub mod temperature_input_getter;
|
||||
pub mod temperature_input_logger;
|
||||
|
||||
use std::{pin::Pin, time::Instant};
|
||||
|
||||
pub trait Actor: Send + Sync {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use ethercat_hal::shared_config::el70x1::EL70x1SpeedRange;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EL70x1VelocityCalculator {
|
||||
max_steps_per_seconds: u16,
|
||||
}
|
||||
|
||||
const MAX_VELOCITY: i16 = 10_000; // 100% of max speed
|
||||
|
||||
impl EL70x1VelocityCalculator {
|
||||
pub fn new(speed_range: &EL70x1SpeedRange) -> Self {
|
||||
let speed_range_value = match speed_range {
|
||||
EL70x1SpeedRange::Steps1000 => 1000,
|
||||
EL70x1SpeedRange::Steps2000 => 2000,
|
||||
EL70x1SpeedRange::Steps4000 => 4000,
|
||||
EL70x1SpeedRange::Steps8000 => 8000,
|
||||
EL70x1SpeedRange::Steps16000 => 16000,
|
||||
EL70x1SpeedRange::Steps32000 => 32000,
|
||||
};
|
||||
Self {
|
||||
max_steps_per_seconds: speed_range_value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert steps per second to the i16 velocity value used by the EL7031
|
||||
pub fn steps_to_velocity(&self, steps_per_second: i32) -> i16 {
|
||||
// Calculate the velocity value (10000 = 100% of max speed)
|
||||
let velocity_f64 =
|
||||
(steps_per_second as f64 / self.max_steps_per_seconds as f64) * MAX_VELOCITY as f64;
|
||||
|
||||
// Clamp to i16 range to be safe
|
||||
velocity_f64.round() as i16
|
||||
}
|
||||
|
||||
/// Convert i16 velocity value back to steps per second
|
||||
pub fn velocity_to_steps(&self, velocity: i16) -> i16 {
|
||||
// Calculate steps per second from velocity value
|
||||
let steps_per_second =
|
||||
(velocity as f64 / MAX_VELOCITY as f64) * self.max_steps_per_seconds as f64;
|
||||
|
||||
steps_per_second.round() as i16
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_velocity_conversion() {
|
||||
let calc = EL70x1VelocityCalculator::new(&EL70x1SpeedRange::Steps4000); // 4000 steps/s
|
||||
|
||||
// 0 sps = 0% velocity
|
||||
assert_eq!(calc.steps_to_velocity(0), 0);
|
||||
// 1000 sps = 25% velocity = 2500
|
||||
let v1 = calc.steps_to_velocity(1000);
|
||||
assert!((v1 - 2500i16).abs() <= 1, "Expected 2500±1, got {}", v1);
|
||||
// 2000 sps = 50% velocity = 5000
|
||||
let v2 = calc.steps_to_velocity(2000);
|
||||
assert!((v2 - 5000i16).abs() <= 1, "Expected 5000±1, got {}", v2);
|
||||
// 3000 sps = 75% velocity = 7500
|
||||
let v3 = calc.steps_to_velocity(3000);
|
||||
assert!((v3 - 7500i16).abs() <= 1, "Expected 7500±1, got {}", v3);
|
||||
// 4000 sps = 100% velocity = 10000
|
||||
let v4 = calc.steps_to_velocity(4000);
|
||||
assert!((v4 - 10000i16).abs() <= 1, "Expected 10000±1, got {}", v4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_velocity_to_steps_conversion() {
|
||||
let calc = EL70x1VelocityCalculator::new(&EL70x1SpeedRange::Steps4000); // 4000 steps/s
|
||||
|
||||
// 0% velocity = 0i16 = 0 sps
|
||||
assert_eq!(calc.velocity_to_steps(0), 0);
|
||||
// 25% velocity = 2500i16 = 1000 sps
|
||||
let s1 = calc.velocity_to_steps(2500);
|
||||
assert_eq!((s1 - 1000i16).abs(), 0, "Expected 1000±0, got {}", s1);
|
||||
// 50% velocity = 5000i16 = 2000 sps
|
||||
let s2 = calc.velocity_to_steps(5000);
|
||||
assert_eq!((s2 - 2000i16).abs(), 0, "Expected 2000±0, got {}", s2);
|
||||
// 75% velocity = 7500i16 = 3000 sps
|
||||
let s3 = calc.velocity_to_steps(7500);
|
||||
assert_eq!((s3 - 3000i16).abs(), 0, "Expected 3000±0, got {}", s3);
|
||||
// 100% velocity = 10000i16 = 4000 sps
|
||||
let s4 = calc.velocity_to_steps(10000);
|
||||
assert_eq!((s4 - 4000i16).abs(), 0, "Expected 4000±0, got {}", s4);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use super::Actor;
|
||||
use core::panic;
|
||||
use el70xx_velocity_converter::EL70x1VelocityCalculator;
|
||||
use ethercat_hal::{
|
||||
io::stepper_velocity_el70x1::StepperVelocityEL70x1, shared_config::el70x1::EL70x1SpeedRange,
|
||||
};
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
pub mod el70xx_velocity_converter;
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct StepperDriverEL70x1 {
|
||||
stepper: StepperVelocityEL70x1,
|
||||
enabled: bool,
|
||||
velocity: i16,
|
||||
pub converter: EL70x1VelocityCalculator,
|
||||
}
|
||||
|
||||
impl StepperDriverEL70x1 {
|
||||
pub fn new(stepper: StepperVelocityEL70x1, speed_range: &EL70x1SpeedRange) -> Self {
|
||||
Self {
|
||||
stepper,
|
||||
enabled: false,
|
||||
velocity: 0,
|
||||
converter: EL70x1VelocityCalculator::new(speed_range),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the speed in steps per second
|
||||
pub fn set_speed(&mut self, steps_per_second: i32) {
|
||||
self.velocity = self.converter.steps_to_velocity(steps_per_second)
|
||||
}
|
||||
|
||||
/// Get the speed in steps per second
|
||||
pub fn get_speed(&self) -> i32 {
|
||||
self.converter.velocity_to_steps(self.velocity) as i32
|
||||
}
|
||||
|
||||
/// Enable or disable the stepper
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
/// Get the enabled state of the stepper
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for StepperDriverEL70x1 {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = match (self.stepper.state)().await {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
panic!("Error while reading StepperVelocity {:?}", e);
|
||||
}
|
||||
};
|
||||
let mut output = state.output.clone();
|
||||
|
||||
// set the output
|
||||
output.stm_control.enable = self.enabled;
|
||||
output.stm_velocity.velocity = self.velocity;
|
||||
|
||||
// write the output
|
||||
match (self.stepper.write)(output).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
panic!("Error while writing StepperVelocity {:?}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
pub struct StepperDriverMaxSpeed {
|
||||
pulse: DigitalOutput,
|
||||
}
|
||||
|
||||
impl StepperDriverMaxSpeed {
|
||||
pub fn new(output: DigitalOutput) -> Self {
|
||||
Self { pulse: output }
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for StepperDriverMaxSpeed {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.pulse.state)().await;
|
||||
match state.output.into() {
|
||||
true => (self.pulse.write)(false.into()).await,
|
||||
false => (self.pulse.write)(true.into()).await,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StepperDriverMaxSpeed> for Arc<RwLock<StepperDriverMaxSpeed>> {
|
||||
fn from(actor: StepperDriverMaxSpeed) -> Self {
|
||||
Arc::new(RwLock::new(actor))
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/// Translates the encoder value to a larger space and handle underflow/overflow situations
|
||||
#[derive(Debug)]
|
||||
pub struct Encoder {
|
||||
pub position: i64,
|
||||
pub last_encoder_position: u32,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
position: 0,
|
||||
last_encoder_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, counter_value: u32, counter_overflow: bool, counter_underflow: bool) {
|
||||
// Handle overflow case
|
||||
if counter_overflow {
|
||||
// When overflow occurs, the counter wraps from MAX to 0
|
||||
// Calculate how much it moved past MAX
|
||||
let delta = counter_value as i64;
|
||||
// Add the remaining distance from last position to MAX, plus the overflow amount
|
||||
self.position += (u32::MAX - self.last_encoder_position) as i64 + 1 + delta;
|
||||
}
|
||||
// Handle underflow case
|
||||
else if counter_underflow {
|
||||
// When underflow occurs, the counter wraps from 0 to MAX
|
||||
// Calculate how much it moved below 0
|
||||
let delta = (u32::MAX - counter_value) as i64;
|
||||
// Subtract the distance from last position to 0, plus the underflow amount
|
||||
self.position -= self.last_encoder_position as i64 + 1 + delta;
|
||||
}
|
||||
// Normal case - no overflow/underflow
|
||||
else {
|
||||
// Calculate the signed difference between current and last position
|
||||
let delta = counter_value as i64 - self.last_encoder_position as i64;
|
||||
// Update the position
|
||||
self.position += delta;
|
||||
}
|
||||
|
||||
// Update the last encoder position for next time
|
||||
self.last_encoder_position = counter_value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Tests that the position tracker correctly handles normal forward movement
|
||||
/// without any overflow conditions.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Initial state is zeroed
|
||||
/// 2. Moving forward updates both position and last_encoder_position
|
||||
/// 3. Consecutive forward movements accumulate correctly
|
||||
#[test]
|
||||
fn test_normal_forward_movement() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Initial state
|
||||
assert_eq!(tracker.position, 0);
|
||||
assert_eq!(tracker.last_encoder_position, 0);
|
||||
|
||||
// Move forward by 100
|
||||
tracker.update(100, false, false);
|
||||
assert_eq!(tracker.position, 100);
|
||||
assert_eq!(tracker.last_encoder_position, 100);
|
||||
|
||||
// Move forward by another 50
|
||||
tracker.update(150, false, false);
|
||||
assert_eq!(tracker.position, 150);
|
||||
assert_eq!(tracker.last_encoder_position, 150);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker correctly handles normal backward movement
|
||||
/// without any underflow conditions.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Starting from a non-zero position
|
||||
/// 2. Moving backward updates both position and last_encoder_position
|
||||
/// 3. Consecutive backward movements accumulate correctly
|
||||
#[test]
|
||||
fn test_normal_backward_movement() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Start at position 1000
|
||||
tracker.position = 1000;
|
||||
tracker.last_encoder_position = 1000;
|
||||
|
||||
// Move backward by 100
|
||||
tracker.update(900, false, false);
|
||||
assert_eq!(tracker.position, 900);
|
||||
assert_eq!(tracker.last_encoder_position, 900);
|
||||
|
||||
// Move backward by another 50
|
||||
tracker.update(850, false, false);
|
||||
assert_eq!(tracker.position, 850);
|
||||
assert_eq!(tracker.last_encoder_position, 850);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker correctly handles overflow conditions
|
||||
/// where the encoder counter wraps from near its maximum value to a small value.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. When overflow flag is set, the tracker correctly calculates the true movement
|
||||
/// 2. Position is updated to account for the full distance traveled
|
||||
/// 3. last_encoder_position is updated to the new counter value
|
||||
#[test]
|
||||
fn test_overflow() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Set position near the maximum u32 value
|
||||
tracker.position = 100;
|
||||
tracker.last_encoder_position = u32::MAX - 10;
|
||||
|
||||
// Overflow: counter goes from (MAX-10) to 5
|
||||
// This means we moved forward by 10 steps to MAX, then 1 step to overflow,
|
||||
// then 5 more steps to reach the current position
|
||||
tracker.update(5, true, false);
|
||||
|
||||
// Expected: position += (10 + 1 + 5) = position += 16
|
||||
assert_eq!(tracker.position, 116);
|
||||
assert_eq!(tracker.last_encoder_position, 5);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker correctly handles underflow conditions
|
||||
/// where the encoder counter wraps from near zero to a value near its maximum.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. When underflow flag is set, the tracker correctly calculates the true movement
|
||||
/// 2. Position is updated to account for the full distance traveled (in negative direction)
|
||||
/// 3. last_encoder_position is updated to the new counter value
|
||||
#[test]
|
||||
fn test_underflow() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Set position near zero
|
||||
tracker.position = 100;
|
||||
tracker.last_encoder_position = 10;
|
||||
|
||||
// Underflow: counter goes from 10 to (MAX-5)
|
||||
// This means we moved backward by 10 steps to 0, then 1 step to underflow,
|
||||
// then 5 more steps to reach the current position
|
||||
tracker.update(u32::MAX - 5, false, true);
|
||||
|
||||
// Expected: position -= (10 + 1 + 5) = position -= 16
|
||||
assert_eq!(tracker.position, 84);
|
||||
assert_eq!(tracker.last_encoder_position, u32::MAX - 5);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker maintains correct behavior across
|
||||
/// a sequence of different operations including normal movement,
|
||||
/// overflow, and underflow.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. The tracker maintains state correctly between operations
|
||||
/// 2. Different types of movements can be handled in sequence
|
||||
/// 3. Position calculations remain accurate throughout various scenarios
|
||||
#[test]
|
||||
fn test_multiple_operations() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Normal forward
|
||||
tracker.update(100, false, false);
|
||||
assert_eq!(tracker.position, 100);
|
||||
|
||||
// Normal backward
|
||||
tracker.update(50, false, false);
|
||||
assert_eq!(tracker.position, 50);
|
||||
|
||||
// Overflow
|
||||
tracker.last_encoder_position = u32::MAX - 5;
|
||||
tracker.position = 50; // Reset for clarity
|
||||
tracker.update(10, true, false);
|
||||
assert_eq!(tracker.position, 66); // 50 + (5 + 1 + 10)
|
||||
|
||||
// Underflow
|
||||
tracker.last_encoder_position = 5;
|
||||
tracker.position = 100; // Reset for clarity
|
||||
tracker.update(u32::MAX - 10, false, true);
|
||||
assert_eq!(tracker.position, 84); // 100 - (5 + 1 + 10)
|
||||
}
|
||||
|
||||
/// Tests that the position tracker can handle multiple overflow and underflow
|
||||
/// events, maintaining an accurate position count that exceeds the u32 range.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Multiple overflow events accumulate correctly in the i64 position
|
||||
/// 2. Multiple underflow events correctly subtract from the position
|
||||
/// 3. The i64 position field properly extends the range beyond u32 limits
|
||||
#[test]
|
||||
fn test_large_movements() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Test tracking beyond u32 limits with multiple overflows
|
||||
for _ in 0..5 {
|
||||
// Simulate overflow: move from (MAX-100) to 100
|
||||
tracker.last_encoder_position = u32::MAX - 100;
|
||||
tracker.update(100, true, false);
|
||||
|
||||
// Each iteration adds (100 + 1 + 100) = 201 to position
|
||||
// We move 100 steps to MAX, 1 step to overflow, and 100 more steps to reach 100
|
||||
}
|
||||
|
||||
// After 5 iterations, position should be 5 * 201 = 1005
|
||||
assert_eq!(tracker.position, 1005);
|
||||
|
||||
// Test tracking with multiple underflows
|
||||
for _ in 0..3 {
|
||||
// Simulate underflow: move from 100 to (MAX-100)
|
||||
tracker.last_encoder_position = 100;
|
||||
tracker.update(u32::MAX - 100, false, true);
|
||||
|
||||
// Each iteration subtracts (100 + 1 + 100) = 201 from position
|
||||
// We move 100 steps to 0, 1 step to underflow, and 100 more steps to reach MAX-100
|
||||
}
|
||||
|
||||
// Final position: 1005 - (3 * 201) = 1005 - 603 = 402
|
||||
assert_eq!(tracker.position, 402);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use super::Actor;
|
||||
use encoder::Encoder;
|
||||
use ethercat_hal::io::pulse_train_output::PulseTrainOutput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
pub mod encoder;
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct StepperDriverPulseTrain {
|
||||
pulse: PulseTrainOutput,
|
||||
encoder: Encoder,
|
||||
frequency: i32,
|
||||
}
|
||||
|
||||
impl<'ptodevice> StepperDriverPulseTrain {
|
||||
pub fn new(output: PulseTrainOutput) -> Self {
|
||||
Self {
|
||||
pulse: output,
|
||||
encoder: Encoder::new(),
|
||||
frequency: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ptodevice> Actor for StepperDriverPulseTrain {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.pulse.state)().await;
|
||||
let mut output = state.output.clone();
|
||||
|
||||
// sync the counter to the encoder
|
||||
self.encoder.update(
|
||||
state.input.counter_value,
|
||||
state.input.counter_overflow,
|
||||
state.input.counter_underflow,
|
||||
);
|
||||
|
||||
// write frequency
|
||||
output.frequency_value = self.frequency;
|
||||
|
||||
(self.pulse.write)(output).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ptodevice> StepperDriverPulseTrain {
|
||||
pub fn set_frequency(&mut self, frequency: i32) {
|
||||
self.frequency = frequency;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use std::{pin::Pin, time::Instant};
|
||||
|
||||
use ethercat_hal::io::temperature_input::TemperatureInput;
|
||||
|
||||
use super::Actor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TemperatureInputGetter {
|
||||
input: TemperatureInput,
|
||||
pub temperature: f32,
|
||||
pub wiring_error: bool,
|
||||
}
|
||||
|
||||
impl TemperatureInputGetter {
|
||||
pub fn new(input: TemperatureInput) -> Self {
|
||||
Self {
|
||||
input,
|
||||
temperature: 0.0,
|
||||
wiring_error: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for TemperatureInputGetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let input = (self.input.state)().await.input;
|
||||
self.temperature = input.temperature;
|
||||
self.wiring_error = input.overvoltage | input.undervoltage;
|
||||
if self.wiring_error {
|
||||
self.temperature = 0.0;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::temperature_input::TemperatureInput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a temperature input
|
||||
pub struct TemperatureInputLogger {
|
||||
input: TemperatureInput,
|
||||
}
|
||||
|
||||
impl TemperatureInputLogger {
|
||||
pub fn new(input: TemperatureInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for TemperatureInputLogger {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)().await;
|
||||
println!("TemperatureInputLogger: {:?}C", state.input.temperature);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClampingTimeagnosticPidController {
|
||||
// Params
|
||||
/// Proportional gain
|
||||
kp: f64,
|
||||
/// Integral gain
|
||||
ki: f64,
|
||||
/// Derivative gain
|
||||
kd: f64,
|
||||
// State
|
||||
/// Proportional error
|
||||
ep: f64,
|
||||
/// Integral error
|
||||
ei: f64,
|
||||
/// Derivative error
|
||||
ed: f64,
|
||||
|
||||
min_ep: Option<f64>,
|
||||
max_ep: Option<f64>,
|
||||
|
||||
min_ei: Option<f64>,
|
||||
max_ei: Option<f64>,
|
||||
|
||||
min_ed: Option<f64>,
|
||||
max_ed: Option<f64>,
|
||||
|
||||
min_signal: Option<f64>,
|
||||
max_signal: Option<f64>,
|
||||
|
||||
last: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ClampingTimeagnosticPidController {
|
||||
pub const fn new(
|
||||
kp: f64,
|
||||
ki: f64,
|
||||
kd: f64,
|
||||
min_ep: Option<f64>,
|
||||
max_ep: Option<f64>,
|
||||
min_ei: Option<f64>,
|
||||
max_ei: Option<f64>,
|
||||
min_ed: Option<f64>,
|
||||
max_ed: Option<f64>,
|
||||
min_signal: Option<f64>,
|
||||
max_signal: Option<f64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
kd,
|
||||
ep: 0.0,
|
||||
ei: 0.0,
|
||||
ed: 0.0,
|
||||
last: None,
|
||||
min_ep,
|
||||
max_ep,
|
||||
min_ei,
|
||||
max_ei,
|
||||
min_ed,
|
||||
max_ed,
|
||||
min_signal,
|
||||
max_signal,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
kd,
|
||||
ep: 0.0,
|
||||
ei: 0.0,
|
||||
ed: 0.0,
|
||||
min_ep: None,
|
||||
max_ep: None,
|
||||
min_ei: None,
|
||||
max_ei: None,
|
||||
min_ed: None,
|
||||
max_ed: None,
|
||||
last: None,
|
||||
min_signal: None,
|
||||
max_signal: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get_kp(&self) -> f64 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub const fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub const fn get_kd(&self) -> f64 {
|
||||
self.kd
|
||||
}
|
||||
|
||||
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
|
||||
self.reset();
|
||||
self.kp = kp;
|
||||
self.ki = ki;
|
||||
self.kd = kd;
|
||||
}
|
||||
|
||||
pub const fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
|
||||
match (min, max) {
|
||||
(Some(min), Some(max)) => value.clamp(min, max),
|
||||
(Some(min), None) => value.max(min),
|
||||
(None, Some(max)) => value.min(max),
|
||||
(None, None) => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
|
||||
match self.last {
|
||||
// First update
|
||||
None => {
|
||||
// Calculate error
|
||||
let ep = error;
|
||||
|
||||
// Calculate signal
|
||||
let signal = self.kp * ep;
|
||||
let clamped_signal = Self::optional_clamp(signal, self.min_signal, self.max_signal);
|
||||
|
||||
// Set values
|
||||
self.ep = Self::optional_clamp(ep, self.min_ep, self.max_ep);
|
||||
self.ei = 0.0;
|
||||
self.ed = 0.0;
|
||||
self.last = Some(t);
|
||||
|
||||
clamped_signal
|
||||
}
|
||||
// Subsequent updates
|
||||
Some(last) => {
|
||||
// Calculate the time delta in seconds
|
||||
let dt = t.duration_since(last).as_secs_f64();
|
||||
|
||||
// Calculate errors
|
||||
let ep = Self::optional_clamp(error, self.min_ep, self.max_ep);
|
||||
|
||||
let ei = Self::optional_clamp(ep.mul_add(dt, self.ei), self.min_ei, self.max_ei);
|
||||
|
||||
let ed = Self::optional_clamp((ep - self.ep) / dt, self.min_ed, self.max_ed);
|
||||
|
||||
// Make factors timeagnostic
|
||||
let kp = self.kp * dt;
|
||||
let ki = self.ki * dt;
|
||||
let kd = self.kd * dt;
|
||||
|
||||
// Calculate signal
|
||||
let signal = kd.mul_add(ed, kp.mul_add(ep, ki * ei));
|
||||
let clamped_signal = Self::optional_clamp(signal, self.min_signal, self.max_signal);
|
||||
|
||||
// Set values
|
||||
self.ep = ep;
|
||||
self.ei = ei;
|
||||
self.ed = ed;
|
||||
self.last = Some(t);
|
||||
|
||||
clamped_signal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn reset(&mut self) {
|
||||
self.ep = 0.0;
|
||||
self.ei = 0.0;
|
||||
self.ed = 0.0;
|
||||
self.last = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::controllers::clamping_timeagnostic_pid::ClampingTimeagnosticPidController;
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_both_bounds() {
|
||||
let val = 5.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(1.0), Some(4.0));
|
||||
assert_eq!(clamped, 4.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(6.0), Some(10.0));
|
||||
assert_eq!(clamped, 6.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), Some(6.0));
|
||||
assert_eq!(clamped, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_only_min() {
|
||||
let val = 3.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(5.0), None);
|
||||
assert_eq!(clamped, 5.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), None);
|
||||
assert_eq!(clamped, 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_only_max() {
|
||||
let val = 7.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(6.0));
|
||||
assert_eq!(clamped, 6.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(8.0));
|
||||
assert_eq!(clamped, 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_no_bounds() {
|
||||
let val = 42.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, None);
|
||||
assert_eq!(clamped, 42.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AccelerationSpeedController {
|
||||
/// Maximum deceleration in units per second (positive value)
|
||||
max_acceleration: f64,
|
||||
|
||||
/// Maximum acceleratoin in units per second (negative value)
|
||||
min_acceleration: f64,
|
||||
|
||||
/// Minimum speed limit (None for no limit)
|
||||
min_speed: Option<f64>,
|
||||
|
||||
/// Maximum speed limit (None for no limit)
|
||||
max_speed: Option<f64>,
|
||||
|
||||
/// Calculated speed at the last update
|
||||
last_speed: f64,
|
||||
|
||||
/// Last update time
|
||||
last_t: Option<Instant>,
|
||||
}
|
||||
|
||||
impl AccelerationSpeedController {
|
||||
pub const fn new(
|
||||
min_speed: Option<f64>,
|
||||
max_speed: Option<f64>,
|
||||
min_acceleration: f64,
|
||||
max_acceleration: f64,
|
||||
initial_speed: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
min_acceleration,
|
||||
max_acceleration,
|
||||
min_speed,
|
||||
max_speed,
|
||||
last_speed: initial_speed,
|
||||
last_t: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new acceleration speed controller with simplified parameters.
|
||||
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(max_acceleration: f64, initial_speed: f64) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, target_speed: f64, t: Instant) -> f64 {
|
||||
// Calculate time delta
|
||||
let dt = match self.last_t {
|
||||
Some(last) => {
|
||||
let duration = t.duration_since(last);
|
||||
duration.as_secs_f64()
|
||||
}
|
||||
None => 0.0, // First update, no acceleration applied
|
||||
};
|
||||
|
||||
// Update the last update time
|
||||
self.last_t = Some(t);
|
||||
|
||||
// Get acceleration
|
||||
let acceleration = if target_speed > self.last_speed {
|
||||
// We are accelerating
|
||||
self.max_acceleration
|
||||
} else if target_speed < self.last_speed {
|
||||
// We are decelerating
|
||||
self.min_acceleration
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let speed_change = acceleration * dt;
|
||||
let new_speed = self.last_speed + speed_change;
|
||||
|
||||
// Prevent overshooting the target speed
|
||||
let new_speed = if acceleration > 0.0 {
|
||||
// Limit speed when accelerating
|
||||
new_speed.min(target_speed)
|
||||
} else if acceleration < 0.0 {
|
||||
// Limit speed when decelerating
|
||||
new_speed.max(target_speed)
|
||||
} else {
|
||||
new_speed
|
||||
};
|
||||
|
||||
// Apply speed limits
|
||||
let new_speed = self.apply_speed_limits(new_speed);
|
||||
|
||||
self.last_speed = new_speed;
|
||||
|
||||
new_speed
|
||||
}
|
||||
|
||||
pub const fn reset(&mut self, initial_speed: f64) {
|
||||
self.last_speed = initial_speed;
|
||||
self.last_t = None; // Reset the last update time
|
||||
}
|
||||
|
||||
pub const fn set_max_acceleration(&mut self, acceleration: f64) {
|
||||
self.max_acceleration = acceleration;
|
||||
}
|
||||
|
||||
const fn apply_speed_limits(&self, speed: f64) -> f64 {
|
||||
let mut limited_speed = speed;
|
||||
|
||||
if let Some(min) = self.min_speed {
|
||||
limited_speed = limited_speed.max(min);
|
||||
}
|
||||
|
||||
if let Some(max) = self.max_speed {
|
||||
limited_speed = limited_speed.min(max);
|
||||
}
|
||||
|
||||
limited_speed
|
||||
}
|
||||
|
||||
pub const fn get_min_speed(&self) -> Option<f64> {
|
||||
self.min_speed
|
||||
}
|
||||
|
||||
pub const fn get_max_speed(&self) -> Option<f64> {
|
||||
self.max_speed
|
||||
}
|
||||
|
||||
pub const fn set_min_speed(&mut self, min_speed: Option<f64>) {
|
||||
self.min_speed = min_speed;
|
||||
}
|
||||
|
||||
pub const fn set_max_speed(&mut self, max_speed: Option<f64>) {
|
||||
self.max_speed = max_speed;
|
||||
}
|
||||
|
||||
pub const fn set_min_acceleration(&mut self, deceleration: f64) {
|
||||
self.min_acceleration = deceleration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::time::Duration;
|
||||
|
||||
const EPSILON: f64 = 0.001;
|
||||
|
||||
// Custom function to create a "future" instant for testing
|
||||
fn future_instant(now: Instant, seconds: f64) -> Instant {
|
||||
now + Duration::from_secs_f64(seconds)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialization() {
|
||||
let controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
assert_relative_eq!(controller.max_acceleration, 10.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(controller.min_acceleration, -15.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(controller.last_speed, 5.0, epsilon = EPSILON);
|
||||
assert!(controller.last_t.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_update() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
let now = Instant::now();
|
||||
|
||||
let speed = controller.update(5.0, now);
|
||||
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(controller.last_speed, 5.0, epsilon = EPSILON);
|
||||
assert_eq!(controller.last_t.unwrap(), now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acceleration() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
// Use 0.1 seconds as our simulated time difference
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let expected_speed = (10.0 * dt).min(5.0); // Should accelerate by max_acceleration * dt, capped at target
|
||||
|
||||
let actual_speed = controller.update(5.0, t2);
|
||||
|
||||
assert!(actual_speed > 0.0);
|
||||
assert!(actual_speed <= 5.0);
|
||||
assert_relative_eq!(actual_speed, expected_speed, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deceleration() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 10.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(10.0, t1); // Initialize last_t
|
||||
|
||||
// Use 0.1 seconds as our simulated time difference
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let expected_speed = (10.0 + (-15.0 * dt)).max(5.0); // Should decelerate by max_deceleration * dt, floored at target
|
||||
|
||||
let actual_speed = controller.update(5.0, t2);
|
||||
|
||||
assert!(actual_speed < 10.0);
|
||||
assert!(actual_speed >= 5.0);
|
||||
assert_relative_eq!(actual_speed, expected_speed, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constant_speed() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 7.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(7.0, t1); // Initialize last_t
|
||||
|
||||
let t2 = future_instant(t1, 0.1);
|
||||
|
||||
let speed = controller.update(7.0, t2);
|
||||
|
||||
assert_relative_eq!(speed, 7.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acceleration_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let max_possible_change = 10.0 * dt;
|
||||
|
||||
// Target is far away, should be limited by max_acceleration
|
||||
let actual_speed = controller.update(100.0, t2);
|
||||
|
||||
assert_relative_eq!(actual_speed, max_possible_change, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deceleration_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 20.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(20.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let max_possible_change = -15.0 * dt;
|
||||
|
||||
// Target is far away, should be limited by max_deceleration
|
||||
let actual_speed = controller.update(0.0, t2);
|
||||
|
||||
assert_relative_eq!(actual_speed, 20.0 + max_possible_change, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_time_delta() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
let now = Instant::now();
|
||||
|
||||
controller.update(5.0, now); // Initialize last_t
|
||||
// Update again with the same time stamp
|
||||
let speed = controller.update(10.0, now);
|
||||
|
||||
// With zero time delta, speed shouldn't change
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acceleration_capped_at_target() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 100.0, 3.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(3.0, t1); // Initialize last_t
|
||||
|
||||
// With a high acceleration of 100.0 and dt of 0.1,
|
||||
// we could go up by 10.0 units, from 3.0 to 13.0
|
||||
// But target is only 5.0, so we should cap at 5.0
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let speed = controller.update(5.0, t2);
|
||||
|
||||
// Speed should be capped at target_speed (5.0), not 13.0
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deceleration_capped_at_target() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -100.0, 10.0, 8.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(8.0, t1); // Initialize last_t
|
||||
|
||||
// With a high deceleration of -100.0 and dt of 0.1,
|
||||
// we could go down by 10.0 units, from 8.0 to -2.0
|
||||
// But target is 5.0, so we should cap at 5.0
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let speed = controller.update(5.0, t2);
|
||||
|
||||
// Speed should be capped at target_speed (5.0), not -2.0
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_acceleration_to_target() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 20.0, 3.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(3.0, t1); // Initialize last_t
|
||||
|
||||
// With acceleration of 20.0 and dt of 0.1,
|
||||
// we would go up exactly 2.0 units, from 3.0 to 5.0
|
||||
// Target is 5.0, so we should hit it exactly
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let speed = controller.update(5.0, t2);
|
||||
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_deceleration_to_target() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -30.0, 10.0, 8.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(8.0, t1); // Initialize last_t
|
||||
|
||||
// With deceleration of -30.0 and dt of 0.1,
|
||||
// we would go down exactly 3.0 units, from 8.0 to 5.0
|
||||
// Target is 5.0, so we should hit it exactly
|
||||
let dt = 0.1;
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
let speed = controller.update(5.0, t2);
|
||||
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_limits_initialization() {
|
||||
let controller =
|
||||
AccelerationSpeedController::new(Some(-50.0), Some(100.0), -10.0, 20.0, 5.0);
|
||||
assert_eq!(controller.get_min_speed(), Some(-50.0));
|
||||
assert_eq!(controller.get_max_speed(), Some(100.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_speed_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(None, Some(50.0), -10.0, 100.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 1.0; // 1 second
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
// Try to reach 80.0, but should be limited to 50.0
|
||||
let speed = controller.update(80.0, t2);
|
||||
assert_eq!(speed, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_speed_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(Some(-30.0), None, -100.0, 10.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 1.0; // 1 second
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
// Try to reach -50.0, but should be limited to -30.0
|
||||
let speed = controller.update(-50.0, t2);
|
||||
assert_eq!(speed, -30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_within_limits() {
|
||||
let mut controller =
|
||||
AccelerationSpeedController::new(Some(-50.0), Some(50.0), -20.0, 20.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 0.5; // 0.5 seconds
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
// Target 10.0, with acceleration 20.0, should reach 10.0 in 0.5s
|
||||
let speed = controller.update(10.0, t2);
|
||||
assert_relative_eq!(speed, 10.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_limit_setters() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
|
||||
// Initially no limits
|
||||
assert_eq!(controller.get_min_speed(), None);
|
||||
assert_eq!(controller.get_max_speed(), None);
|
||||
|
||||
// Set limits
|
||||
controller.set_min_speed(Some(-100.0));
|
||||
controller.set_max_speed(Some(100.0));
|
||||
|
||||
assert_eq!(controller.get_min_speed(), Some(-100.0));
|
||||
assert_eq!(controller.get_max_speed(), Some(100.0));
|
||||
|
||||
// Remove limits
|
||||
controller.set_min_speed(None);
|
||||
controller.set_max_speed(None);
|
||||
|
||||
assert_eq!(controller.get_min_speed(), None);
|
||||
assert_eq!(controller.get_max_speed(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_simple_constructor() {
|
||||
let controller = AccelerationSpeedController::new_simple(20.0, 5.0);
|
||||
|
||||
// Check that acceleration values are symmetric
|
||||
assert_eq!(controller.min_acceleration, -20.0);
|
||||
assert_eq!(controller.max_acceleration, 20.0);
|
||||
assert_eq!(controller.last_speed, 5.0);
|
||||
|
||||
// Check that speed limits are None
|
||||
assert_eq!(controller.get_min_speed(), None);
|
||||
assert_eq!(controller.get_max_speed(), None);
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{AngularAcceleration, AngularVelocity},
|
||||
};
|
||||
|
||||
use super::acceleration_speed_controller::AccelerationSpeedController;
|
||||
|
||||
/// [`LinearAngularAccelerationController`] wraps [`LinearAccelerationController`]
|
||||
/// to handle angular velocities and accelerations.
|
||||
#[derive(Debug)]
|
||||
pub struct AngularAccelerationSpeedController {
|
||||
pub controller: AccelerationSpeedController,
|
||||
}
|
||||
|
||||
impl AngularAccelerationSpeedController {
|
||||
pub fn new(
|
||||
min_speed: Option<AngularVelocity>,
|
||||
max_speed: Option<AngularVelocity>,
|
||||
min_acceleration: AngularAcceleration,
|
||||
max_acceleration: AngularAcceleration,
|
||||
initial_speed: AngularVelocity,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationSpeedController::new(
|
||||
min_speed.map(|s| s.get::<radian_per_second>()),
|
||||
max_speed.map(|s| s.get::<radian_per_second>()),
|
||||
min_acceleration.get::<radian_per_second_squared>(),
|
||||
max_acceleration.get::<radian_per_second_squared>(),
|
||||
initial_speed.get::<radian_per_second>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new angular acceleration speed controller with simplified parameters.
|
||||
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(
|
||||
max_acceleration: AngularAcceleration,
|
||||
initial_speed: AngularVelocity,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, target_speed: AngularVelocity, t: Instant) -> AngularVelocity {
|
||||
let target_speed = target_speed.get::<radian_per_second>();
|
||||
let new_speed = self.controller.update(target_speed, t);
|
||||
AngularVelocity::new::<radian_per_second>(new_speed)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, initial_speed: AngularVelocity) {
|
||||
let initial_speed = initial_speed.get::<radian_per_second>();
|
||||
self.controller.reset(initial_speed);
|
||||
}
|
||||
|
||||
pub fn set_max_acceleration(&mut self, acceleration: AngularAcceleration) {
|
||||
self.controller
|
||||
.set_max_acceleration(acceleration.get::<radian_per_second_squared>());
|
||||
}
|
||||
pub fn set_min_acceleration(&mut self, deceleration: AngularAcceleration) {
|
||||
self.controller
|
||||
.set_min_acceleration(deceleration.get::<radian_per_second_squared>());
|
||||
}
|
||||
|
||||
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<AngularVelocity>) {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.map(|s| s.get::<radian_per_second>()));
|
||||
}
|
||||
|
||||
pub fn set_max_speed(&mut self, max_speed: Option<AngularVelocity>) {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.map(|s| s.get::<radian_per_second>()));
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Velocity},
|
||||
velocity::meter_per_second,
|
||||
};
|
||||
|
||||
use super::acceleration_speed_controller::AccelerationSpeedController;
|
||||
|
||||
/// [`LinearAccelerationController`] wraps [`LinearF64AccelerationController`]
|
||||
/// to handle linear velocities and accelerations.
|
||||
#[derive(Debug)]
|
||||
pub struct LinearAccelerationLimitingController {
|
||||
pub controller: AccelerationSpeedController,
|
||||
}
|
||||
|
||||
impl LinearAccelerationLimitingController {
|
||||
pub fn new(
|
||||
min_speed: Option<Velocity>,
|
||||
max_speed: Option<Velocity>,
|
||||
acceleration: Acceleration,
|
||||
deceleration: Acceleration,
|
||||
initial_speed: Velocity,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationSpeedController::new(
|
||||
min_speed.map(|s| s.get::<meter_per_second>()),
|
||||
max_speed.map(|s| s.get::<meter_per_second>()),
|
||||
acceleration.get::<meter_per_second_squared>(),
|
||||
deceleration.get::<meter_per_second_squared>(),
|
||||
initial_speed.get::<meter_per_second>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
/// Creates a new linear acceleration speed controller with simplified parameters.
|
||||
/// Sets deceleration to -acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(acceleration: Acceleration, initial_speed: Velocity) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
acceleration, // acceleration
|
||||
-acceleration, // deceleration (negative for deceleration)
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
pub fn update(&mut self, target_speed: Velocity, t: Instant) -> Velocity {
|
||||
let target_speed = target_speed.get::<meter_per_second>();
|
||||
let new_speed = self.controller.update(target_speed, t);
|
||||
Velocity::new::<meter_per_second>(new_speed)
|
||||
}
|
||||
pub fn reset(&mut self, initial_speed: Velocity) {
|
||||
let initial_speed = initial_speed.get::<meter_per_second>();
|
||||
self.controller.reset(initial_speed);
|
||||
}
|
||||
pub fn set_acceleration(&mut self, acceleration: Acceleration) {
|
||||
self.controller
|
||||
.set_max_acceleration(acceleration.get::<meter_per_second_squared>());
|
||||
}
|
||||
pub fn set_deceleration(&mut self, deceleration: Acceleration) {
|
||||
self.controller
|
||||
.set_min_acceleration(deceleration.get::<meter_per_second_squared>());
|
||||
}
|
||||
pub fn get_min_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
pub fn get_max_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<Velocity>) {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.map(|s| s.get::<meter_per_second>()));
|
||||
}
|
||||
pub fn set_max_speed(&mut self, max_speed: Option<Velocity>) {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.map(|s| s.get::<meter_per_second>()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod acceleration_speed_controller;
|
||||
pub mod angular_acceleration_speed_controller;
|
||||
pub mod linear_acceleration_speed_controller;
|
||||
@@ -1 +1,4 @@
|
||||
pub mod clamping_timeagnostic_pid;
|
||||
pub mod first_degree_motion;
|
||||
pub mod pid;
|
||||
pub mod second_degree_motion;
|
||||
@@ -9,7 +9,6 @@ pub struct PidController {
|
||||
ki: f64,
|
||||
/// Derivative gain
|
||||
kd: f64,
|
||||
|
||||
// State
|
||||
/// Proportional error
|
||||
ep: f64,
|
||||
@@ -22,7 +21,7 @@ pub struct PidController {
|
||||
}
|
||||
|
||||
impl PidController {
|
||||
pub fn new(kp: f64, ki: f64, kd: f64) -> Self {
|
||||
pub const fn new(kp: f64, ki: f64, kd: f64) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
@@ -34,8 +33,27 @@ impl PidController {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
|
||||
self.reset();
|
||||
self.kp = kp;
|
||||
self.ki = ki;
|
||||
self.kd = kd;
|
||||
}
|
||||
|
||||
pub const fn get_kp(&self) -> f64 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub const fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub const fn get_kd(&self) -> f64 {
|
||||
self.kd
|
||||
}
|
||||
|
||||
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
|
||||
let signal = match self.last {
|
||||
match self.last {
|
||||
// First update
|
||||
None => {
|
||||
// Calculate error
|
||||
@@ -47,6 +65,7 @@ impl PidController {
|
||||
// Set values
|
||||
self.ep = ep;
|
||||
self.ei = 0.0;
|
||||
|
||||
self.ed = 0.0;
|
||||
self.last = Some(t);
|
||||
|
||||
@@ -59,11 +78,11 @@ impl PidController {
|
||||
|
||||
// Calculate errors
|
||||
let ep = error;
|
||||
let ei = self.ei + ep * dt;
|
||||
let ei = ep.mul_add(dt, self.ei);
|
||||
let ed = (ep - self.ep) / dt;
|
||||
|
||||
// Calculate signal
|
||||
let signal = self.kp * ep + self.ki * ei + self.kd * ed;
|
||||
let signal = self.kd.mul_add(ed, self.kp.mul_add(ep, self.ki * ei));
|
||||
|
||||
// Set values
|
||||
self.ep = ep;
|
||||
@@ -73,12 +92,10 @@ impl PidController {
|
||||
|
||||
signal
|
||||
}
|
||||
};
|
||||
|
||||
signal
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
pub const fn reset(&mut self) {
|
||||
self.ep = 0.0;
|
||||
self.ei = 0.0;
|
||||
self.ed = 0.0;
|
||||
|
||||
+1225
File diff suppressed because it is too large.
Load diff
+219
@@ -0,0 +1,219 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
angle::radian,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{Angle, AngularAcceleration, AngularVelocity},
|
||||
};
|
||||
|
||||
use super::acceleration_position_controller::{
|
||||
AccelerationPositionController, MotionControllerError,
|
||||
};
|
||||
|
||||
/// Angular Acceleration Position Controller with proper physical units
|
||||
#[derive(Debug)]
|
||||
pub struct AngularAccelerationPositionController {
|
||||
controller: AccelerationPositionController,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
impl AngularAccelerationPositionController {
|
||||
/// Create a new angular position controller with acceleration limits
|
||||
pub fn new(
|
||||
min_position: Option<Angle>,
|
||||
max_position: Option<Angle>,
|
||||
min_speed: AngularVelocity,
|
||||
max_speed: AngularVelocity,
|
||||
min_acceleration: AngularAcceleration,
|
||||
max_acceleration: AngularAcceleration,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationPositionController::new(
|
||||
min_speed.get::<radian_per_second>(), // min_speed
|
||||
max_speed.get::<radian_per_second>(), // max_speed
|
||||
min_acceleration.get::<radian_per_second_squared>(), // min_acceleration
|
||||
max_acceleration.get::<radian_per_second_squared>(), // max_acceleration
|
||||
min_position.map(|angle| angle.get::<radian>()), // min_position
|
||||
max_position.map(|angle| angle.get::<radian>()), // max_position
|
||||
1e-6, // position_tolerance
|
||||
1e-6, // speed_tolerance
|
||||
)
|
||||
.expect("Failed to create AccelerationPositionController"),
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new angular position controller with simple symmetric limits
|
||||
///
|
||||
/// This is a convenience constructor that creates symmetric limits:
|
||||
/// - Position limits: [-position, +position] (if position is Some)
|
||||
/// - Speed limits: [-speed, +speed]
|
||||
/// - Acceleration limits: [-acceleration, +acceleration]
|
||||
/// - Default tolerances: 1e-6 for both position and speed
|
||||
///
|
||||
/// # Parameters (ordered: position, speed, acceleration)
|
||||
/// - `position`: Optional maximum position magnitude (None for no limits, Some(x) creates [-x, +x])
|
||||
/// - `speed`: Maximum angular velocity magnitude (creates limits [-speed, +speed])
|
||||
/// - `acceleration`: Maximum angular acceleration magnitude (creates limits [-acceleration, +acceleration])
|
||||
pub fn new_simple(
|
||||
position: Option<Angle>,
|
||||
speed: AngularVelocity,
|
||||
acceleration: AngularAcceleration,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationPositionController::new_simple(
|
||||
position.map(|angle| angle.get::<radian>()),
|
||||
speed.get::<radian_per_second>(),
|
||||
acceleration.get::<radian_per_second_squared>(),
|
||||
)
|
||||
.expect("Failed to create AccelerationPositionController"),
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the controller with a new target angle
|
||||
pub fn update(&mut self, target_angle: Angle, t: Instant) -> Angle {
|
||||
// Calculate dt from the last update
|
||||
let dt = if let Some(last_t) = self.last_update {
|
||||
t.duration_since(last_t).as_secs_f64()
|
||||
} else {
|
||||
0.0 // First update, no time has passed
|
||||
};
|
||||
self.last_update = Some(t);
|
||||
|
||||
// Update controller with raw angle values
|
||||
let result = self
|
||||
.controller
|
||||
.update(target_angle.get::<radian>(), dt)
|
||||
.expect("Failed to update AccelerationPositionController");
|
||||
Angle::new::<radian>(result)
|
||||
}
|
||||
|
||||
/// Get the current angle position
|
||||
pub fn get_position(&self) -> Angle {
|
||||
Angle::new::<radian>(self.controller.get_position())
|
||||
}
|
||||
|
||||
/// Get the target angle
|
||||
pub fn get_target_position(&self) -> Angle {
|
||||
Angle::new::<radian>(self.controller.get_target_position())
|
||||
}
|
||||
|
||||
/// Get the minimum angle position limit
|
||||
pub fn get_min_position(&self) -> Option<Angle> {
|
||||
self.controller.get_min_position().map(Angle::new::<radian>)
|
||||
}
|
||||
|
||||
/// Get the maximum angle position limit
|
||||
pub fn get_max_position(&self) -> Option<Angle> {
|
||||
self.controller.get_max_position().map(Angle::new::<radian>)
|
||||
}
|
||||
|
||||
/// Set the minimum angle position limit
|
||||
pub fn set_min_position(
|
||||
&mut self,
|
||||
min_position: Option<Angle>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_position(min_position.map(|angle| angle.get::<radian>()))
|
||||
}
|
||||
|
||||
/// Set the maximum angle position limit
|
||||
pub fn set_max_position(
|
||||
&mut self,
|
||||
max_position: Option<Angle>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_position(max_position.map(|angle| angle.get::<radian>()))
|
||||
}
|
||||
|
||||
/// Get the current angular velocity
|
||||
pub fn get_speed(&self) -> AngularVelocity {
|
||||
AngularVelocity::new::<radian_per_second>(self.controller.get_speed())
|
||||
}
|
||||
|
||||
/// Set the minimum angular velocity
|
||||
pub fn set_min_speed(
|
||||
&mut self,
|
||||
min_speed: AngularVelocity,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.get::<radian_per_second>())
|
||||
}
|
||||
|
||||
/// Set the maximum angular velocity
|
||||
pub fn set_max_speed(
|
||||
&mut self,
|
||||
max_speed: AngularVelocity,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.get::<radian_per_second>())
|
||||
}
|
||||
|
||||
/// Get the current angular acceleration
|
||||
pub fn get_acceleration(&self) -> AngularAcceleration {
|
||||
AngularAcceleration::new::<radian_per_second_squared>(self.controller.get_acceleration())
|
||||
}
|
||||
|
||||
/// Set the minimum angular acceleration
|
||||
pub fn set_min_acceleration(
|
||||
&mut self,
|
||||
min_acceleration: AngularAcceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_acceleration(min_acceleration.get::<radian_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Set the maximum angular acceleration
|
||||
pub fn set_max_acceleration(
|
||||
&mut self,
|
||||
max_acceleration: AngularAcceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_acceleration(max_acceleration.get::<radian_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Resets the controller to a specific angular position
|
||||
///
|
||||
/// This method completely reinitializes the controller state, setting:
|
||||
/// - Current angular position to the specified value
|
||||
/// - Target angular position to the specified value (stops any ongoing motion)
|
||||
/// - Current angular velocity to 0
|
||||
/// - Current angular acceleration to 0
|
||||
/// - Motion phase to Idle
|
||||
/// - Clears any motion planning state and timing information
|
||||
///
|
||||
/// Use this method when you need to teleport the controlled object to a new angular position
|
||||
/// or when recovering from errors/emergency stops.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `position` - The new angular position to reset to
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns `Ok(())` on success, or `Err(MotionControllerError)` if the position is outside configured limits.
|
||||
///
|
||||
/// # Errors
|
||||
/// - `InvalidPositionLimits`: If the specified position is outside the configured min/max position limits
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
|
||||
/// use units::f64::Angle;
|
||||
/// use units::angle::degree;
|
||||
///
|
||||
/// let mut controller = AngularAccelerationPositionController::new_simple(
|
||||
/// Some(Angle::new::<degree>(180.0)),
|
||||
/// // ... other parameters
|
||||
/// )?;
|
||||
///
|
||||
/// // Reset to 45 degrees
|
||||
/// let new_position = Angle::new::<degree>(45.0);
|
||||
/// controller.reset(new_position)?;
|
||||
/// ```
|
||||
pub fn reset(&mut self, position: Angle) -> Result<(), MotionControllerError> {
|
||||
self.controller.reset(position.get::<radian>())?;
|
||||
self.last_update = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_jerk::radian_per_second_cubed,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{AngularAcceleration, AngularJerk, AngularVelocity},
|
||||
};
|
||||
|
||||
use super::acceleration_position_controller::MotionControllerError;
|
||||
use super::jerk_speed_controller::JerkSpeedController;
|
||||
|
||||
/// Angular Jerk Speed Controller with proper physical units
|
||||
///
|
||||
/// This controller provides angular motion control with proper SI units for rotational systems.
|
||||
/// It wraps the core JerkSpeedController and provides unit-typed interfaces for angular velocity
|
||||
/// (AngularVelocity), angular acceleration (AngularAcceleration), and angular jerk (AngularJerk).
|
||||
///
|
||||
/// The controller manages smooth rotational speed profiles with configurable limits and tolerances,
|
||||
/// ensuring that angular velocity, acceleration, and jerk constraints are respected during motion.
|
||||
/// This is particularly useful for motor control, servo systems, and any rotational machinery
|
||||
/// requiring smooth speed transitions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
/// f64::{AngularVelocity, AngularAcceleration, AngularJerk}
|
||||
/// };
|
||||
///
|
||||
/// let max_speed = AngularVelocity::new::<revolution_per_minute>(3000.0);
|
||||
/// let max_accel = AngularAcceleration::new::<revolution_per_minute_per_second>(1000.0);
|
||||
/// let max_jerk = AngularJerk::new::<revolution_per_minute_per_second_squared>(500.0);
|
||||
///
|
||||
/// let controller = AngularJerkSpeedController::new_simple(
|
||||
/// Some(max_speed),
|
||||
/// max_accel,
|
||||
/// max_jerk,
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct AngularJerkSpeedController {
|
||||
controller: JerkSpeedController,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
impl AngularJerkSpeedController {
|
||||
/// Create a new angular speed controller with jerk limits
|
||||
///
|
||||
/// This constructor allows full control over all angular velocity, acceleration, and jerk limits.
|
||||
/// Use this when you need asymmetric limits or fine-grained control over the rotational motion profile.
|
||||
///
|
||||
/// # Parameters (ordered: speed, acceleration, jerk)
|
||||
/// * `min_speed` - Optional minimum allowed angular velocity (None for no limit)
|
||||
/// * `max_speed` - Optional maximum allowed angular velocity (None for no limit)
|
||||
/// * `min_acceleration` - Minimum angular acceleration (typically negative for deceleration)
|
||||
/// * `max_acceleration` - Maximum angular acceleration (typically positive)
|
||||
/// * `min_jerk` - Minimum angular jerk for decreasing acceleration (typically negative)
|
||||
/// * `max_jerk` - Maximum angular jerk for increasing acceleration (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// A new AngularJerkSpeedController instance
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
/// f64::{AngularVelocity, AngularAcceleration, AngularJerk}
|
||||
/// };
|
||||
///
|
||||
/// // Create controller with asymmetric limits
|
||||
/// let controller = AngularJerkSpeedController::new(
|
||||
/// Some(AngularVelocity::new::<revolution_per_minute>(-1000.0)), // Min: -1000 RPM
|
||||
/// Some(AngularVelocity::new::<revolution_per_minute>(3000.0)), // Max: +3000 RPM
|
||||
/// AngularAcceleration::new::<revolution_per_minute_per_second>(-800.0), // Min accel
|
||||
/// AngularAcceleration::new::<revolution_per_minute_per_second>(1200.0), // Max accel
|
||||
/// AngularJerk::new::<revolution_per_minute_per_second_squared>(-400.0), // Min jerk
|
||||
/// AngularJerk::new::<revolution_per_minute_per_second_squared>(600.0), // Max jerk
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(
|
||||
min_speed: Option<AngularVelocity>,
|
||||
max_speed: Option<AngularVelocity>,
|
||||
min_acceleration: AngularAcceleration,
|
||||
max_acceleration: AngularAcceleration,
|
||||
min_jerk: AngularJerk,
|
||||
max_jerk: AngularJerk,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: JerkSpeedController::new(
|
||||
min_speed.map(|speed| speed.get::<radian_per_second>()),
|
||||
max_speed.map(|speed| speed.get::<radian_per_second>()),
|
||||
min_acceleration.get::<radian_per_second_squared>(),
|
||||
max_acceleration.get::<radian_per_second_squared>(),
|
||||
min_jerk.get::<radian_per_second_cubed>(),
|
||||
max_jerk.get::<radian_per_second_cubed>(),
|
||||
),
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new simple angular jerk speed controller with symmetric limits
|
||||
///
|
||||
/// This is a convenience constructor that creates symmetric limits around zero for all parameters.
|
||||
/// This is the most common use case and simplifies controller setup for typical rotational applications.
|
||||
///
|
||||
/// The created limits are:
|
||||
/// - Angular velocity limits: [-speed, +speed] (if speed is Some), no limits if None
|
||||
/// - Angular acceleration limits: [-acceleration, +acceleration]
|
||||
/// - Angular jerk limits: [-jerk, +jerk]
|
||||
/// - Default tolerances: 1e-6 for both position and speed
|
||||
///
|
||||
/// # Parameters (ordered: speed, acceleration, jerk)
|
||||
/// * `speed` - Optional maximum angular velocity magnitude (None for no limits, Some(x) creates [-x, +x])
|
||||
/// * `acceleration` - Maximum angular acceleration magnitude (creates limits [-acceleration, +acceleration])
|
||||
/// * `jerk` - Maximum angular jerk magnitude (creates limits [-jerk, +jerk])
|
||||
///
|
||||
/// # Returns
|
||||
/// A new AngularJerkSpeedController instance
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if acceleration or jerk values are negative or zero
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
/// f64::{AngularVelocity, AngularAcceleration, AngularJerk}
|
||||
/// };
|
||||
///
|
||||
/// // Create controller with symmetric limits
|
||||
/// let max_speed = AngularVelocity::new::<revolution_per_minute>(2000.0);
|
||||
/// let max_accel = AngularAcceleration::new::<revolution_per_minute_per_second>(1000.0);
|
||||
/// let max_jerk = AngularJerk::new::<revolution_per_minute_per_second_squared>(500.0);
|
||||
///
|
||||
/// let controller = AngularJerkSpeedController::new_simple(
|
||||
/// Some(max_speed), // Angular velocity limits: [-2000, +2000] RPM
|
||||
/// max_accel, // Angular acceleration limits: [-1000, +1000] RPM/s
|
||||
/// max_jerk, // Angular jerk limits: [-500, +500] RPM/s²
|
||||
/// );
|
||||
///
|
||||
/// // Create controller with no speed limits
|
||||
/// let unlimited_controller = AngularJerkSpeedController::new_simple(
|
||||
/// None, // No angular velocity limits
|
||||
/// max_accel, // Angular acceleration limits: [-1000, +1000] RPM/s
|
||||
/// max_jerk, // Angular jerk limits: [-500, +500] RPM/s²
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new_simple(
|
||||
speed: Option<AngularVelocity>,
|
||||
acceleration: AngularAcceleration,
|
||||
jerk: AngularJerk,
|
||||
) -> Self {
|
||||
let controller = JerkSpeedController::new_simple(
|
||||
speed.map(|s| s.get::<radian_per_second>()),
|
||||
acceleration.get::<radian_per_second_squared>(),
|
||||
jerk.get::<radian_per_second_cubed>(),
|
||||
);
|
||||
|
||||
Self {
|
||||
controller,
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the controller with a new target angular velocity
|
||||
///
|
||||
/// Calculates the next angular velocity in the motion profile based on the target and elapsed time.
|
||||
/// The controller automatically manages angular acceleration and jerk to reach the target angular
|
||||
/// velocity while respecting all configured limits.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `target_speed` - The desired target angular velocity
|
||||
/// * `t` - Current timestamp for calculating time delta
|
||||
///
|
||||
/// # Returns
|
||||
/// The current angular velocity after applying the motion profile for this time step
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use std::time::Instant;
|
||||
/// use units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
|
||||
///
|
||||
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
|
||||
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
|
||||
///
|
||||
/// let current_speed = controller.update(target, Instant::now());
|
||||
/// ```
|
||||
pub fn update(&mut self, target_speed: AngularVelocity, t: Instant) -> AngularVelocity {
|
||||
// Calculate dt from the last update
|
||||
let dt = if let Some(last_t) = self.last_update {
|
||||
t.duration_since(last_t).as_secs_f64()
|
||||
} else {
|
||||
0.0 // First update, no time has passed
|
||||
};
|
||||
self.last_update = Some(t);
|
||||
|
||||
// Convert target to raw value, update controller, and convert result back to AngularVelocity
|
||||
let target_raw = target_speed.get::<radian_per_second>();
|
||||
let speed_raw = self.controller.update(target_raw, dt);
|
||||
AngularVelocity::new::<radian_per_second>(speed_raw)
|
||||
}
|
||||
|
||||
/// Get the current angular velocity
|
||||
///
|
||||
/// Returns the controller's current angular velocity in the motion profile.
|
||||
///
|
||||
/// # Returns
|
||||
/// Current angular velocity as an AngularVelocity value
|
||||
pub fn get_speed(&self) -> AngularVelocity {
|
||||
AngularVelocity::new::<radian_per_second>(self.controller.get_speed())
|
||||
}
|
||||
|
||||
/// Get the target angular velocity
|
||||
///
|
||||
/// Returns the current target angular velocity that the controller is trying to reach.
|
||||
///
|
||||
/// # Returns
|
||||
/// Target angular velocity as an AngularVelocity value
|
||||
pub fn get_target_speed(&self) -> AngularVelocity {
|
||||
AngularVelocity::new::<radian_per_second>(self.controller.get_target_speed())
|
||||
}
|
||||
|
||||
/// Get the minimum angular velocity limit
|
||||
///
|
||||
/// Returns the minimum angular velocity constraint, if one is set.
|
||||
///
|
||||
/// # Returns
|
||||
/// Minimum angular velocity limit as an Option<AngularVelocity>, None if no limit is set
|
||||
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
/// Get the maximum angular velocity limit
|
||||
///
|
||||
/// Returns the maximum angular velocity constraint, if one is set.
|
||||
///
|
||||
/// # Returns
|
||||
/// Maximum angular velocity limit as an Option<AngularVelocity>, None if no limit is set
|
||||
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
/// Set the minimum angular velocity limit
|
||||
pub fn set_min_speed(
|
||||
&mut self,
|
||||
min_speed: Option<AngularVelocity>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.map(|speed| speed.get::<radian_per_second>()))
|
||||
}
|
||||
|
||||
/// Set the maximum angular velocity limit
|
||||
pub fn set_max_speed(
|
||||
&mut self,
|
||||
max_speed: Option<AngularVelocity>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.map(|speed| speed.get::<radian_per_second>()))
|
||||
}
|
||||
|
||||
/// Get the current angular acceleration
|
||||
pub fn get_acceleration(&self) -> AngularAcceleration {
|
||||
AngularAcceleration::new::<radian_per_second_squared>(self.controller.get_acceleration())
|
||||
}
|
||||
|
||||
/// Set the minimum angular acceleration
|
||||
pub fn set_min_acceleration(
|
||||
&mut self,
|
||||
min_acceleration: AngularAcceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_acceleration(min_acceleration.get::<radian_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Set the maximum angular acceleration
|
||||
pub fn set_max_acceleration(
|
||||
&mut self,
|
||||
max_acceleration: AngularAcceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_acceleration(max_acceleration.get::<radian_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Get the current angular jerk
|
||||
pub fn get_jerk(&self) -> AngularJerk {
|
||||
AngularJerk::new::<radian_per_second_cubed>(self.controller.get_jerk())
|
||||
}
|
||||
|
||||
/// Set the minimum angular jerk
|
||||
pub fn set_min_jerk(&mut self, min_jerk: AngularJerk) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_jerk(min_jerk.get::<radian_per_second_cubed>())
|
||||
}
|
||||
|
||||
/// Set the maximum angular jerk
|
||||
pub fn set_max_jerk(&mut self, max_jerk: AngularJerk) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_jerk(max_jerk.get::<radian_per_second_cubed>())
|
||||
}
|
||||
|
||||
/// Reset the controller to a new angular velocity and acceleration
|
||||
///
|
||||
/// This resets all internal state including:
|
||||
/// - Current angular velocity to the provided value
|
||||
/// - Current angular acceleration to 0
|
||||
/// - Current angular jerk to 0
|
||||
/// - Target angular velocity to the current velocity
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `velocity`: The new current angular velocity
|
||||
pub fn reset(&mut self, velocity: AngularVelocity) -> Result<(), MotionControllerError> {
|
||||
self.controller.reset(velocity.get::<radian_per_second>())?;
|
||||
self.last_update = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
use super::acceleration_position_controller::{
|
||||
AccelerationPositionController, MotionControllerError,
|
||||
};
|
||||
|
||||
/// Controller for speed-based motion with jerk limits.
|
||||
///
|
||||
/// This controller manages motion profiles where speed is the primary controlled variable,
|
||||
/// with constraints on acceleration (rate of change of speed) and jerk (rate of change of acceleration).
|
||||
/// It wraps an AccelerationPositionController but remaps the parameters to represent one higher
|
||||
/// degree of derivation in the motion hierarchy.
|
||||
///
|
||||
/// The control hierarchy is:
|
||||
/// - **Speed**: The primary controlled variable (analogous to position in position control)
|
||||
/// - **Acceleration**: First derivative of speed (analogous to velocity in position control)
|
||||
/// - **Jerk**: Second derivative of speed, rate of change of acceleration (analogous to acceleration in position control)
|
||||
///
|
||||
/// This controller is particularly useful for applications where you need to control rotational
|
||||
/// or linear speed with smooth acceleration profiles, such as motor control, conveyor systems,
|
||||
/// or any application requiring smooth speed transitions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Create a controller for speed control with jerk limiting
|
||||
/// let controller = JerkSpeedController::new_simple(
|
||||
/// Some(100.0), // Max speed ±100 units/s
|
||||
/// 50.0, // Max acceleration ±50 units/s²
|
||||
/// 25.0, // Max jerk ±25 units/s³
|
||||
/// );
|
||||
///
|
||||
/// // Update towards target speed
|
||||
/// let current_speed = controller.update(80.0, 0.01); // Target 80 units/s, dt=10ms
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct JerkSpeedController {
|
||||
base_controller: AccelerationPositionController,
|
||||
}
|
||||
|
||||
impl JerkSpeedController {
|
||||
/// Create a new speed controller with the given constraints
|
||||
///
|
||||
/// This constructor allows full control over all speed, acceleration, and jerk limits.
|
||||
/// Use this when you need asymmetric limits or fine-grained control over the motion profile.
|
||||
///
|
||||
/// # Parameters (ordered: speed, acceleration, jerk)
|
||||
/// * `min_speed` - Optional minimum allowed speed (None for no limit)
|
||||
/// * `max_speed` - Optional maximum allowed speed (None for no limit)
|
||||
/// * `min_acceleration` - Minimum acceleration (typically negative for deceleration)
|
||||
/// * `max_acceleration` - Maximum acceleration (typically positive)
|
||||
/// * `min_jerk` - Minimum jerk for decreasing acceleration (typically negative)
|
||||
/// * `max_jerk` - Maximum jerk for increasing acceleration (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// A new JerkSpeedController instance
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the underlying AccelerationPositionController cannot be created with the given parameters
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Create controller with asymmetric limits
|
||||
/// let controller = JerkSpeedController::new(
|
||||
/// Some(-50.0), // Min speed: -50 units/s
|
||||
/// Some(100.0), // Max speed: +100 units/s
|
||||
/// -30.0, // Min acceleration: -30 units/s²
|
||||
/// 40.0, // Max acceleration: +40 units/s²
|
||||
/// -20.0, // Min jerk: -20 units/s³
|
||||
/// 25.0, // Max jerk: +25 units/s³
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(
|
||||
min_speed: Option<f64>,
|
||||
max_speed: Option<f64>,
|
||||
min_acceleration: f64,
|
||||
max_acceleration: f64,
|
||||
min_jerk: f64,
|
||||
max_jerk: f64,
|
||||
) -> Self {
|
||||
// Create the base controller with renamed parameters
|
||||
let base_controller = AccelerationPositionController::new(
|
||||
min_jerk, // min_speed in the base controller
|
||||
max_jerk, // max_speed in the base controller
|
||||
min_acceleration, // min_acceleration in the base controller
|
||||
max_acceleration, // max_acceleration in the base controller
|
||||
min_speed, // min_position in the base controller
|
||||
max_speed, // max_position in the base controller
|
||||
1e-6, // position_tolerance
|
||||
1e-6, // speed_tolerance
|
||||
)
|
||||
.expect("Failed to create AccelerationPositionController");
|
||||
|
||||
Self { base_controller }
|
||||
}
|
||||
|
||||
/// Create a new speed controller with simple symmetric limits
|
||||
///
|
||||
/// This is a convenience constructor that creates symmetric limits around zero for all parameters.
|
||||
/// This is the most common use case and simplifies controller setup for typical applications.
|
||||
///
|
||||
/// The created limits are:
|
||||
/// - Speed limits: [-speed, +speed] (if speed is Some), no limits if None
|
||||
/// - Acceleration limits: [-acceleration, +acceleration]
|
||||
/// - Jerk limits: [-jerk, +jerk]
|
||||
/// - Default tolerances: 1e-6 for both position and speed
|
||||
///
|
||||
/// # Parameters (ordered: speed, acceleration, jerk)
|
||||
/// * `speed` - Optional maximum speed magnitude (None for no limits, Some(x) creates [-x, +x])
|
||||
/// * `acceleration` - Maximum acceleration magnitude (creates limits [-acceleration, +acceleration])
|
||||
/// * `jerk` - Maximum jerk magnitude (creates limits [-jerk, +jerk])
|
||||
///
|
||||
/// # Returns
|
||||
/// A new JerkSpeedController instance
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if acceleration or jerk values are negative or zero
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Create controller with symmetric limits
|
||||
/// let controller = JerkSpeedController::new_simple(
|
||||
/// Some(100.0), // Speed limits: [-100, +100] units/s
|
||||
/// 50.0, // Acceleration limits: [-50, +50] units/s²
|
||||
/// 25.0, // Jerk limits: [-25, +25] units/s³
|
||||
/// );
|
||||
///
|
||||
/// // Create controller with no speed limits
|
||||
/// let unlimited_controller = JerkSpeedController::new_simple(
|
||||
/// None, // No speed limits
|
||||
/// 30.0, // Acceleration limits: [-30, +30] units/s²
|
||||
/// 15.0, // Jerk limits: [-15, +15] units/s³
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new_simple(speed: Option<f64>, acceleration: f64, jerk: f64) -> Self {
|
||||
if acceleration <= 0.0 || jerk <= 0.0 {
|
||||
panic!("Acceleration and jerk must be positive values");
|
||||
}
|
||||
|
||||
Self::new(
|
||||
speed.map(|s| -s), // min_speed
|
||||
speed, // max_speed
|
||||
-acceleration, // min_acceleration
|
||||
acceleration, // max_acceleration
|
||||
-jerk, // min_jerk
|
||||
jerk, // max_jerk
|
||||
)
|
||||
}
|
||||
|
||||
/// Update the controller state based on the target speed and time step
|
||||
///
|
||||
/// Calculates the next speed in the motion profile based on the target speed and elapsed time.
|
||||
/// The controller automatically manages acceleration and jerk to reach the target speed
|
||||
/// while respecting all configured limits.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `target_speed` - The desired target speed to reach
|
||||
/// * `dt` - Time step in seconds since the last update
|
||||
///
|
||||
/// # Returns
|
||||
/// The current speed after applying the motion profile for this time step
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the underlying controller update fails (should not happen in normal operation)
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let mut controller = JerkSpeedController::new_simple(Some(100.0), 50.0, 25.0);
|
||||
///
|
||||
/// // Control loop
|
||||
/// loop {
|
||||
/// let current_speed = controller.update(target_speed, 0.01); // 10ms update cycle
|
||||
/// // Use current_speed for your application
|
||||
/// }
|
||||
/// ```
|
||||
pub fn update(&mut self, target_speed: f64, dt: f64) -> f64 {
|
||||
// Call the base controller's update method with the target speed
|
||||
self.base_controller
|
||||
.update(target_speed, dt)
|
||||
.expect("Failed to update controller")
|
||||
}
|
||||
|
||||
/// Get the current speed
|
||||
///
|
||||
/// Returns the controller's current speed in the motion profile.
|
||||
///
|
||||
/// # Returns
|
||||
/// Current speed as a floating-point value
|
||||
pub const fn get_speed(&self) -> f64 {
|
||||
self.base_controller.get_position()
|
||||
}
|
||||
|
||||
/// Get the current acceleration
|
||||
///
|
||||
/// Returns the controller's current acceleration (rate of change of speed).
|
||||
///
|
||||
/// # Returns
|
||||
/// Current acceleration as a floating-point value
|
||||
pub const fn get_acceleration(&self) -> f64 {
|
||||
self.base_controller.get_speed()
|
||||
}
|
||||
|
||||
/// Get the current jerk
|
||||
///
|
||||
/// Returns the controller's current jerk (rate of change of acceleration).
|
||||
///
|
||||
/// # Returns
|
||||
/// Current jerk as a floating-point value
|
||||
pub const fn get_jerk(&self) -> f64 {
|
||||
self.base_controller.get_acceleration()
|
||||
}
|
||||
|
||||
/// Get the target speed
|
||||
///
|
||||
/// Returns the current target speed that the controller is trying to reach.
|
||||
///
|
||||
/// # Returns
|
||||
/// Target speed as a floating-point value
|
||||
pub const fn get_target_speed(&self) -> f64 {
|
||||
self.base_controller.get_target_position()
|
||||
}
|
||||
|
||||
/// Get the minimum speed limit
|
||||
///
|
||||
/// Returns the minimum speed constraint, if one is set.
|
||||
///
|
||||
/// # Returns
|
||||
/// Minimum speed limit as an Option<f64>, None if no limit is set
|
||||
pub const fn get_min_speed(&self) -> Option<f64> {
|
||||
self.base_controller.get_min_position()
|
||||
}
|
||||
|
||||
/// Get the maximum speed limit
|
||||
///
|
||||
/// Returns the maximum speed constraint, if one is set.
|
||||
///
|
||||
/// # Returns
|
||||
/// Maximum speed limit as an Option<f64>, None if no limit is set
|
||||
pub const fn get_max_speed(&self) -> Option<f64> {
|
||||
self.base_controller.get_max_position()
|
||||
}
|
||||
|
||||
/// Set the minimum speed limit
|
||||
///
|
||||
/// Updates the minimum speed constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_speed` - New minimum speed limit (None to remove limit)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., greater than the maximum speed limit)
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<f64>) -> Result<(), MotionControllerError> {
|
||||
self.base_controller.set_min_position(min_speed)
|
||||
}
|
||||
|
||||
/// Set the maximum speed limit
|
||||
///
|
||||
/// Updates the maximum speed constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `max_speed` - New maximum speed limit (None to remove limit)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., less than the minimum speed limit)
|
||||
pub fn set_max_speed(&mut self, max_speed: Option<f64>) -> Result<(), MotionControllerError> {
|
||||
self.base_controller.set_max_position(max_speed)
|
||||
}
|
||||
|
||||
/// Set the minimum acceleration
|
||||
///
|
||||
/// Updates the minimum acceleration constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_acceleration` - New minimum acceleration limit (typically negative for deceleration)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., greater than the maximum acceleration limit)
|
||||
pub fn set_min_acceleration(
|
||||
&mut self,
|
||||
min_acceleration: f64,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.base_controller.set_min_speed(min_acceleration)
|
||||
}
|
||||
|
||||
/// Set the maximum acceleration
|
||||
///
|
||||
/// Updates the maximum acceleration constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `max_acceleration` - New maximum acceleration limit (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., less than the minimum acceleration limit or negative/zero value)
|
||||
pub fn set_max_acceleration(
|
||||
&mut self,
|
||||
max_acceleration: f64,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.base_controller.set_max_speed(max_acceleration)
|
||||
}
|
||||
|
||||
/// Set the minimum jerk
|
||||
///
|
||||
/// Updates the minimum jerk constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_jerk` - New minimum jerk limit (typically negative for decreasing acceleration)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., greater than the maximum jerk limit)
|
||||
pub fn set_min_jerk(&mut self, min_jerk: f64) -> Result<(), MotionControllerError> {
|
||||
self.base_controller.set_min_acceleration(min_jerk)
|
||||
}
|
||||
|
||||
/// Set the maximum jerk
|
||||
///
|
||||
/// Updates the maximum jerk constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `max_jerk` - New maximum jerk limit (typically positive for increasing acceleration)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., less than the minimum jerk limit or negative/zero value)
|
||||
pub fn set_max_jerk(&mut self, max_jerk: f64) -> Result<(), MotionControllerError> {
|
||||
self.base_controller.set_max_acceleration(max_jerk)
|
||||
}
|
||||
|
||||
/// Reset the controller to a new speed and acceleration
|
||||
///
|
||||
/// This resets all internal state of the controller to start fresh from a new state.
|
||||
/// The controller will reset its speed, clear any accumulated motion profile state,
|
||||
/// and set the target speed to the current speed.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `speed` - The new current speed to reset to
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the reset operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the speed is outside the configured limits
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let mut controller = JerkSpeedController::new_simple(Some(100.0), 50.0, 25.0);
|
||||
///
|
||||
/// // Reset to zero speed with zero acceleration
|
||||
/// controller.reset(0.0)?;
|
||||
///
|
||||
/// // Reset to specific speed
|
||||
/// controller.reset(25.0)?;
|
||||
/// ```
|
||||
pub fn reset(&mut self, speed: f64) -> Result<(), MotionControllerError> {
|
||||
// Reset to the speed position (treating speed as position in the base controller)
|
||||
self.base_controller.reset(speed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Length, Velocity},
|
||||
length::meter,
|
||||
velocity::meter_per_second,
|
||||
};
|
||||
|
||||
use super::acceleration_position_controller::{
|
||||
AccelerationPositionController, MotionControllerError,
|
||||
};
|
||||
|
||||
/// Linear Acceleration Position Controller with proper physical units
|
||||
///
|
||||
/// This controller provides linear motion control with proper SI units (meters, m/s, m/s²).
|
||||
/// It wraps the core AccelerationPositionController and provides unit-typed interfaces
|
||||
/// for position (Length), velocity (Velocity), and acceleration (Acceleration).
|
||||
///
|
||||
/// The controller manages smooth motion profiles with configurable limits and tolerances,
|
||||
/// ensuring that position, speed, and acceleration constraints are respected during motion.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use units::f64::{Length, Velocity, Acceleration};
|
||||
///
|
||||
/// let position_limit = Length::new::<meter>(10.0);
|
||||
/// let max_speed = Velocity::new::<meter_per_second>(2.0);
|
||||
/// let max_acceleration = Acceleration::new::<meter_per_second_squared>(5.0);
|
||||
///
|
||||
/// let controller = LinearAccelerationPositionController::new_simple(
|
||||
/// Some(position_limit),
|
||||
/// max_speed,
|
||||
/// max_acceleration,
|
||||
/// ).unwrap();
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct LinearAccelerationPositionController {
|
||||
controller: AccelerationPositionController,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
impl LinearAccelerationPositionController {
|
||||
/// Create a new linear position controller with acceleration limits
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_position` - Minimum position limit (optional)
|
||||
/// * `max_position` - Maximum position limit (optional)
|
||||
/// * `min_speed` - Minimum speed limit (typically negative)
|
||||
/// * `max_speed` - Maximum speed limit (typically positive)
|
||||
/// * `min_acceleration` - Minimum acceleration limit (typically negative for deceleration)
|
||||
/// * `max_acceleration` - Maximum acceleration limit (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// A new LinearAccelerationPositionController instance
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the underlying AccelerationPositionController cannot be created with the given parameters
|
||||
pub fn new(
|
||||
min_position: Option<Length>,
|
||||
max_position: Option<Length>,
|
||||
min_speed: Velocity,
|
||||
max_speed: Velocity,
|
||||
min_acceleration: Acceleration,
|
||||
max_acceleration: Acceleration,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationPositionController::new(
|
||||
min_speed.get::<meter_per_second>(),
|
||||
max_speed.get::<meter_per_second>(),
|
||||
min_acceleration.get::<meter_per_second_squared>(),
|
||||
max_acceleration.get::<meter_per_second_squared>(),
|
||||
min_position.map(|length| length.get::<meter>()),
|
||||
max_position.map(|length| length.get::<meter>()),
|
||||
1e-6, // position_tolerance
|
||||
1e-6, // speed_tolerance
|
||||
)
|
||||
.expect("Failed to create AccelerationPositionController"),
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new simple linear acceleration position controller with symmetric limits
|
||||
///
|
||||
/// This is a convenience constructor that creates symmetric limits around zero for
|
||||
/// position, speed, and acceleration. This is useful for most common use cases.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `position` - Maximum position limit (creates [-position, +position] range if Some)
|
||||
/// * `speed` - Maximum speed limit (creates [-speed, +speed] range)
|
||||
/// * `acceleration` - Maximum acceleration limit (creates [-acceleration, +acceleration] range)
|
||||
///
|
||||
/// # Returns
|
||||
/// A Result containing the new controller or a MotionControllerError
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if:
|
||||
/// - Speed or acceleration values are negative or zero
|
||||
/// - The underlying controller cannot be created with the calculated limits
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use units::f64::{Length, Velocity, Acceleration};
|
||||
///
|
||||
/// let position_limit = Length::new::<meter>(5.0); // Creates [-5.0, +5.0] meter range
|
||||
/// let max_speed = Velocity::new::<meter_per_second>(1.0); // Creates [-1.0, +1.0] m/s range
|
||||
/// let max_accel = Acceleration::new::<meter_per_second_squared>(2.0); // Creates [-2.0, +2.0] m/s² range
|
||||
///
|
||||
/// let controller = LinearAccelerationPositionController::new_simple(
|
||||
/// Some(position_limit),
|
||||
/// max_speed,
|
||||
/// max_accel,
|
||||
/// )?;
|
||||
/// ```
|
||||
pub fn new_simple(
|
||||
position: Option<Length>,
|
||||
speed: Velocity,
|
||||
acceleration: Acceleration,
|
||||
) -> Result<Self, MotionControllerError> {
|
||||
let controller = AccelerationPositionController::new_simple(
|
||||
position.map(|p| p.get::<meter>()),
|
||||
speed.get::<meter_per_second>(),
|
||||
acceleration.get::<meter_per_second_squared>(),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
controller,
|
||||
last_update: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the controller with a new target position
|
||||
///
|
||||
/// Calculates the next position in the motion profile based on the target position
|
||||
/// and elapsed time since the last update. The controller automatically manages
|
||||
/// acceleration and velocity to reach the target while respecting all limits.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `target_position` - The desired target position
|
||||
/// * `t` - Current timestamp for calculating time delta
|
||||
///
|
||||
/// # Returns
|
||||
/// The current position after applying the motion profile for this time step
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the underlying controller update fails (should not happen in normal operation)
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use std::time::Instant;
|
||||
/// use units::{length::meter, f64::Length};
|
||||
///
|
||||
/// let mut controller = LinearAccelerationPositionController::new_simple(
|
||||
/// Some(Length::new::<meter>(10.0)),
|
||||
/// // ... other parameters
|
||||
/// )?;
|
||||
///
|
||||
/// let target = Length::new::<meter>(5.0);
|
||||
/// let current_position = controller.update(target, Instant::now());
|
||||
/// ```
|
||||
pub fn update(&mut self, target_position: Length, t: Instant) -> Length {
|
||||
// Calculate dt from the last update
|
||||
let dt = if let Some(last_t) = self.last_update {
|
||||
t.duration_since(last_t).as_secs_f64()
|
||||
} else {
|
||||
0.0 // First update, no time has passed
|
||||
};
|
||||
self.last_update = Some(t);
|
||||
|
||||
// Convert target to raw value, update controller, and convert result back to Length
|
||||
let target_raw = target_position.get::<meter>();
|
||||
let position_raw = self
|
||||
.controller
|
||||
.update(target_raw, dt)
|
||||
.expect("Failed to update controller");
|
||||
Length::new::<meter>(position_raw)
|
||||
}
|
||||
|
||||
/// Get the current position
|
||||
///
|
||||
/// Returns the controller's current position in the motion profile.
|
||||
///
|
||||
/// # Returns
|
||||
/// Current position as a Length value
|
||||
pub fn get_position(&self) -> Length {
|
||||
Length::new::<meter>(self.controller.get_position())
|
||||
}
|
||||
|
||||
/// Get the target position
|
||||
///
|
||||
/// Returns the current target position that the controller is trying to reach.
|
||||
///
|
||||
/// # Returns
|
||||
/// Target position as a Length value
|
||||
pub fn get_target_position(&self) -> Length {
|
||||
Length::new::<meter>(self.controller.get_target_position())
|
||||
}
|
||||
|
||||
/// Get the minimum position limit
|
||||
///
|
||||
/// Returns the minimum position constraint, if one is set.
|
||||
///
|
||||
/// # Returns
|
||||
/// Minimum position limit as an Option<Length>, None if no limit is set
|
||||
pub fn get_min_position(&self) -> Option<Length> {
|
||||
self.controller.get_min_position().map(Length::new::<meter>)
|
||||
}
|
||||
|
||||
/// Get the maximum position limit
|
||||
///
|
||||
/// Returns the maximum position constraint, if one is set.
|
||||
///
|
||||
/// # Returns
|
||||
/// Maximum position limit as an Option<Length>, None if no limit is set
|
||||
pub fn get_max_position(&self) -> Option<Length> {
|
||||
self.controller.get_max_position().map(Length::new::<meter>)
|
||||
}
|
||||
|
||||
/// Set the minimum position limit
|
||||
///
|
||||
/// Updates the minimum position constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_position` - New minimum position limit (None to remove limit)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., greater than the maximum position limit)
|
||||
pub fn set_min_position(
|
||||
&mut self,
|
||||
min_position: Option<Length>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_position(min_position.map(|length| length.get::<meter>()))
|
||||
}
|
||||
|
||||
/// Set the maximum position limit
|
||||
///
|
||||
/// Updates the maximum position constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `max_position` - New maximum position limit (None to remove limit)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., less than the minimum position limit)
|
||||
pub fn set_max_position(
|
||||
&mut self,
|
||||
max_position: Option<Length>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_position(max_position.map(|length| length.get::<meter>()))
|
||||
}
|
||||
|
||||
/// Get the current speed
|
||||
///
|
||||
/// Returns the controller's current velocity in the motion profile.
|
||||
///
|
||||
/// # Returns
|
||||
/// Current velocity as a Velocity value
|
||||
pub fn get_speed(&self) -> Velocity {
|
||||
Velocity::new::<meter_per_second>(self.controller.get_speed())
|
||||
}
|
||||
|
||||
/// Set the minimum speed
|
||||
///
|
||||
/// Updates the minimum velocity constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_speed` - New minimum speed limit (typically negative for reverse motion)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., greater than the maximum speed limit)
|
||||
pub fn set_min_speed(&mut self, min_speed: Velocity) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.get::<meter_per_second>())
|
||||
}
|
||||
|
||||
/// Set the maximum speed
|
||||
///
|
||||
/// Updates the maximum velocity constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `max_speed` - New maximum speed limit (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., less than the minimum speed limit or negative/zero value)
|
||||
pub fn set_max_speed(&mut self, max_speed: Velocity) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.get::<meter_per_second>())
|
||||
}
|
||||
|
||||
/// Get the current acceleration
|
||||
///
|
||||
/// Returns the controller's current acceleration in the motion profile.
|
||||
///
|
||||
/// # Returns
|
||||
/// Current acceleration as an Acceleration value
|
||||
pub fn get_acceleration(&self) -> Acceleration {
|
||||
Acceleration::new::<meter_per_second_squared>(self.controller.get_acceleration())
|
||||
}
|
||||
|
||||
/// Set the minimum acceleration
|
||||
///
|
||||
/// Updates the minimum acceleration constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `min_acceleration` - New minimum acceleration limit (typically negative for deceleration)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., greater than the maximum acceleration limit)
|
||||
pub fn set_min_acceleration(
|
||||
&mut self,
|
||||
min_acceleration: Acceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_acceleration(min_acceleration.get::<meter_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Set the maximum acceleration
|
||||
///
|
||||
/// Updates the maximum acceleration constraint for the controller.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `max_acceleration` - New maximum acceleration limit (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the new limit would be invalid
|
||||
/// (e.g., less than the minimum acceleration limit or negative/zero value)
|
||||
pub fn set_max_acceleration(
|
||||
&mut self,
|
||||
max_acceleration: Acceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_acceleration(max_acceleration.get::<meter_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Reset the controller to a new position and velocity
|
||||
///
|
||||
/// This resets all internal state of the controller to start fresh from a new state.
|
||||
/// The controller will reset its position, clear any accumulated motion profile state,
|
||||
/// and set the target position to the current position.
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `position` - The new current position to reset to
|
||||
///
|
||||
/// # Returns
|
||||
/// Result indicating success or failure of the reset operation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns MotionControllerError if the position is outside the configured limits
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
|
||||
///
|
||||
/// let new_position = Length::new::<meter>(0.0);
|
||||
///
|
||||
/// controller.reset(new_position)?;
|
||||
/// ```
|
||||
pub fn reset(&mut self, position: Length) -> Result<(), MotionControllerError> {
|
||||
self.controller.reset(position.get::<meter>())?;
|
||||
self.last_update = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Jerk, Velocity},
|
||||
jerk::meter_per_second_cubed,
|
||||
velocity::meter_per_second,
|
||||
};
|
||||
|
||||
use super::acceleration_position_controller::MotionControllerError;
|
||||
use super::jerk_speed_controller::JerkSpeedController;
|
||||
|
||||
/// Linear Jerk Speed Controller with proper physical units
|
||||
///
|
||||
/// This controller provides linear motion control with proper SI units for translational systems.
|
||||
/// It wraps the core JerkSpeedController and provides unit-typed interfaces for velocity (Velocity),
|
||||
/// acceleration (Acceleration), and jerk (Jerk).
|
||||
///
|
||||
/// The controller manages smooth linear speed profiles with configurable limits and tolerances,
|
||||
/// ensuring that velocity, acceleration, and jerk constraints are respected during motion.
|
||||
/// This is particularly useful for linear actuators, conveyor systems, CNC machines, and any
|
||||
/// translational machinery requiring smooth speed transitions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
/// f64::{Velocity, Acceleration, Jerk}
|
||||
/// };
|
||||
///
|
||||
/// let max_speed = Velocity::new::<meter_per_second>(5.0);
|
||||
/// let max_accel = Acceleration::new::<meter_per_second_squared>(2.0);
|
||||
/// let max_jerk = Jerk::new::<meter_per_second_cubed>(1.0);
|
||||
///
|
||||
/// let controller = LinearJerkSpeedController::new_simple(
|
||||
/// Some(max_speed),
|
||||
/// max_accel,
|
||||
/// max_jerk,
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct LinearJerkSpeedController {
|
||||
controller: JerkSpeedController,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
impl LinearJerkSpeedController {
|
||||
/// Create a new linear speed controller with jerk limits
|
||||
///
|
||||
/// This constructor allows full control over all velocity, acceleration, and jerk limits.
|
||||
/// Use this when you need asymmetric limits or fine-grained control over the linear motion profile.
|
||||
///
|
||||
/// # Parameters (ordered: speed, acceleration, jerk)
|
||||
/// * `min_speed` - Optional minimum allowed velocity (None for no limit)
|
||||
/// * `max_speed` - Optional maximum allowed velocity (None for no limit)
|
||||
/// * `min_acceleration` - Minimum acceleration (typically negative for deceleration)
|
||||
/// * `max_acceleration` - Maximum acceleration (typically positive)
|
||||
/// * `min_jerk` - Minimum jerk for decreasing acceleration (typically negative)
|
||||
/// * `max_jerk` - Maximum jerk for increasing acceleration (typically positive)
|
||||
///
|
||||
/// # Returns
|
||||
/// A new LinearJerkSpeedController instance
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
/// f64::{Velocity, Acceleration, Jerk}
|
||||
/// };
|
||||
///
|
||||
/// // Create controller with asymmetric limits
|
||||
/// let controller = LinearJerkSpeedController::new(
|
||||
/// Some(Velocity::new::<meter_per_second>(-2.0)), // Min: -2 m/s
|
||||
/// Some(Velocity::new::<meter_per_second>(5.0)), // Max: +5 m/s
|
||||
/// Acceleration::new::<meter_per_second_squared>(-3.0), // Min accel
|
||||
/// Acceleration::new::<meter_per_second_squared>(4.0), // Max accel
|
||||
/// Jerk::new::<meter_per_second_cubed>(-2.0), // Min jerk
|
||||
/// Jerk::new::<meter_per_second_cubed>(3.0), // Max jerk
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(
|
||||
min_speed: Option<Velocity>,
|
||||
max_speed: Option<Velocity>,
|
||||
min_acceleration: Acceleration,
|
||||
max_acceleration: Acceleration,
|
||||
min_jerk: Jerk,
|
||||
max_jerk: Jerk,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: JerkSpeedController::new(
|
||||
min_speed.map(|speed| speed.get::<meter_per_second>()),
|
||||
max_speed.map(|speed| speed.get::<meter_per_second>()),
|
||||
min_acceleration.get::<meter_per_second_squared>(),
|
||||
max_acceleration.get::<meter_per_second_squared>(),
|
||||
min_jerk.get::<meter_per_second_cubed>(),
|
||||
max_jerk.get::<meter_per_second_cubed>(),
|
||||
),
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new simple linear jerk speed controller with symmetric limits
|
||||
///
|
||||
/// This is a convenience constructor that creates symmetric limits around zero for all parameters.
|
||||
/// This is the most common use case and simplifies controller setup for typical linear applications.
|
||||
///
|
||||
/// The created limits are:
|
||||
/// - Velocity limits: [-speed, +speed] (if speed is Some), no limits if None
|
||||
/// - Acceleration limits: [-acceleration, +acceleration]
|
||||
/// - Jerk limits: [-jerk, +jerk]
|
||||
/// - Default tolerances: 1e-6 for both position and speed
|
||||
///
|
||||
/// # Parameters (ordered: speed, acceleration, jerk)
|
||||
/// * `speed` - Optional maximum velocity magnitude (None for no limits, Some(x) creates [-x, +x])
|
||||
/// * `acceleration` - Maximum acceleration magnitude (creates limits [-acceleration, +acceleration])
|
||||
/// * `jerk` - Maximum jerk magnitude (creates limits [-jerk, +jerk])
|
||||
///
|
||||
/// # Returns
|
||||
/// A new LinearJerkSpeedController instance
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if acceleration or jerk values are negative or zero
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
/// f64::{Velocity, Acceleration, Jerk}
|
||||
/// };
|
||||
///
|
||||
/// // Create controller with symmetric limits
|
||||
/// let max_speed = Velocity::new::<meter_per_second>(3.0);
|
||||
/// let max_accel = Acceleration::new::<meter_per_second_squared>(2.0);
|
||||
/// let max_jerk = Jerk::new::<meter_per_second_cubed>(1.5);
|
||||
///
|
||||
/// let controller = LinearJerkSpeedController::new_simple(
|
||||
/// Some(max_speed), // Velocity limits: [-3, +3] m/s
|
||||
/// max_accel, // Acceleration limits: [-2, +2] m/s²
|
||||
/// max_jerk, // Jerk limits: [-1.5, +1.5] m/s³
|
||||
/// );
|
||||
///
|
||||
/// // Create controller with no speed limits
|
||||
/// let unlimited_controller = LinearJerkSpeedController::new_simple(
|
||||
/// None, // No velocity limits
|
||||
/// max_accel, // Acceleration limits: [-2, +2] m/s²
|
||||
/// max_jerk, // Jerk limits: [-1.5, +1.5] m/s³
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new_simple(speed: Option<Velocity>, acceleration: Acceleration, jerk: Jerk) -> Self {
|
||||
let controller = JerkSpeedController::new_simple(
|
||||
speed.map(|s| s.get::<meter_per_second>()),
|
||||
acceleration.get::<meter_per_second_squared>(),
|
||||
jerk.get::<meter_per_second_cubed>(),
|
||||
);
|
||||
|
||||
Self {
|
||||
controller,
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the controller with a new target speed
|
||||
pub fn update(&mut self, target_speed: Velocity, t: Instant) -> Velocity {
|
||||
// Calculate dt from the last update
|
||||
let dt = if let Some(last_t) = self.last_update {
|
||||
t.duration_since(last_t).as_secs_f64()
|
||||
} else {
|
||||
0.0 // First update, no time has passed
|
||||
};
|
||||
self.last_update = Some(t);
|
||||
|
||||
// Convert target to raw value, update controller, and convert result back to Velocity
|
||||
let target_raw = target_speed.get::<meter_per_second>();
|
||||
let speed_raw = self.controller.update(target_raw, dt);
|
||||
Velocity::new::<meter_per_second>(speed_raw)
|
||||
}
|
||||
|
||||
/// Get the current speed
|
||||
pub fn get_speed(&self) -> Velocity {
|
||||
Velocity::new::<meter_per_second>(self.controller.get_speed())
|
||||
}
|
||||
|
||||
/// Get the target speed
|
||||
pub fn get_target_speed(&self) -> Velocity {
|
||||
Velocity::new::<meter_per_second>(self.controller.get_target_speed())
|
||||
}
|
||||
|
||||
/// Get the minimum velocity limit
|
||||
pub fn get_min_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
|
||||
/// Get the maximum velocity limit
|
||||
pub fn get_max_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
|
||||
/// Set the minimum velocity limit
|
||||
pub fn set_min_speed(
|
||||
&mut self,
|
||||
min_speed: Option<Velocity>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.map(|speed| speed.get::<meter_per_second>()))
|
||||
}
|
||||
|
||||
/// Set the maximum velocity limit
|
||||
pub fn set_max_speed(
|
||||
&mut self,
|
||||
max_speed: Option<Velocity>,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.map(|speed| speed.get::<meter_per_second>()))
|
||||
}
|
||||
|
||||
/// Get the current acceleration
|
||||
pub fn get_acceleration(&self) -> Acceleration {
|
||||
Acceleration::new::<meter_per_second_squared>(self.controller.get_acceleration())
|
||||
}
|
||||
|
||||
/// Set the minimum acceleration
|
||||
pub fn set_min_acceleration(
|
||||
&mut self,
|
||||
min_acceleration: Acceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_acceleration(min_acceleration.get::<meter_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Set the maximum acceleration
|
||||
pub fn set_max_acceleration(
|
||||
&mut self,
|
||||
max_acceleration: Acceleration,
|
||||
) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_acceleration(max_acceleration.get::<meter_per_second_squared>())
|
||||
}
|
||||
|
||||
/// Get the current jerk
|
||||
pub fn get_jerk(&self) -> Jerk {
|
||||
Jerk::new::<meter_per_second_cubed>(self.controller.get_jerk())
|
||||
}
|
||||
|
||||
/// Set the minimum jerk
|
||||
pub fn set_min_jerk(&mut self, min_jerk: Jerk) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_min_jerk(min_jerk.get::<meter_per_second_cubed>())
|
||||
}
|
||||
|
||||
/// Set the maximum jerk
|
||||
pub fn set_max_jerk(&mut self, max_jerk: Jerk) -> Result<(), MotionControllerError> {
|
||||
self.controller
|
||||
.set_max_jerk(max_jerk.get::<meter_per_second_cubed>())
|
||||
}
|
||||
|
||||
/// Reset the controller to a new velocity and acceleration
|
||||
///
|
||||
/// This resets all internal state including:
|
||||
/// - Current velocity to the provided value
|
||||
/// - Current acceleration to 0
|
||||
/// - Current jerk to 0
|
||||
/// - Target velocity to the current velocity
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `velocity`: The new current velocity
|
||||
pub fn reset(&mut self, velocity: Velocity) -> Result<(), MotionControllerError> {
|
||||
self.controller.reset(velocity.get::<meter_per_second>())?;
|
||||
self.last_update = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod acceleration_position_controller;
|
||||
pub mod angular_acceleration_position_controller;
|
||||
pub mod angular_jerk_speed_controller;
|
||||
pub mod jerk_speed_controller;
|
||||
pub mod linear_acceleration_position_controller;
|
||||
pub mod linear_jerk_speed_controller;
|
||||
@@ -0,0 +1,297 @@
|
||||
use units::{angle::radian, f64::Angle};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AngleConverter {
|
||||
pub flip_x: bool, // true to flip X-axis (mirror horizontally)
|
||||
pub flip_y: bool, // true to flip Y-axis (mirror vertically)
|
||||
pub is_cw: bool, // true for clockwise, false for counter-clockwise
|
||||
}
|
||||
|
||||
impl AngleConverter {
|
||||
/// Create a custom coordinate system
|
||||
pub const fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
|
||||
Self {
|
||||
flip_x,
|
||||
flip_y,
|
||||
is_cw,
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard mathematical coordinate system (CCW positive, 0° at positive X)
|
||||
pub const fn mathematical() -> Self {
|
||||
Self::new(false, false, false)
|
||||
}
|
||||
|
||||
/// Screen/graphics coordinate system (CW positive, 0° at positive X, Y-flipped)
|
||||
pub const fn screen() -> Self {
|
||||
Self::new(false, true, false)
|
||||
}
|
||||
|
||||
/// System where 0° points up, clockwise positive
|
||||
pub const fn y_up_cw() -> Self {
|
||||
Self::new(false, false, true)
|
||||
}
|
||||
|
||||
/// System where 0° points down, counter-clockwise positive
|
||||
pub const fn y_down_ccw() -> Self {
|
||||
Self::new(false, true, true)
|
||||
}
|
||||
|
||||
/// System where 0° points left, clockwise positive
|
||||
pub const fn x_left_cw() -> Self {
|
||||
Self::new(true, false, false)
|
||||
}
|
||||
|
||||
/// System where 0° points right, counter-clockwise positive (same as mathematical)
|
||||
pub const fn x_right_ccw() -> Self {
|
||||
Self::mathematical()
|
||||
}
|
||||
|
||||
/// System where 0° points up, counter-clockwise positive
|
||||
pub const fn y_up_ccw() -> Self {
|
||||
Self::new(false, false, false)
|
||||
}
|
||||
|
||||
/// System where 0° points down, clockwise positive
|
||||
pub const fn y_down_cw() -> Self {
|
||||
Self::new(false, true, false)
|
||||
}
|
||||
|
||||
/// System where 0° points left, counter-clockwise positive
|
||||
pub const fn x_left_ccw() -> Self {
|
||||
Self::new(true, false, true)
|
||||
}
|
||||
|
||||
/// Convert angle from mathematical system to this system (f32)
|
||||
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle(math_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Apply axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result += 180.0, // Both flips: 180° rotation
|
||||
}
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
self.normalize_angle(result)
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f32)
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle(system_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
// Apply inverse axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result -= 180.0, // Both flips: inverse 180° rotation
|
||||
}
|
||||
|
||||
self.normalize_angle(result)
|
||||
}
|
||||
|
||||
/// Convert angle in radians from mathematical system to this system (f32)
|
||||
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
|
||||
let degrees = math_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_encode(degrees);
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle in radians from this system to mathematical system (f32)
|
||||
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
|
||||
let degrees = system_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_decode(degrees);
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle from mathematical system to this system (f64)
|
||||
pub fn degrees_encode_f64(&self, math_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle_f64(math_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Apply axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result += 180.0, // Both flips: 180° rotation
|
||||
}
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
self.normalize_angle_f64(result)
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f64)
|
||||
pub fn degrees_decode_f64(&self, system_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle_f64(system_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
// Apply inverse axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result -= 180.0, // Both flips: inverse 180° rotation
|
||||
}
|
||||
|
||||
self.normalize_angle_f64(result)
|
||||
}
|
||||
|
||||
/// Convert angle in radians from mathematical system to this system (f64)
|
||||
pub fn radians_encode_f64(&self, math_angle_radians: f64) -> f64 {
|
||||
let degrees = math_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_encode_f64(degrees);
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle in radians from this system to mathematical system (f64)
|
||||
pub fn radians_decode_f64(&self, system_angle_radians: f64) -> f64 {
|
||||
let degrees = system_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_decode_f64(degrees);
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f32)
|
||||
fn normalize_angle(&self, angle: f64) -> f64 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f64)
|
||||
fn normalize_angle_f64(&self, angle: f64) -> f64 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
}
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AngleConverterUom {
|
||||
angle_converter: AngleConverter,
|
||||
}
|
||||
|
||||
impl AngleConverterUom {
|
||||
pub const fn new(angle_converter: AngleConverter) -> Self {
|
||||
Self { angle_converter }
|
||||
}
|
||||
|
||||
pub fn encode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
|
||||
}
|
||||
|
||||
pub fn decode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_constructors() {
|
||||
let math = AngleConverter::mathematical();
|
||||
assert_eq!(math.flip_x, false);
|
||||
assert_eq!(math.flip_y, false);
|
||||
assert_eq!(math.is_cw, false);
|
||||
|
||||
let screen = AngleConverter::screen();
|
||||
assert_eq!(screen.flip_x, false);
|
||||
assert_eq!(screen.flip_y, true);
|
||||
assert_eq!(screen.is_cw, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_screen_converter_bidirectional() {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Math to screen (Y-flip only)
|
||||
assert_eq!(converter.degrees_encode(0.0), 0.0); // Right stays right
|
||||
assert_eq!(converter.degrees_encode(90.0), 270.0); // Up becomes down
|
||||
assert_eq!(converter.degrees_encode(270.0), 90.0); // Down becomes up
|
||||
assert_eq!(converter.degrees_encode(180.0), 180.0); // Left stays left
|
||||
|
||||
// Screen to math (should be inverse)
|
||||
assert_eq!(converter.degrees_decode(0.0), 0.0); // Right stays right
|
||||
assert_eq!(converter.degrees_decode(270.0), 90.0); // Down becomes up
|
||||
assert_eq!(converter.degrees_decode(90.0), 270.0); // Up becomes down
|
||||
assert_eq!(converter.degrees_decode(180.0), 180.0); // Left stays left
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_x_flip_converter() {
|
||||
let converter = AngleConverter::x_left_cw();
|
||||
|
||||
// Math to x-flipped system
|
||||
assert_eq!(converter.degrees_encode(0.0), 180.0); // Right becomes left
|
||||
assert_eq!(converter.degrees_encode(90.0), 90.0); // Up stays up
|
||||
assert_eq!(converter.degrees_encode(180.0), 0.0); // Left becomes right
|
||||
assert_eq!(converter.degrees_encode(270.0), 270.0); // Down stays down
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_both_flips_converter() {
|
||||
let converter = AngleConverter::new(true, true, false);
|
||||
|
||||
// Math to both-flipped system (180° rotation)
|
||||
assert_eq!(converter.degrees_encode(0.0), 180.0); // Right becomes left
|
||||
assert_eq!(converter.degrees_encode(90.0), 270.0); // Up becomes down
|
||||
assert_eq!(converter.degrees_encode(180.0), 0.0); // Left becomes right
|
||||
assert_eq!(converter.degrees_encode(270.0), 90.0); // Down becomes up
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radians_conversion() {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Test radians_encode
|
||||
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
|
||||
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
|
||||
assert!((result - expected).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_conversion() {
|
||||
let converter = AngleConverter::new(true, false, true); // X-flip + CW
|
||||
|
||||
let original = 75.0_f64;
|
||||
let converted = converter.degrees_encode(original);
|
||||
let roundtrip = converter.degrees_decode(converted);
|
||||
|
||||
assert!((original - roundtrip as f64).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
+53
-26
@@ -1,55 +1,82 @@
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::revolution,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::revolution_per_second,
|
||||
f64::{Angle, AngularAcceleration, AngularVelocity},
|
||||
};
|
||||
|
||||
/// Converts between motor steps and angular measurements
|
||||
///
|
||||
/// This converter handles the transformation between motor steps and angular
|
||||
/// measurements (angle, angular velocity, and angular acceleration) based on
|
||||
/// the number of steps required for a full revolution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StepConverter {
|
||||
steps_per_revolution: i16,
|
||||
pub struct AngularStepConverter {
|
||||
/// Number of motor steps required for one complete revolution
|
||||
pub steps_per_revolution: i16,
|
||||
}
|
||||
|
||||
impl StepConverter {
|
||||
pub fn new(steps_per_revolution: i16) -> Self {
|
||||
impl AngularStepConverter {
|
||||
/// Create a new converter with the specified steps per revolution
|
||||
pub const fn new(steps_per_revolution: i16) -> Self {
|
||||
Self {
|
||||
steps_per_revolution,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert steps to angle
|
||||
///
|
||||
/// Formula: angle (in revolutions) = steps / steps_per_revolution
|
||||
pub fn steps_to_angle(&self, steps: f64) -> Angle {
|
||||
let revolutions = steps / self.steps_per_revolution as f64;
|
||||
Angle::new::<revolution>(revolutions)
|
||||
}
|
||||
|
||||
/// Convert angle to steps
|
||||
///
|
||||
/// Formula: steps = angle (in revolutions) * steps_per_revolution
|
||||
pub fn angle_to_steps(&self, angle: Angle) -> f64 {
|
||||
let revolutions = angle.get::<revolution>();
|
||||
revolutions * self.steps_per_revolution as f64
|
||||
}
|
||||
|
||||
/// Convert steps/second to angular velocity
|
||||
pub fn steps_to_angular_velocity(&self, steps: f64) -> AngularVelocity {
|
||||
let revolutions_per_second = steps / self.steps_per_revolution as f64;
|
||||
///
|
||||
/// Formula: angular velocity (in rev/s) = steps_per_second / steps_per_revolution
|
||||
pub fn steps_to_angular_velocity(&self, steps_per_second: f64) -> AngularVelocity {
|
||||
let revolutions_per_second = steps_per_second / self.steps_per_revolution as f64;
|
||||
AngularVelocity::new::<revolution_per_second>(revolutions_per_second)
|
||||
}
|
||||
|
||||
/// Convert angular velocity to steps/second
|
||||
///
|
||||
/// Formula: steps_per_second = angular velocity (in rev/s) * steps_per_revolution
|
||||
pub fn angular_velocity_to_steps(&self, angular_velocity: AngularVelocity) -> f64 {
|
||||
let revolutions_per_second = angular_velocity.get::<revolution_per_second>();
|
||||
revolutions_per_second * self.steps_per_revolution as f64
|
||||
}
|
||||
|
||||
/// Convert steps/second^2 to angular acceleration
|
||||
pub fn steps_to_angular_acceleration(&self, steps: f64) -> AngularAcceleration {
|
||||
let revolutions_per_second_squared = steps / self.steps_per_revolution as f64;
|
||||
/// Convert steps/second² to angular acceleration
|
||||
///
|
||||
/// Formula:
|
||||
/// - revolutions_per_second² = steps_per_second² / steps_per_revolution
|
||||
/// - angular acceleration (in rad/s²) = revolutions_per_second² * 2π
|
||||
pub fn steps_to_angular_acceleration(
|
||||
&self,
|
||||
steps_per_second_squared: f64,
|
||||
) -> AngularAcceleration {
|
||||
let revolutions_per_second_squared =
|
||||
steps_per_second_squared / self.steps_per_revolution as f64;
|
||||
let radians_per_second_squared =
|
||||
revolutions_per_second_squared * 2.0 * std::f64::consts::PI;
|
||||
AngularAcceleration::new::<radian_per_second_squared>(radians_per_second_squared)
|
||||
}
|
||||
|
||||
/// Convert angular acceleration to steps/second^2
|
||||
/// Convert angular acceleration to steps/second²
|
||||
///
|
||||
/// Formula:
|
||||
/// - revolutions_per_second² = angular acceleration (in rad/s²) / 2π
|
||||
/// - steps_per_second² = revolutions_per_second² * steps_per_revolution
|
||||
pub fn angular_acceleration_to_steps(&self, angular_acceleration: AngularAcceleration) -> f64 {
|
||||
let radians_per_second_squared = angular_acceleration.get::<radian_per_second_squared>();
|
||||
let revolutions_per_second_squared =
|
||||
@@ -63,20 +90,20 @@ mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::f64::EPSILON;
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::degree, angular_acceleration::degree_per_second_squared,
|
||||
angular_velocity::degree_per_second,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
assert_eq!(converter.steps_per_revolution, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_angle() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test full revolution
|
||||
let angle = converter.steps_to_angle(200.0);
|
||||
@@ -94,14 +121,14 @@ mod tests {
|
||||
assert_relative_eq!(angle.get::<degree>(), 90.0, epsilon = EPSILON);
|
||||
|
||||
// Test with different steps_per_revolution
|
||||
let converter = StepConverter::new(400);
|
||||
let converter = AngularStepConverter::new(400);
|
||||
let angle = converter.steps_to_angle(100.0);
|
||||
assert_relative_eq!(angle.get::<revolution>(), 0.25, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angle_to_steps() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test full revolution
|
||||
let steps = converter.angle_to_steps(Angle::new::<revolution>(1.0));
|
||||
@@ -116,14 +143,14 @@ mod tests {
|
||||
assert_relative_eq!(steps, 50.0, epsilon = EPSILON);
|
||||
|
||||
// Test with different steps_per_revolution
|
||||
let converter = StepConverter::new(400);
|
||||
let converter = AngularStepConverter::new(400);
|
||||
let steps = converter.angle_to_steps(Angle::new::<degree>(90.0));
|
||||
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_angular_velocity() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test one revolution per second
|
||||
let angular_velocity = converter.steps_to_angular_velocity(200.0);
|
||||
@@ -147,7 +174,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Test with different steps_per_revolution
|
||||
let converter = StepConverter::new(400);
|
||||
let converter = AngularStepConverter::new(400);
|
||||
let angular_velocity = converter.steps_to_angular_velocity(200.0);
|
||||
assert_relative_eq!(
|
||||
angular_velocity.get::<revolution_per_second>(),
|
||||
@@ -158,7 +185,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_angular_velocity_to_steps() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test one revolution per second
|
||||
let steps =
|
||||
@@ -171,7 +198,7 @@ mod tests {
|
||||
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
|
||||
|
||||
// Test with different steps_per_revolution
|
||||
let converter = StepConverter::new(400);
|
||||
let converter = AngularStepConverter::new(400);
|
||||
let steps =
|
||||
converter.angular_velocity_to_steps(AngularVelocity::new::<revolution_per_second>(0.5));
|
||||
assert_relative_eq!(steps, 200.0, epsilon = EPSILON);
|
||||
@@ -179,7 +206,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_angular_acceleration() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test one revolution per second squared
|
||||
let angular_acceleration = converter.steps_to_angular_acceleration(200.0);
|
||||
@@ -199,7 +226,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Test with different steps_per_revolution
|
||||
let converter = StepConverter::new(400);
|
||||
let converter = AngularStepConverter::new(400);
|
||||
let angular_acceleration = converter.steps_to_angular_acceleration(400.0);
|
||||
assert_relative_eq!(
|
||||
angular_acceleration.get::<radian_per_second_squared>(),
|
||||
@@ -210,7 +237,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_angular_acceleration_to_steps() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test one revolution per second squared (2π rad/s²)
|
||||
let rps2 = 2.0 * std::f64::consts::PI;
|
||||
@@ -226,7 +253,7 @@ mod tests {
|
||||
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
|
||||
|
||||
// Test with different steps_per_revolution
|
||||
let converter = StepConverter::new(400);
|
||||
let converter = AngularStepConverter::new(400);
|
||||
let steps = converter.angular_acceleration_to_steps(AngularAcceleration::new::<
|
||||
radian_per_second_squared,
|
||||
>(rps2));
|
||||
@@ -235,7 +262,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_conversions() {
|
||||
let converter = StepConverter::new(200);
|
||||
let converter = AngularStepConverter::new(200);
|
||||
|
||||
// Test angle roundtrip
|
||||
let original_steps = 123.0;
|
||||
@@ -0,0 +1,279 @@
|
||||
//! # Circular Converter
|
||||
//!
|
||||
//! This module provides the `CircularConverter` which handles transformations between
|
||||
//! linear and angular measurements for circular motion systems.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! The `CircularConverter` is designed to work alongside other converters:
|
||||
//! - Use `AngularStepConverter` for angular-to-step conversions
|
||||
//! - Use `LinearStepConverter` for complete linear-to-step conversions (uses `CircularConverter` internally)
|
||||
//! - Use `CircularConverter` directly when you need linear-to-angular conversions without steps
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! use control_core::converters::circular_converter::CircularConverter;
|
||||
//! use units::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
|
||||
//!
|
||||
//! let converter = CircularConverter::from_radius(Length::new::<meter>(0.1));
|
||||
//! let velocity = Velocity::new::<meter_per_second>(1.0);
|
||||
//! let angular_velocity = converter.linear_to_angular_velocity(velocity);
|
||||
//! ```
|
||||
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
angle::radian,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
|
||||
length::meter,
|
||||
velocity::meter_per_second,
|
||||
};
|
||||
|
||||
/// A converter that handles transformations between linear and angular measurements
|
||||
/// for circular motion. This includes conversions for position, velocity, acceleration,
|
||||
/// and higher-order derivatives based on a circle's geometric properties.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CircularConverter {
|
||||
radius: Length,
|
||||
circumference: Length,
|
||||
diameter: Length,
|
||||
}
|
||||
|
||||
impl CircularConverter {
|
||||
/// Create a new converter from radius
|
||||
pub fn from_radius(radius: Length) -> Self {
|
||||
let diameter = radius * 2.0;
|
||||
let circumference =
|
||||
Length::new::<meter>(2.0 * std::f64::consts::PI * radius.get::<meter>());
|
||||
Self {
|
||||
radius,
|
||||
diameter,
|
||||
circumference,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter from diameter
|
||||
pub fn from_diameter(diameter: Length) -> Self {
|
||||
let radius = diameter / 2.0;
|
||||
Self::from_radius(radius)
|
||||
}
|
||||
|
||||
/// Create a new converter from circumference
|
||||
pub fn from_circumference(circumference: Length) -> Self {
|
||||
let radius =
|
||||
Length::new::<meter>(circumference.get::<meter>() / (2.0 * std::f64::consts::PI));
|
||||
Self::from_radius(radius)
|
||||
}
|
||||
|
||||
/// Get the radius
|
||||
pub fn radius(&self) -> Length {
|
||||
self.radius
|
||||
}
|
||||
|
||||
/// Get the diameter
|
||||
pub fn diameter(&self) -> Length {
|
||||
self.diameter
|
||||
}
|
||||
|
||||
/// Get the circumference
|
||||
pub fn circumference(&self) -> Length {
|
||||
self.circumference
|
||||
}
|
||||
|
||||
// Linear to Angular Position Conversions
|
||||
|
||||
/// Convert linear distance to angular position
|
||||
/// Formula: angle = distance / radius
|
||||
pub fn linear_to_angular_position(&self, distance: Length) -> Angle {
|
||||
let angle_rad = distance.get::<meter>() / self.radius.get::<meter>();
|
||||
Angle::new::<radian>(angle_rad)
|
||||
}
|
||||
|
||||
/// Convert angular position to linear distance
|
||||
/// Formula: distance = angle * radius
|
||||
pub fn angular_to_linear_position(&self, angle: Angle) -> Length {
|
||||
let distance = angle.get::<radian>() * self.radius.get::<meter>();
|
||||
Length::new::<meter>(distance)
|
||||
}
|
||||
|
||||
// Linear to Angular Velocity Conversions
|
||||
|
||||
/// Convert linear velocity to angular velocity
|
||||
/// Formula: angular_velocity = linear_velocity / radius
|
||||
pub fn linear_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
|
||||
let angular_velocity_rad_per_s =
|
||||
velocity.get::<meter_per_second>() / self.radius.get::<meter>();
|
||||
AngularVelocity::new::<radian_per_second>(angular_velocity_rad_per_s)
|
||||
}
|
||||
|
||||
/// Convert angular velocity to linear velocity
|
||||
/// Formula: linear_velocity = angular_velocity * radius
|
||||
pub fn angular_to_linear_velocity(&self, angular_velocity: AngularVelocity) -> Velocity {
|
||||
let velocity = angular_velocity.get::<radian_per_second>() * self.radius.get::<meter>();
|
||||
Velocity::new::<meter_per_second>(velocity)
|
||||
}
|
||||
|
||||
// Linear to Angular Acceleration Conversions
|
||||
|
||||
/// Convert linear acceleration to angular acceleration
|
||||
/// Formula: angular_acceleration = linear_acceleration / radius
|
||||
pub fn linear_to_angular_acceleration(
|
||||
&self,
|
||||
acceleration: Acceleration,
|
||||
) -> AngularAcceleration {
|
||||
let angular_acceleration_rad_per_s2 =
|
||||
acceleration.get::<meter_per_second_squared>() / self.radius.get::<meter>();
|
||||
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2)
|
||||
}
|
||||
|
||||
/// Convert angular acceleration to linear acceleration
|
||||
/// Formula: linear_acceleration = angular_acceleration * radius
|
||||
pub fn angular_to_linear_acceleration(
|
||||
&self,
|
||||
angular_acceleration: AngularAcceleration,
|
||||
) -> Acceleration {
|
||||
let acceleration =
|
||||
angular_acceleration.get::<radian_per_second_squared>() * self.radius.get::<meter>();
|
||||
Acceleration::new::<meter_per_second_squared>(acceleration)
|
||||
}
|
||||
|
||||
// Jerk Conversions (using f64 for now since UOM may not have jerk units)
|
||||
|
||||
/// Convert linear jerk to angular jerk
|
||||
/// Formula: angular_jerk = linear_jerk / radius
|
||||
/// Units: rad/s³ = (m/s³) / m
|
||||
pub fn linear_to_angular_jerk(&self, linear_jerk: f64) -> f64 {
|
||||
linear_jerk / self.radius.get::<meter>()
|
||||
}
|
||||
|
||||
/// Convert angular jerk to linear jerk
|
||||
/// Formula: linear_jerk = angular_jerk * radius
|
||||
/// Units: m/s³ = (rad/s³) * m
|
||||
pub fn angular_to_linear_jerk(&self, angular_jerk: f64) -> f64 {
|
||||
angular_jerk * self.radius.get::<meter>()
|
||||
}
|
||||
|
||||
// Arc Length Calculations
|
||||
|
||||
/// Calculate arc length from angle
|
||||
/// Formula: arc_length = angle * radius
|
||||
pub fn angle_to_arc_length(&self, angle: Angle) -> Length {
|
||||
self.angular_to_linear_position(angle)
|
||||
}
|
||||
|
||||
/// Calculate angle from arc length
|
||||
/// Formula: angle = arc_length / radius
|
||||
pub fn arc_length_to_angle(&self, arc_length: Length) -> Angle {
|
||||
self.linear_to_angular_position(arc_length)
|
||||
}
|
||||
|
||||
// Circumference-based calculations
|
||||
|
||||
/// Convert linear distance to number of revolutions
|
||||
/// Formula: revolutions = distance / circumference
|
||||
pub fn linear_distance_to_revolutions(&self, distance: Length) -> f64 {
|
||||
distance.get::<meter>() / self.circumference.get::<meter>()
|
||||
}
|
||||
|
||||
/// Convert number of revolutions to linear distance
|
||||
/// Formula: distance = revolutions * circumference
|
||||
pub fn revolutions_to_linear_distance(&self, revolutions: f64) -> Length {
|
||||
Length::new::<meter>(revolutions * self.circumference.get::<meter>())
|
||||
}
|
||||
|
||||
/// Convert linear velocity to revolutions per second
|
||||
/// Formula: rps = velocity / circumference
|
||||
pub fn linear_velocity_to_rps(&self, velocity: Velocity) -> f64 {
|
||||
velocity.get::<meter_per_second>() / self.circumference.get::<meter>()
|
||||
}
|
||||
|
||||
/// Convert revolutions per second to linear velocity
|
||||
/// Formula: velocity = rps * circumference
|
||||
pub fn rps_to_linear_velocity(&self, rps: f64) -> Velocity {
|
||||
Velocity::new::<meter_per_second>(rps * self.circumference.get::<meter>())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use units::length::centimeter;
|
||||
|
||||
#[test]
|
||||
fn test_constructors() {
|
||||
let radius = Length::new::<centimeter>(5.0);
|
||||
let diameter = Length::new::<centimeter>(10.0);
|
||||
let circumference = Length::new::<centimeter>(2.0 * std::f64::consts::PI * 5.0);
|
||||
|
||||
let converter1 = CircularConverter::from_radius(radius);
|
||||
let converter2 = CircularConverter::from_diameter(diameter);
|
||||
let converter3 = CircularConverter::from_circumference(circumference);
|
||||
|
||||
// All should have approximately the same values
|
||||
assert!((converter1.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
|
||||
assert!((converter2.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
|
||||
assert!((converter3.radius().get::<centimeter>() - 5.0).abs() < 1e-10);
|
||||
|
||||
assert!((converter1.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
|
||||
assert!((converter2.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
|
||||
assert!((converter3.diameter().get::<centimeter>() - 10.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_conversions() {
|
||||
let converter = CircularConverter::from_radius(Length::new::<meter>(1.0));
|
||||
|
||||
// Test linear to angular
|
||||
let distance = Length::new::<meter>(std::f64::consts::PI);
|
||||
let angle = converter.linear_to_angular_position(distance);
|
||||
assert!((angle.get::<radian>() - std::f64::consts::PI).abs() < 1e-10);
|
||||
|
||||
// Test angular to linear
|
||||
let angle = Angle::new::<radian>(std::f64::consts::PI / 2.0);
|
||||
let distance = converter.angular_to_linear_position(angle);
|
||||
assert!((distance.get::<meter>() - std::f64::consts::PI / 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_velocity_conversions() {
|
||||
let converter = CircularConverter::from_radius(Length::new::<meter>(0.5));
|
||||
|
||||
// Test linear to angular
|
||||
let velocity = Velocity::new::<meter_per_second>(1.0);
|
||||
let angular_velocity = converter.linear_to_angular_velocity(velocity);
|
||||
assert!((angular_velocity.get::<radian_per_second>() - 2.0).abs() < 1e-10);
|
||||
|
||||
// Test angular to linear
|
||||
let angular_velocity = AngularVelocity::new::<radian_per_second>(4.0);
|
||||
let velocity = converter.angular_to_linear_velocity(angular_velocity);
|
||||
assert!((velocity.get::<meter_per_second>() - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_revolution_conversions() {
|
||||
let converter = CircularConverter::from_radius(Length::new::<meter>(1.0));
|
||||
|
||||
// One full revolution should equal circumference
|
||||
let revolutions = 1.0;
|
||||
let distance = converter.revolutions_to_linear_distance(revolutions);
|
||||
assert!((distance.get::<meter>() - converter.circumference().get::<meter>()).abs() < 1e-10);
|
||||
|
||||
// Converting back should give 1 revolution
|
||||
let calculated_revolutions = converter.linear_distance_to_revolutions(distance);
|
||||
assert!((calculated_revolutions - 1.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jerk_conversions() {
|
||||
let converter = CircularConverter::from_radius(Length::new::<meter>(2.0));
|
||||
|
||||
let linear_jerk = 4.0; // m/s³
|
||||
let angular_jerk = converter.linear_to_angular_jerk(linear_jerk);
|
||||
assert!((angular_jerk - 2.0).abs() < 1e-10); // 4.0 / 2.0 = 2.0 rad/s³
|
||||
|
||||
let calculated_linear_jerk = converter.angular_to_linear_jerk(angular_jerk);
|
||||
assert!((calculated_linear_jerk - linear_jerk).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
use units::{
|
||||
angle::revolution,
|
||||
angular_velocity::revolution_per_second,
|
||||
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
|
||||
};
|
||||
|
||||
use super::angular_step_converter::AngularStepConverter;
|
||||
use super::circular_converter::CircularConverter;
|
||||
|
||||
/// Converts between linear measurements and motor steps
|
||||
///
|
||||
/// This converter handles the transformation between linear motion (distance, velocity,
|
||||
/// acceleration) and motor steps, using the relationship between linear and rotary motion
|
||||
/// through a wheel or pulley of a specific radius.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LinearStepConverter {
|
||||
angular_step_converter: AngularStepConverter,
|
||||
circular_converter: CircularConverter,
|
||||
}
|
||||
|
||||
// Constructor and basic getters
|
||||
impl LinearStepConverter {
|
||||
/// Create a new converter from radius and steps per revolution
|
||||
pub fn from_radius(steps_per_revolution: i16, radius: Length) -> Self {
|
||||
Self {
|
||||
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
|
||||
circular_converter: CircularConverter::from_radius(radius),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter from diameter and steps per revolution
|
||||
pub fn from_diameter(steps_per_revolution: i16, diameter: Length) -> Self {
|
||||
Self {
|
||||
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
|
||||
circular_converter: CircularConverter::from_diameter(diameter),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new converter from circumference and steps per revolution
|
||||
pub fn from_circumference(steps_per_revolution: i16, circumference: Length) -> Self {
|
||||
Self {
|
||||
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
|
||||
circular_converter: CircularConverter::from_circumference(circumference),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the radius used by the converter
|
||||
pub fn radius(&self) -> Length {
|
||||
self.circular_converter.radius()
|
||||
}
|
||||
|
||||
/// Get the diameter of the system
|
||||
pub fn diameter(&self) -> Length {
|
||||
self.circular_converter.diameter()
|
||||
}
|
||||
|
||||
/// Get the circumference of the system
|
||||
pub fn circumference(&self) -> Length {
|
||||
self.circular_converter.circumference()
|
||||
}
|
||||
|
||||
/// Get the steps per revolution
|
||||
pub const fn steps_per_revolution(&self) -> i16 {
|
||||
self.angular_step_converter.steps_per_revolution
|
||||
}
|
||||
}
|
||||
|
||||
// Linear to/from steps conversions
|
||||
impl LinearStepConverter {
|
||||
/// Convert linear distance to steps
|
||||
///
|
||||
/// Formula: steps = (distance / circumference) * steps_per_revolution
|
||||
pub fn distance_to_steps(&self, distance: Length) -> f64 {
|
||||
// Convert distance to revolutions using CircularConverter
|
||||
let revolutions = self
|
||||
.circular_converter
|
||||
.linear_distance_to_revolutions(distance);
|
||||
let angle = Angle::new::<revolution>(revolutions);
|
||||
|
||||
// Convert angle to steps
|
||||
self.angular_step_converter.angle_to_steps(angle)
|
||||
}
|
||||
|
||||
/// Convert steps to linear distance
|
||||
///
|
||||
/// Formula: distance = (steps / steps_per_revolution) * circumference
|
||||
pub fn steps_to_distance(&self, steps: f64) -> Length {
|
||||
// Convert steps to angle
|
||||
let angle = self.angular_step_converter.steps_to_angle(steps);
|
||||
|
||||
// Convert angle to distance using CircularConverter
|
||||
let revolutions = angle.get::<revolution>();
|
||||
self.circular_converter
|
||||
.revolutions_to_linear_distance(revolutions)
|
||||
}
|
||||
|
||||
/// Convert linear velocity to steps/second
|
||||
///
|
||||
/// Formula: steps/second = (velocity / circumference) * steps_per_revolution
|
||||
pub fn velocity_to_steps(&self, velocity: Velocity) -> f64 {
|
||||
// Convert linear velocity to revolutions per second using CircularConverter
|
||||
let rps = self.circular_converter.linear_velocity_to_rps(velocity);
|
||||
let angular_velocity = AngularVelocity::new::<revolution_per_second>(rps);
|
||||
|
||||
// Convert angular velocity to steps/second
|
||||
self.angular_step_converter
|
||||
.angular_velocity_to_steps(angular_velocity)
|
||||
}
|
||||
|
||||
/// Convert steps/second to linear velocity
|
||||
///
|
||||
/// Formula: velocity = (steps_per_second / steps_per_revolution) * circumference
|
||||
pub fn steps_to_velocity(&self, steps_per_second: f64) -> Velocity {
|
||||
// Convert steps/second to angular velocity
|
||||
let angular_velocity = self
|
||||
.angular_step_converter
|
||||
.steps_to_angular_velocity(steps_per_second);
|
||||
|
||||
// Convert angular velocity to linear velocity using CircularConverter
|
||||
let rps = angular_velocity.get::<revolution_per_second>();
|
||||
self.circular_converter.rps_to_linear_velocity(rps)
|
||||
}
|
||||
|
||||
/// Convert linear acceleration to steps/second²
|
||||
///
|
||||
/// Formula: steps/second² = (acceleration / radius) * (steps_per_revolution / (2π))
|
||||
pub fn acceleration_to_steps(&self, acceleration: Acceleration) -> f64 {
|
||||
// Convert linear acceleration to angular acceleration using CircularConverter
|
||||
let angular_acceleration = self
|
||||
.circular_converter
|
||||
.linear_to_angular_acceleration(acceleration);
|
||||
|
||||
// Convert angular acceleration to steps/second²
|
||||
self.angular_step_converter
|
||||
.angular_acceleration_to_steps(angular_acceleration)
|
||||
}
|
||||
|
||||
/// Convert steps/second² to linear acceleration
|
||||
///
|
||||
/// Formula: acceleration = (steps_per_second² / steps_per_revolution) * (2π) * radius
|
||||
pub fn steps_to_acceleration(&self, steps_per_second_squared: f64) -> Acceleration {
|
||||
// Convert steps/second² to angular acceleration
|
||||
let angular_acceleration = self
|
||||
.angular_step_converter
|
||||
.steps_to_angular_acceleration(steps_per_second_squared);
|
||||
|
||||
// Convert angular acceleration to linear acceleration using CircularConverter
|
||||
self.circular_converter
|
||||
.angular_to_linear_acceleration(angular_acceleration)
|
||||
}
|
||||
}
|
||||
|
||||
// Linear to/from angular conversions
|
||||
impl LinearStepConverter {
|
||||
/// Convert linear distance to angle
|
||||
///
|
||||
/// Formula: angle (in revolutions) = distance / circumference
|
||||
pub fn distance_to_angle(&self, distance: Length) -> Angle {
|
||||
let revolutions = self
|
||||
.circular_converter
|
||||
.linear_distance_to_revolutions(distance);
|
||||
Angle::new::<revolution>(revolutions)
|
||||
}
|
||||
|
||||
/// Convert angle to linear distance
|
||||
///
|
||||
/// Formula: distance = angle (in revolutions) * circumference
|
||||
pub fn angle_to_distance(&self, angle: Angle) -> Length {
|
||||
let revolutions = angle.get::<revolution>();
|
||||
self.circular_converter
|
||||
.revolutions_to_linear_distance(revolutions)
|
||||
}
|
||||
|
||||
/// Convert linear velocity to angular velocity
|
||||
///
|
||||
/// Formula: angular velocity (in rev/s) = velocity / circumference
|
||||
pub fn velocity_to_angular_velocity(&self, velocity: Velocity) -> AngularVelocity {
|
||||
let rps = self.circular_converter.linear_velocity_to_rps(velocity);
|
||||
AngularVelocity::new::<revolution_per_second>(rps)
|
||||
}
|
||||
|
||||
/// Convert angular velocity to linear velocity
|
||||
///
|
||||
/// Formula: velocity = angular velocity (in rev/s) * circumference
|
||||
pub fn angular_velocity_to_velocity(&self, angular_velocity: AngularVelocity) -> Velocity {
|
||||
let rps = angular_velocity.get::<revolution_per_second>();
|
||||
self.circular_converter.rps_to_linear_velocity(rps)
|
||||
}
|
||||
|
||||
/// Convert linear acceleration to angular acceleration
|
||||
///
|
||||
/// Formula: angular acceleration (in rad/s²) = acceleration / radius
|
||||
pub fn acceleration_to_angular_acceleration(
|
||||
&self,
|
||||
acceleration: Acceleration,
|
||||
) -> AngularAcceleration {
|
||||
self.circular_converter
|
||||
.linear_to_angular_acceleration(acceleration)
|
||||
}
|
||||
|
||||
/// Convert angular acceleration to linear acceleration
|
||||
///
|
||||
/// Formula: acceleration = angular acceleration (in rad/s²) * radius
|
||||
pub fn angular_acceleration_to_acceleration(
|
||||
&self,
|
||||
angular_acceleration: AngularAcceleration,
|
||||
) -> Acceleration {
|
||||
self.circular_converter
|
||||
.angular_to_linear_acceleration(angular_acceleration)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward angular to/from steps conversions from StepConverter
|
||||
impl LinearStepConverter {
|
||||
/// Convert steps to angle
|
||||
pub fn steps_to_angle(&self, steps: f64) -> Angle {
|
||||
self.angular_step_converter.steps_to_angle(steps)
|
||||
}
|
||||
|
||||
/// Convert angle to steps
|
||||
pub fn angle_to_steps(&self, angle: Angle) -> f64 {
|
||||
self.angular_step_converter.angle_to_steps(angle)
|
||||
}
|
||||
|
||||
/// Convert steps/second to angular velocity
|
||||
pub fn steps_to_angular_velocity(&self, steps: f64) -> AngularVelocity {
|
||||
self.angular_step_converter.steps_to_angular_velocity(steps)
|
||||
}
|
||||
|
||||
/// Convert angular velocity to steps/second
|
||||
pub fn angular_velocity_to_steps(&self, angular_velocity: AngularVelocity) -> f64 {
|
||||
self.angular_step_converter
|
||||
.angular_velocity_to_steps(angular_velocity)
|
||||
}
|
||||
|
||||
/// Convert steps/second² to angular acceleration
|
||||
pub fn steps_to_angular_acceleration(&self, steps: f64) -> AngularAcceleration {
|
||||
self.angular_step_converter
|
||||
.steps_to_angular_acceleration(steps)
|
||||
}
|
||||
|
||||
/// Convert angular acceleration to steps/second²
|
||||
pub fn angular_acceleration_to_steps(&self, angular_acceleration: AngularAcceleration) -> f64 {
|
||||
self.angular_step_converter
|
||||
.angular_acceleration_to_steps(angular_acceleration)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::f64::EPSILON;
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
|
||||
length::meter, velocity::meter_per_second,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
assert_eq!(converter.steps_per_revolution(), 200);
|
||||
assert_eq!(converter.radius().get::<meter>(), 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diameter_and_circumference() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// Test diameter
|
||||
let diameter = converter.diameter();
|
||||
assert_relative_eq!(diameter.get::<meter>(), 0.2, epsilon = EPSILON);
|
||||
|
||||
// Test circumference
|
||||
let circumference = converter.circumference();
|
||||
let expected_circumference = 2.0 * std::f64::consts::PI * 0.1;
|
||||
assert_relative_eq!(
|
||||
circumference.get::<meter>(),
|
||||
expected_circumference,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Tests for linear <-> steps conversions
|
||||
#[test]
|
||||
fn test_distance_to_steps() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// One full revolution
|
||||
let steps = converter.distance_to_steps(Length::new::<meter>(circumference));
|
||||
assert_relative_eq!(steps, 200.0, epsilon = EPSILON);
|
||||
|
||||
// Half revolution
|
||||
let steps = converter.distance_to_steps(Length::new::<meter>(circumference / 2.0));
|
||||
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_distance() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// One full revolution
|
||||
let distance = converter.steps_to_distance(200.0);
|
||||
assert_relative_eq!(distance.get::<meter>(), circumference, epsilon = EPSILON);
|
||||
|
||||
// Half revolution
|
||||
let distance = converter.steps_to_distance(100.0);
|
||||
assert_relative_eq!(
|
||||
distance.get::<meter>(),
|
||||
circumference / 2.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_velocity_to_steps() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// Linear velocity equivalent to one revolution per second
|
||||
let velocity = Velocity::new::<meter_per_second>(circumference);
|
||||
let steps_per_second = converter.velocity_to_steps(velocity);
|
||||
assert_relative_eq!(steps_per_second, 200.0, epsilon = EPSILON);
|
||||
|
||||
// Half revolution per second
|
||||
let velocity = Velocity::new::<meter_per_second>(circumference / 2.0);
|
||||
let steps_per_second = converter.velocity_to_steps(velocity);
|
||||
assert_relative_eq!(steps_per_second, 100.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_velocity() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// Steps for one revolution per second
|
||||
let velocity = converter.steps_to_velocity(200.0);
|
||||
assert_relative_eq!(
|
||||
velocity.get::<meter_per_second>(),
|
||||
circumference,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Half revolution per second
|
||||
let velocity = converter.steps_to_velocity(100.0);
|
||||
assert_relative_eq!(
|
||||
velocity.get::<meter_per_second>(),
|
||||
circumference / 2.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acceleration_to_steps() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// Create a linear acceleration of 1 m/s²
|
||||
// This will result in angular acceleration of a/r = 1/0.1 = 10 rad/s²
|
||||
let acceleration = Acceleration::new::<meter_per_second_squared>(1.0);
|
||||
let steps_per_second_squared = converter.acceleration_to_steps(acceleration);
|
||||
|
||||
// Expected steps for 10 rad/s² angular acceleration:
|
||||
// 10 rad/s² * (1 rev/2π rad) * 200 steps/rev = 318.31 steps/s²
|
||||
let expected_steps = 200.0 * 10.0 / (2.0 * std::f64::consts::PI);
|
||||
assert_relative_eq!(
|
||||
steps_per_second_squared,
|
||||
expected_steps,
|
||||
epsilon = EPSILON * 10.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_steps_to_acceleration() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// Steps for one revolution per second squared
|
||||
let acceleration = converter.steps_to_acceleration(200.0);
|
||||
let expected = 2.0 * std::f64::consts::PI * 0.1; // rad/s² * radius
|
||||
assert_relative_eq!(
|
||||
acceleration.get::<meter_per_second_squared>(),
|
||||
expected,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Tests for linear <-> angular conversions
|
||||
#[test]
|
||||
fn test_distance_to_angle() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// Full circumference = 1 revolution
|
||||
let angle = converter.distance_to_angle(Length::new::<meter>(circumference));
|
||||
assert_relative_eq!(angle.get::<revolution>(), 1.0, epsilon = EPSILON);
|
||||
|
||||
// Half circumference = 0.5 revolution
|
||||
let angle = converter.distance_to_angle(Length::new::<meter>(circumference / 2.0));
|
||||
assert_relative_eq!(angle.get::<revolution>(), 0.5, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angle_to_distance() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// 1 revolution = full circumference
|
||||
let distance = converter.angle_to_distance(Angle::new::<revolution>(1.0));
|
||||
assert_relative_eq!(distance.get::<meter>(), circumference, epsilon = EPSILON);
|
||||
|
||||
// 0.5 revolution = half circumference
|
||||
let distance = converter.angle_to_distance(Angle::new::<revolution>(0.5));
|
||||
assert_relative_eq!(
|
||||
distance.get::<meter>(),
|
||||
circumference / 2.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_velocity_to_angular_velocity() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// Linear velocity of full circumference per second = 1 revolution per second
|
||||
let angular_velocity = converter
|
||||
.velocity_to_angular_velocity(Velocity::new::<meter_per_second>(circumference));
|
||||
assert_relative_eq!(
|
||||
angular_velocity.get::<revolution_per_second>(),
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Half circumference per second = 0.5 revolution per second
|
||||
let angular_velocity =
|
||||
converter.velocity_to_angular_velocity(Velocity::new::<meter_per_second>(
|
||||
circumference / 2.0,
|
||||
));
|
||||
assert_relative_eq!(
|
||||
angular_velocity.get::<revolution_per_second>(),
|
||||
0.5,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angular_velocity_to_velocity() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
let circumference = 2.0 * std::f64::consts::PI * 0.1; // 0.628m
|
||||
|
||||
// 1 revolution per second = linear velocity of full circumference per second
|
||||
let velocity = converter
|
||||
.angular_velocity_to_velocity(AngularVelocity::new::<revolution_per_second>(1.0));
|
||||
assert_relative_eq!(
|
||||
velocity.get::<meter_per_second>(),
|
||||
circumference,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// 0.5 revolution per second = half circumference per second
|
||||
let velocity = converter
|
||||
.angular_velocity_to_velocity(AngularVelocity::new::<revolution_per_second>(0.5));
|
||||
assert_relative_eq!(
|
||||
velocity.get::<meter_per_second>(),
|
||||
circumference / 2.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acceleration_to_angular_acceleration() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// Linear acceleration of 1 m/s² = angular acceleration of 10 rad/s²
|
||||
let angular_accel = converter.acceleration_to_angular_acceleration(Acceleration::new::<
|
||||
meter_per_second_squared,
|
||||
>(1.0));
|
||||
assert_relative_eq!(
|
||||
angular_accel.get::<radian_per_second_squared>(),
|
||||
10.0, // a/r = 1/0.1 = 10
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angular_acceleration_to_acceleration() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// Angular acceleration of 10 rad/s² = linear acceleration of 1 m/s²
|
||||
let acceleration =
|
||||
converter.angular_acceleration_to_acceleration(AngularAcceleration::new::<
|
||||
radian_per_second_squared,
|
||||
>(10.0));
|
||||
assert_relative_eq!(
|
||||
acceleration.get::<meter_per_second_squared>(),
|
||||
1.0, // α*r = 10*0.1 = 1
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Tests for forwarded angular <-> steps methods
|
||||
#[test]
|
||||
fn test_forwarded_steps_to_angle() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// 200 steps = 1 revolution
|
||||
let angle = converter.steps_to_angle(200.0);
|
||||
assert_relative_eq!(angle.get::<revolution>(), 1.0, epsilon = EPSILON);
|
||||
|
||||
// 100 steps = 0.5 revolution
|
||||
let angle = converter.steps_to_angle(100.0);
|
||||
assert_relative_eq!(angle.get::<revolution>(), 0.5, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forwarded_angle_to_steps() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// 1 revolution = 200 steps
|
||||
let steps = converter.angle_to_steps(Angle::new::<revolution>(1.0));
|
||||
assert_relative_eq!(steps, 200.0, epsilon = EPSILON);
|
||||
|
||||
// 0.5 revolution = 100 steps
|
||||
let steps = converter.angle_to_steps(Angle::new::<revolution>(0.5));
|
||||
assert_relative_eq!(steps, 100.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_conversions() {
|
||||
let converter = LinearStepConverter::from_radius(200, Length::new::<meter>(0.1));
|
||||
|
||||
// Test linear distance to steps roundtrip
|
||||
let original_distance = Length::new::<meter>(0.456);
|
||||
let steps = converter.distance_to_steps(original_distance);
|
||||
let roundtrip_distance = converter.steps_to_distance(steps);
|
||||
assert_relative_eq!(
|
||||
original_distance.get::<meter>(),
|
||||
roundtrip_distance.get::<meter>(),
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test linear velocity to steps roundtrip
|
||||
let original_velocity = Velocity::new::<meter_per_second>(0.789);
|
||||
let steps_per_second = converter.velocity_to_steps(original_velocity);
|
||||
let roundtrip_velocity = converter.steps_to_velocity(steps_per_second);
|
||||
assert_relative_eq!(
|
||||
original_velocity.get::<meter_per_second>(),
|
||||
roundtrip_velocity.get::<meter_per_second>(),
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test linear distance to angle roundtrip
|
||||
let original_distance = Length::new::<meter>(0.123);
|
||||
let angle = converter.distance_to_angle(original_distance);
|
||||
let roundtrip_distance = converter.angle_to_distance(angle);
|
||||
assert_relative_eq!(
|
||||
original_distance.get::<meter>(),
|
||||
roundtrip_distance.get::<meter>(),
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test angle to steps roundtrip
|
||||
let original_angle = Angle::new::<revolution>(0.75);
|
||||
let steps = converter.angle_to_steps(original_angle);
|
||||
let roundtrip_angle = converter.steps_to_angle(steps);
|
||||
assert_relative_eq!(
|
||||
original_angle.get::<revolution>(),
|
||||
roundtrip_angle.get::<revolution>(),
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
pub mod angle_converter;
|
||||
pub mod angular_step_converter;
|
||||
pub mod circular_converter;
|
||||
pub mod linear_step_converter;
|
||||
pub mod motor_converter;
|
||||
pub mod step_converter;
|
||||
@@ -1,9 +1 @@
|
||||
pub struct MotorConverter {}
|
||||
impl MotorConverter {
|
||||
pub fn rpm_to_hz(rpm: f32) -> f32 {
|
||||
rpm / 60.0
|
||||
}
|
||||
pub fn hz_to_rpm(hz: f32) -> f32 {
|
||||
hz * 60.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::anyhow;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Downcast<T> {
|
||||
fn downcast(&self) -> Result<T, anyhow::Error>;
|
||||
}
|
||||
|
||||
pub trait DowncastRef<T> {
|
||||
fn downcast_ref(&self) -> Result<&T, anyhow::Error>;
|
||||
}
|
||||
|
||||
impl<T: 'static, U: Any + 'static> Downcast<Arc<Mutex<T>>> for Arc<Mutex<U>> {
|
||||
fn downcast(&self) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let lock = self.lock_blocking();
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
let any: &dyn Any = &*lock;
|
||||
|
||||
if any.is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned: Arc<Mutex<dyn Any>> = self.clone();
|
||||
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
let arc = Arc::from_raw(Arc::into_raw(cloned) as *const Mutex<T>);
|
||||
|
||||
Ok(arc)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("[{}::downcast] Downcast failed", module_path!()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
||||
std::{ethercat_now, tx_rx_task},
|
||||
};
|
||||
use interfaces::Interface;
|
||||
use std::{process::Command, sync::Arc, time::Duration};
|
||||
|
||||
// Constants for EtherCAT configuration
|
||||
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
|
||||
@@ -12,94 +11,115 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
/// Finds an ethernet interface that is suitable for EtherCAT communication.
|
||||
///
|
||||
/// ```ignore
|
||||
/// match discover_ethercat_interface().await {
|
||||
/// Ok(interface) => println!("Found working interface: {}", interface),
|
||||
/// Err(_) => println!("No working interface found"),
|
||||
/// }
|
||||
/// ```
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
return Ok(reports_as_ethernet && !actually_wifi);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
log::info!("Discovering EtherCAT interface...");
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Optional: log the panic in a controlled way
|
||||
if let Some(location) = panic_info.location() {
|
||||
log::debug!(
|
||||
tracing::debug!(
|
||||
"Suppressed panic in interface test: {} (at {}:{})",
|
||||
panic_info,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
log::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
// Don't call the default hook, which would print the backtrace
|
||||
}));
|
||||
|
||||
// Get eligible interfaces
|
||||
let mut interfaces = Interface::get_all()
|
||||
.or_else(|e| Err(anyhow::anyhow!("Failed to get network interfaces: {}", e)))?
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
|
||||
.into_iter()
|
||||
.filter(|iface| {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
|
||||
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort interfaces by name
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
// Try all interfaces concurrently
|
||||
let tasks = interfaces
|
||||
.iter()
|
||||
.map(|iface| {
|
||||
let name = iface.name.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::panic::catch_unwind(|| {
|
||||
smol::block_on(async {
|
||||
match test_interface(&name) {
|
||||
Ok(_) => {
|
||||
log::debug!("Found working EtherCAT interface: {}", name);
|
||||
Some(name.clone())
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Interface {} failed: {}", name, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for i in 0..interfaces.len() {
|
||||
match test_interface(&interfaces[i].name) {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Return first successful interface
|
||||
let result =
|
||||
tasks
|
||||
.into_iter()
|
||||
.find_map(|task| match task.join().expect("Should join thread") {
|
||||
Some(name) => Some(name),
|
||||
None => None,
|
||||
});
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match result {
|
||||
Some(name) => Ok(name),
|
||||
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
match interface {
|
||||
Some(interface) => return Ok(interface.to_string()),
|
||||
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
log::debug!("Testing interface: {}", interface);
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
@@ -133,7 +153,12 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
},
|
||||
MainDeviceConfig::default(),
|
||||
MainDeviceConfig {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
let result = maindevice
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
pub mod eeprom_identification;
|
||||
pub mod interface_discovery;
|
||||
@@ -0,0 +1,42 @@
|
||||
pub struct ListChange<'t, T>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
pub added: Vec<&'t T>,
|
||||
pub removed: Vec<&'t T>,
|
||||
pub same: Vec<&'t T>,
|
||||
}
|
||||
|
||||
pub fn compare_lists<'oldlist, 'newlist, 't, T: PartialEq>(
|
||||
old_list: &'oldlist Vec<T>,
|
||||
new_list: &'newlist Vec<T>,
|
||||
) -> ListChange<'t, T>
|
||||
where
|
||||
T: PartialEq,
|
||||
'oldlist: 't,
|
||||
'newlist: 't,
|
||||
{
|
||||
let mut added = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
let mut same = Vec::new();
|
||||
|
||||
for item in new_list.iter() {
|
||||
if !old_list.iter().any(|x| x == item) {
|
||||
added.push(item);
|
||||
} else {
|
||||
same.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
for item in old_list.iter() {
|
||||
if !new_list.iter().any(|x| x == item) {
|
||||
removed.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
ListChange {
|
||||
added,
|
||||
removed,
|
||||
same,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
use serde::Serialize;
|
||||
use serde::ser::{
|
||||
SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
|
||||
SerializeTupleStruct, SerializeTupleVariant, Serializer,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::hash::{DefaultHasher, Hasher};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HashSerializerError {}
|
||||
|
||||
impl std::fmt::Display for HashSerializerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{:?}", self))
|
||||
// f.debug_struct("HashSerialiyerError")
|
||||
// .finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HashSerializerError {}
|
||||
|
||||
impl serde::ser::Error for HashSerializerError {
|
||||
fn custom<T>(_msg: T) -> Self
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HashSerializer<'a, H: Hasher>(&'a mut H);
|
||||
impl<H: Hasher> SerializeSeq for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> SerializeTuple for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> SerializeTupleStruct for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> SerializeTupleVariant for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> SerializeMap for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_key<T>(&mut self, key: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
key.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn serialize_value<T>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> SerializeStruct for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
// Optional: hash the field name to avoid collisions
|
||||
self.0.write(key.as_bytes());
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> SerializeStructVariant for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
|
||||
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
self.0.write(key.as_bytes());
|
||||
value.serialize(HashSerializer(self.0))
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hasher> Serializer for HashSerializer<'_, H> {
|
||||
type Ok = ();
|
||||
type Error = HashSerializerError;
|
||||
type SerializeSeq = Self;
|
||||
type SerializeTuple = Self;
|
||||
type SerializeTupleStruct = Self;
|
||||
type SerializeTupleVariant = Self;
|
||||
type SerializeMap = Self;
|
||||
type SerializeStruct = Self;
|
||||
type SerializeStructVariant = Self;
|
||||
|
||||
fn serialize_bool(self, v: bool) -> Result<(), Self::Error> {
|
||||
self.0.write_u8(v as u8);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_i8(self, v: i8) -> Result<(), Self::Error> {
|
||||
self.0.write_i8(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_i16(self, v: i16) -> Result<(), Self::Error> {
|
||||
self.0.write_i16(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_i32(self, v: i32) -> Result<(), Self::Error> {
|
||||
self.0.write_i32(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_i64(self, v: i64) -> Result<(), Self::Error> {
|
||||
self.0.write_i64(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_u8(self, v: u8) -> Result<(), Self::Error> {
|
||||
self.0.write_u8(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_u16(self, v: u16) -> Result<(), Self::Error> {
|
||||
self.0.write_u16(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_u32(self, v: u32) -> Result<(), Self::Error> {
|
||||
self.0.write_u32(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_u64(self, v: u64) -> Result<(), Self::Error> {
|
||||
self.0.write_u64(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_f32(self, v: f32) -> Result<(), Self::Error> {
|
||||
self.0.write_u32(v.to_bits());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_f64(self, v: f64) -> Result<(), Self::Error> {
|
||||
self.0.write_u64(v.to_bits());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_char(self, v: char) -> Result<(), Self::Error> {
|
||||
let mut buf = [0; 4];
|
||||
self.0.write(v.encode_utf8(&mut buf).as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_str(self, v: &str) -> Result<(), Self::Error> {
|
||||
self.0.write(v.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_bytes(self, v: &[u8]) -> Result<(), Self::Error> {
|
||||
self.0.write(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_none(self) -> Result<(), Self::Error> {
|
||||
self.0.write_u8(0); // type tag for None
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<(), Self::Error> {
|
||||
self.0.write_u8(1); // type tag for Some
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<(), Self::Error> {
|
||||
self.0.write_u8(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> {
|
||||
self.serialize_unit()
|
||||
}
|
||||
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.write_u32(variant_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_newtype_struct<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
value: &T,
|
||||
) -> Result<(), Self::Error> {
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
value: &T,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.write_u32(variant_index);
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
// For sequences, tuples, maps, structs, just return self for now
|
||||
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeTupleStruct, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeTupleVariant, Self::Error> {
|
||||
self.0.write_u32(variant_index);
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStruct, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStructVariant, Self::Error> {
|
||||
self.0.write_u32(variant_index);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
// A helper that uses Serialize to feed into a Hasher without allocating a buffer:
|
||||
pub fn hash_with_serde_model<T: Serialize>(value: T) -> u64 {
|
||||
let mut h = DefaultHasher::new();
|
||||
let ser = HashSerializer(&mut h);
|
||||
value.serialize(ser).expect("serialize to hasher failed");
|
||||
h.finish()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/// Folds bytes using XOR to produce a u128 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u128.
|
||||
/// If there are fewer than 16 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u128(bytes: &[u8]) -> u128 {
|
||||
let mut result = 0u128;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 16) * 8;
|
||||
result ^= (byte as u128) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Folds bytes using XOR to produce a u64 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u64.
|
||||
/// If there are fewer than 8 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u64(bytes: &[u8]) -> u64 {
|
||||
let mut result = 0u64;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 8) * 8;
|
||||
result ^= (byte as u64) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Folds bytes using XOR to produce a u32 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u32.
|
||||
/// If there are fewer than 4 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u32(bytes: &[u8]) -> u32 {
|
||||
let mut result = 0u32;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 4) * 8;
|
||||
result ^= (byte as u32) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Folds bytes using XOR to produce a u16 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u16.
|
||||
/// If there are fewer than 2 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u16(bytes: &[u8]) -> u16 {
|
||||
let mut result = 0u16;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 2) * 8;
|
||||
result ^= (byte as u16) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Folds bytes using XOR to produce a u8 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them all together to produce a u8.
|
||||
pub fn byte_folding_u8(bytes: &[u8]) -> u8 {
|
||||
bytes.iter().fold(0u8, |acc, &byte| acc ^ byte)
|
||||
}
|
||||
|
||||
/// Computes a djb2 hash of the input bytes.
|
||||
///
|
||||
/// The djb2 algorithm is a simple non-cryptographic hash function created by
|
||||
/// Daniel J. Bernstein. It uses the formula: `hash = hash * 33 + byte` for each byte.
|
||||
///
|
||||
/// This implementation uses the traditional 32-bit hash value and uses wrapping
|
||||
/// arithmetic to handle potential overflows gracefully.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `bytes` - A slice of bytes to hash
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A 32-bit hash value as a `u32`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use control_core::helpers::hashing::hash_djb2;
|
||||
///
|
||||
/// let hash = hash_djb2(b"hello world");
|
||||
/// assert_ne!(hash, 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Algorithm Details
|
||||
///
|
||||
/// - Initial hash value: 5381 (djb2 magic number)
|
||||
/// - For each byte: `hash = hash * 33 + byte`
|
||||
/// - Uses wrapping arithmetic to prevent overflow panics
|
||||
pub fn hash_djb2(bytes: &[u8]) -> u32 {
|
||||
let mut hash: u32 = 5381;
|
||||
|
||||
for byte in bytes {
|
||||
hash = hash.wrapping_mul(33).wrapping_add(*byte as u32);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash() {
|
||||
let test_case = "/dev/ttyUSB1";
|
||||
let hash_u32 = hash_djb2(test_case.as_bytes());
|
||||
let hash_u16 = byte_folding_u16(&hash_u32.to_le_bytes());
|
||||
tracing::info!("{}", test_case);
|
||||
assert_eq!(test_case, "/dev/ttyUSB1");
|
||||
assert_eq!(hash_u32, 266121182);
|
||||
assert_eq!(hash_u16, 40962);
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
/// Normalizes a value from a given range [min, max] to the range [0, 1].
|
||||
///
|
||||
/// This function maps a value from an arbitrary range to the standard
|
||||
/// normalized range [0, 1]. Values o/// Plot for Steepness = -1:ge are clamped
|
||||
/// to the boundaries (0.0 or 1.0).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - The input value to normalize
|
||||
/// * `min` - The minimum value of the input range
|
||||
/// * `max` - The maximum value of the input range
|
||||
///
|
||||
/// # Returns
|
||||
/// * Normalized value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Panics
|
||||
/// * Panics if `min >= max`
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::normalize;
|
||||
/// let normalized = normalize(5.0, 0.0, 10.0); // Returns 0.5
|
||||
/// let clamped = normalize(-2.0, 0.0, 10.0); // Returns 0.0 (clamped)
|
||||
/// ```
|
||||
pub fn normalize<T>(value: T, min: T, max: T) -> f64
|
||||
where
|
||||
T: Into<f64>,
|
||||
{
|
||||
let value: f64 = value.into();
|
||||
let min: f64 = min.into();
|
||||
let max: f64 = max.into();
|
||||
|
||||
if min >= max {
|
||||
panic!("min must be less than max");
|
||||
}
|
||||
if value < min {
|
||||
return 0.0;
|
||||
} else if value > max {
|
||||
return 1.0;
|
||||
}
|
||||
(value - min) / (max - min)
|
||||
}
|
||||
|
||||
/// Scales a normalized value from the range [0, 1] to a new range [new_min, new_max].
|
||||
///
|
||||
/// This function takes a normalized value (typically from 0 to 1) and maps it
|
||||
/// to a new target range. It's the inverse operation of normalization.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `normalized_value` - Input value in range [0.0, 1.0]
|
||||
/// * `new_min` - The minimum value of the target range
|
||||
/// * `new_max` - The maximum value of the target range
|
||||
///
|
||||
/// # Returns
|
||||
/// * Scaled value in the range [new_min, new_max]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::scale;
|
||||
/// let scaled = scale(0.5, 0.0, 100.0); // Returns 50.0
|
||||
/// let negative_range = scale(0.25, -10.0, 10.0); // Returns -5.0
|
||||
/// ```
|
||||
pub fn scale<T>(normalized_value: f64, new_min: T, new_max: T) -> T
|
||||
where
|
||||
T: Into<f64> + From<f64>,
|
||||
{
|
||||
let normalized_value: f64 = normalized_value;
|
||||
let new_min: f64 = new_min.into();
|
||||
let new_max: f64 = new_max.into();
|
||||
|
||||
let result = normalized_value.mul_add(new_max - new_min, new_min);
|
||||
T::from(result)
|
||||
}
|
||||
|
||||
/// Performs hinge interpolation creating a piecewise linear function.
|
||||
///
|
||||
/// This function creates a piecewise linear curve that:
|
||||
/// - For values < x: linearly interpolates from (0, 0) to (x, y)
|
||||
/// - For values > x: linearly interpolates from (x, y) to (1, 1)
|
||||
/// - For value == x: returns y
|
||||
///
|
||||
/// The function assumes the input value is normalized to the range [0, 1],
|
||||
/// and creates a "hinge" or "elbow" shape at the point (x, y).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - The input value (typically in range [0, 1])
|
||||
/// * `x` - The x-coordinate of the hinge point (typically in range [0, 1])
|
||||
/// * `y` - The y-coordinate of the hinge point (typically in range [0, 1])
|
||||
///
|
||||
/// # Returns
|
||||
/// * Interpolated value based on the hinge function
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::interpolate_hinge;
|
||||
/// // Create a hinge at (0.3, 0.7)
|
||||
/// let result1 = interpolate_hinge(0.15, 0.3, 0.7); // Returns 0.35 (halfway to hinge)
|
||||
/// let result2 = interpolate_hinge(0.3, 0.3, 0.7); // Returns 0.7 (at hinge)
|
||||
/// let result3 = interpolate_hinge(0.65, 0.3, 0.7); // Returns 0.85 (halfway from hinge to 1)
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// Hinge at (0.8, 0.2)
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠤⠤⠒⠒⠊⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠒⠒⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⣀⣀⠤⠤⠔⠒⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠉⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
pub fn interpolate_hinge<T>(value: T, x: T, y: T) -> f64
|
||||
where
|
||||
T: Into<f64>,
|
||||
{
|
||||
let value_f64: f64 = value.into();
|
||||
let x_f64: f64 = x.into();
|
||||
let y_f64: f64 = y.into();
|
||||
|
||||
// If we are under X we scale linearly from 0 to y
|
||||
// If we are over X we scale linearly from y to 1
|
||||
if value_f64 < x_f64 {
|
||||
normalize(value_f64, 0.0, x_f64) * y_f64
|
||||
} else if value_f64 > x_f64 {
|
||||
normalize(value_f64, x_f64, 1.0).mul_add(1.0 - y_f64, y_f64)
|
||||
} else {
|
||||
y_f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms a normalized value from the range [0, 1] to a [0, 1] range using a normalized exponential function.
|
||||
///
|
||||
/// This function creates smooth curves that are either convex (fast start, slow end)
|
||||
/// or concave (slow start, fast end) depending on the steepness parameter sign.
|
||||
/// The curve always passes through (0,0) and (1,1).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `normalized_value` - Input value in range [0.0, 1.0]
|
||||
/// * `steepness` - Controls the curve shape:
|
||||
/// - `steepness = 0.0`: Linear interpolation (y = x)
|
||||
/// - `steepness > 0.0`: Convex curve (slow start → fast end)
|
||||
/// - `steepness < 0.0`: Concave curve (fast start → slow end)
|
||||
/// - Typical range: -5.0 to 5.0 for most applications
|
||||
///
|
||||
/// # Returns
|
||||
/// * Transformed value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::interpolate_exponential;
|
||||
/// let convex = interpolate_exponential(0.5, 2.0); // Slow start, fast end
|
||||
/// let concave = interpolate_exponential(0.5, -2.0); // Fast start, slow end
|
||||
/// let linear = interpolate_exponential(0.5, 0.0); // Linear (returns 0.5)
|
||||
/// ```
|
||||
///
|
||||
/// # Plots
|
||||
/// Steepness = -3
|
||||
/// Steepness = -3
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠔⠒⠒⠉⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = -1
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡲⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 0
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢂⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 1
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⢀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢂⣀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 3
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⣀⣀⠤⠤⠒⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠉⠉⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
pub fn interpolate_exponential(normalized_value: f64, steepness: f64) -> f64 {
|
||||
if steepness.abs() < 1e-10 {
|
||||
// When a is very close to zero, the function becomes linear
|
||||
return normalized_value;
|
||||
}
|
||||
let numerator = steepness.exp().powf(normalized_value) - 1.0;
|
||||
let denominator = steepness.exp_m1();
|
||||
|
||||
clip(numerator / denominator)
|
||||
}
|
||||
|
||||
/// A piecewise exponential interpolation function that creates smooth S-curves
|
||||
/// through the points (0,0), (0.5,0.5), and (1,1).
|
||||
///
|
||||
/// The function combines two normalized exponential segments:
|
||||
/// - First half (0 to 0.5): inverted exponential (concave, fast start → slow end)
|
||||
/// - Second half (0.5 to 1): standard exponential (convex, slow start → fast end)
|
||||
///
|
||||
/// This creates an S-like curve that's concave then convex, similar to
|
||||
/// a smoothed step function or sigmoid-like transition.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - Input value, should be in range [0.0, 1.0]
|
||||
/// * `steepness` - Controls how steep the S-curve is
|
||||
/// - `steepness = 0.0`: Linear interpolation (y = x)
|
||||
/// - `steepness > 0.0`: More pronounced S-curve
|
||||
/// - Higher values create sharper transitions at the boundaries
|
||||
/// - Typical range: 0.5 to 5.0 for most applications
|
||||
///
|
||||
/// # Returns
|
||||
/// * Interpolated y value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::interpolate_inflected_exponential;
|
||||
/// let y = interpolate_inflected_exponential(0.25, 2.0); // Gentle S-curve
|
||||
/// let y = interpolate_inflected_exponential(0.75, 5.0); // Sharp S-curve
|
||||
/// ```
|
||||
///
|
||||
/// # Plots
|
||||
/// Steepness = 0.5
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⢀⠤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡢⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 1
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡲⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 2
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠒⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 5
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠉⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠤⠤⠤⠤⠤⠤⠒⠒⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⢀⠔⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⣁⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
pub fn interpolate_inflected_exponential(x: f64, steepness: f64) -> f64 {
|
||||
debug_assert!((0.0..=1.0).contains(&x), "x must be in range [0.0, 1.0]");
|
||||
debug_assert!(steepness >= 0.0, "steepness must be non-negative");
|
||||
|
||||
if steepness == 0.0 {
|
||||
// Linear case
|
||||
return x;
|
||||
}
|
||||
|
||||
if x <= 0.5 {
|
||||
// First segment: inverted exponential (concave)
|
||||
// Maps [0, 0.5] → [0, 0.5] with concave shape
|
||||
let x_norm = 2.0 * x; // Scale to [0, 1]
|
||||
|
||||
// Use inverted exponential interpolation
|
||||
let result = 1.0 - interpolate_exponential(1.0 - x_norm, steepness);
|
||||
0.5 * result
|
||||
} else {
|
||||
// Second segment: standard exponential (convex)
|
||||
// Maps [0.5, 1] → [0.5, 1] with convex shape
|
||||
let x_norm = 2.0 * (x - 0.5); // Scale to [0, 1]
|
||||
|
||||
// Use standard exponential interpolation
|
||||
let result = interpolate_exponential(x_norm, steepness);
|
||||
0.5f64.mul_add(result, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverts a value from the range [0, 1] to [1, 0].
|
||||
///
|
||||
/// This function flips a normalized value, where 0 becomes 1 and 1 becomes 0.
|
||||
/// Values outside [0, 1] are first clamped to this range before inversion.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - Input value (will be clamped to [0.0, 1.0])
|
||||
///
|
||||
/// # Returns
|
||||
/// * Inverted value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::invert;
|
||||
/// let inverted = invert(0.3); // Returns 0.7
|
||||
/// let boundary = invert(0.0); // Returns 1.0
|
||||
/// let clamped = invert(1.5); // Returns 0.0 (input clamped to 1.0 first)
|
||||
/// ```
|
||||
pub fn invert(value: f64) -> f64 {
|
||||
let value = clip(value);
|
||||
1.0 - value
|
||||
}
|
||||
|
||||
/// Clips a value to the range [0, 1].
|
||||
///
|
||||
/// This function ensures that any input value is constrained to the
|
||||
/// normalized range [0, 1] by clamping values below 0 to 0 and
|
||||
/// values above 1 to 1.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - Input value to clip
|
||||
///
|
||||
/// # Returns
|
||||
/// * Clipped value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::clip;
|
||||
/// let normal = clip(0.5); // Returns 0.5
|
||||
/// let low = clip(-0.3); // Returns 0.0
|
||||
/// let high = clip(1.7); // Returns 1.0
|
||||
/// ```
|
||||
pub fn clip(value: f64) -> f64 {
|
||||
if value < 0.0 {
|
||||
return 0.0;
|
||||
} else if value > 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const EPSILON: f64 = 1e-10;
|
||||
|
||||
#[test]
|
||||
fn test_scale() {
|
||||
// Basic normalization with same types
|
||||
assert_relative_eq!(normalize(5.0f64, 0.0f64, 10.0f64), 0.5, epsilon = EPSILON);
|
||||
assert_relative_eq!(normalize(5.0f32, 0.0f32, 10.0f32), 0.5, epsilon = EPSILON);
|
||||
assert_relative_eq!(normalize(5i32, 0i32, 10i32), 0.5, epsilon = EPSILON);
|
||||
assert_relative_eq!(normalize(5u8, 0u8, 10u8), 0.5, epsilon = EPSILON);
|
||||
|
||||
// Edge cases
|
||||
assert_relative_eq!(normalize(0.0, 0.0, 10.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(normalize(10.0, 0.0, 10.0), 1.0, epsilon = EPSILON);
|
||||
|
||||
// Out of range (should clamp)
|
||||
assert_relative_eq!(normalize(-5.0, 0.0, 10.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(normalize(15.0, 0.0, 10.0), 1.0, epsilon = EPSILON);
|
||||
|
||||
// Different ranges
|
||||
assert_relative_eq!(normalize(0.0, -10.0, 10.0), 0.5, epsilon = EPSILON);
|
||||
assert_relative_eq!(normalize(5.0, -5.0, 5.0), 1.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_linear() {
|
||||
// Basic interpolation - f64
|
||||
let result_f64: f64 = scale(0.5, 0.0, 10.0);
|
||||
assert_relative_eq!(result_f64, 5.0, epsilon = EPSILON);
|
||||
|
||||
// For non-f64 return types, we need to use a separate implementation
|
||||
// that uses `as` for the conversion
|
||||
|
||||
// For testing f32 output with f32 input - use direct calculation
|
||||
let normalized_value = 0.5;
|
||||
let new_min_f32 = 0.0f32;
|
||||
let new_max_f32 = 10.0f32;
|
||||
let expected_f32 = (normalized_value * (new_max_f32 as f64 - new_min_f32 as f64)
|
||||
+ new_min_f32 as f64) as f32;
|
||||
assert_relative_eq!(expected_f32, 5.0f32, epsilon = EPSILON as f32);
|
||||
|
||||
// Testing i32 return type with direct calculation
|
||||
let new_min_i32 = 0i32;
|
||||
let new_max_i32 = 10i32;
|
||||
let expected_i32 = (normalized_value * (new_max_i32 as f64 - new_min_i32 as f64)
|
||||
+ new_min_i32 as f64) as i32;
|
||||
assert_eq!(expected_i32, 5i32);
|
||||
|
||||
// Edge cases
|
||||
assert_relative_eq!(scale(0.0, 0.0, 10.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(scale(1.0, 0.0, 10.0), 10.0, epsilon = EPSILON);
|
||||
|
||||
// Negative ranges
|
||||
assert_relative_eq!(scale(0.5, -10.0, 10.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(scale(0.5, -20.0, -10.0), -15.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_exponential() {
|
||||
// Test boundary points (0,0) and (1,1) for various values of a
|
||||
for a in [-10.0, -5.0, -1.0, -0.1, 0.1, 1.0, 5.0, 10.0].iter() {
|
||||
assert_relative_eq!(interpolate_exponential(0.0, *a), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(interpolate_exponential(1.0, *a), 1.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
// Test behavior for x outside [0,1]
|
||||
let a = 2.0;
|
||||
assert_relative_eq!(interpolate_exponential(-0.1, a), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(interpolate_exponential(1.1, a), 1.0, epsilon = EPSILON);
|
||||
|
||||
// Test monotonicity - function should be strictly increasing for any a
|
||||
for a in [-10.0, -1.0, 0.0, 1.0, 10.0].iter() {
|
||||
let mut prev = interpolate_exponential(0.0, *a);
|
||||
for i in 1..=10 {
|
||||
let x = i as f64 / 10.0;
|
||||
let current = interpolate_exponential(x, *a);
|
||||
assert!(
|
||||
current > prev,
|
||||
"Failed monotonicity test at x={}, a={}",
|
||||
x,
|
||||
a
|
||||
);
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
// Test symmetry property: f(x, a) + f(1-x, -a) ≈ 1
|
||||
for x in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].iter() {
|
||||
for a in [0.5, 1.0, 2.0, 5.0].iter() {
|
||||
let sum = interpolate_exponential(*x, *a) + interpolate_exponential(1.0 - *x, -*a);
|
||||
assert_relative_eq!(sum, 1.0, epsilon = 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invert() {
|
||||
assert_relative_eq!(invert(0.0), 1.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(invert(1.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(invert(0.3), 0.7, epsilon = EPSILON);
|
||||
assert_relative_eq!(invert(0.5), 0.5, epsilon = EPSILON);
|
||||
|
||||
// Test clipping instead of panicking
|
||||
assert_relative_eq!(invert(-0.1), 1.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(invert(1.1), 0.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clip() {
|
||||
assert_relative_eq!(clip(0.5), 0.5, epsilon = EPSILON);
|
||||
assert_relative_eq!(clip(0.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(clip(1.0), 1.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(clip(-0.5), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(clip(1.5), 1.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_inflected_exponential() {
|
||||
// Test boundary points (0,0), (0.5,0.5), and (1,1) for various steepness values
|
||||
for steepness in [0.0, 0.5, 1.0, 2.0, 5.0].iter() {
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(0.0, *steepness),
|
||||
0.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(0.5, *steepness),
|
||||
0.5,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(1.0, *steepness),
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Test linear case (steepness = 0)
|
||||
for x in [0.0, 0.25, 0.5, 0.75, 1.0].iter() {
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(*x, 0.0),
|
||||
*x,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Test monotonicity - function should be strictly increasing
|
||||
for steepness in [0.5, 1.0, 2.0, 5.0].iter() {
|
||||
let mut prev = interpolate_inflected_exponential(0.0, *steepness);
|
||||
for i in 1..=20 {
|
||||
let x = i as f64 / 20.0;
|
||||
let current = interpolate_inflected_exponential(x, *steepness);
|
||||
assert!(
|
||||
current >= prev,
|
||||
"Failed monotonicity test at x={}, steepness={}, prev={}, current={}",
|
||||
x,
|
||||
steepness,
|
||||
prev,
|
||||
current
|
||||
);
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
// Test S-curve property: first half should be concave, second half convex
|
||||
let steepness = 2.0;
|
||||
|
||||
// First half: check concave property (derivative decreasing)
|
||||
let x1 = 0.1;
|
||||
let x2 = 0.2;
|
||||
let x3 = 0.3;
|
||||
let y1 = interpolate_inflected_exponential(x1, steepness);
|
||||
let y2 = interpolate_inflected_exponential(x2, steepness);
|
||||
let y3 = interpolate_inflected_exponential(x3, steepness);
|
||||
|
||||
let slope1 = (y2 - y1) / (x2 - x1);
|
||||
let slope2 = (y3 - y2) / (x3 - x2);
|
||||
assert!(
|
||||
slope1 > slope2,
|
||||
"First half should be concave (decreasing slope)"
|
||||
);
|
||||
|
||||
// Second half: check convex property (derivative increasing)
|
||||
let x1 = 0.7;
|
||||
let x2 = 0.8;
|
||||
let x3 = 0.9;
|
||||
let y1 = interpolate_inflected_exponential(x1, steepness);
|
||||
let y2 = interpolate_inflected_exponential(x2, steepness);
|
||||
let y3 = interpolate_inflected_exponential(x3, steepness);
|
||||
|
||||
let slope1 = (y2 - y1) / (x2 - x1);
|
||||
let slope2 = (y3 - y2) / (x3 - x2);
|
||||
assert!(
|
||||
slope1 < slope2,
|
||||
"Second half should be convex (increasing slope)"
|
||||
);
|
||||
|
||||
// Test that output is always in [0, 1] range
|
||||
for steepness in [0.0, 1.0, 5.0, 10.0].iter() {
|
||||
for i in 0..=100 {
|
||||
let x = i as f64 / 100.0;
|
||||
let y = interpolate_inflected_exponential(x, *steepness);
|
||||
assert!(
|
||||
y >= 0.0 && y <= 1.0,
|
||||
"Output {} out of range [0,1] for x={}, steepness={}",
|
||||
y,
|
||||
x,
|
||||
steepness
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_hinge() {
|
||||
// Test basic functionality with hinge at (0.3, 0.7)
|
||||
let x_hinge = 0.3;
|
||||
let y_hinge = 0.7;
|
||||
|
||||
// Test exact hinge point
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(x_hinge, x_hinge, y_hinge),
|
||||
y_hinge,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test left segment (value < x): should interpolate from (0,0) to (x,y)
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.0, x_hinge, y_hinge),
|
||||
0.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.15, x_hinge, y_hinge),
|
||||
0.35, // 0.15/0.3 * 0.7 = 0.5 * 0.7 = 0.35
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test right segment (value > x): should interpolate from (x,y) to (1,1)
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(1.0, x_hinge, y_hinge),
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.65, x_hinge, y_hinge),
|
||||
0.85, // y + ((0.65-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + 0.5 * 0.3 = 0.85
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test with different hinge points
|
||||
// Hinge at (0.5, 0.5) - should be linear
|
||||
assert_relative_eq!(interpolate_hinge(0.25, 0.5, 0.5), 0.25, epsilon = EPSILON);
|
||||
assert_relative_eq!(interpolate_hinge(0.75, 0.5, 0.5), 0.75, epsilon = EPSILON);
|
||||
|
||||
// Hinge at (0.2, 0.9) - steep left, shallow right
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.1, 0.2, 0.9),
|
||||
0.45, // 0.1/0.2 * 0.9 = 0.5 * 0.9 = 0.45
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.6, 0.2, 0.9),
|
||||
0.95, // 0.9 + ((0.6-0.2)/(1.0-0.2)) * (1.0-0.9) = 0.9 + 0.5 * 0.1 = 0.95
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test edge cases
|
||||
// Hinge at (0, 0)
|
||||
assert_relative_eq!(interpolate_hinge(0.0, 0.0, 0.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.5, 0.0, 0.0),
|
||||
0.5, // Should interpolate from (0,0) to (1,1)
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Hinge at (1, 1)
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.5, 1.0, 1.0),
|
||||
0.5, // Should interpolate from (0,0) to (1,1)
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(interpolate_hinge(1.0, 1.0, 1.0), 1.0, epsilon = EPSILON);
|
||||
|
||||
// Test with different numeric types
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.5f32, 0.3f32, 0.7f32),
|
||||
0.7857142857142857, // 0.7 + ((0.5-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + (2/7) * 0.3
|
||||
epsilon = 1e-7 // Relaxed epsilon for f32 precision
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(1u8, 0u8, 1u8), // 1.0, 0.0, 1.0 - should return 1.0
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test monotonicity - function should be non-decreasing
|
||||
let test_x = 0.4;
|
||||
let test_y = 0.6;
|
||||
let mut prev = interpolate_hinge(0.0, test_x, test_y);
|
||||
|
||||
for i in 1..=20 {
|
||||
let value = i as f64 / 20.0;
|
||||
let current = interpolate_hinge(value, test_x, test_y);
|
||||
assert!(
|
||||
current >= prev,
|
||||
"Failed monotonicity test at value={}, x={}, y={}, prev={}, current={}",
|
||||
value,
|
||||
test_x,
|
||||
test_y,
|
||||
prev,
|
||||
current
|
||||
);
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_exponential_graph() {
|
||||
use textplots::{Chart, Plot, Shape};
|
||||
|
||||
println!("\n=== Exponential Interpolation Examples ===");
|
||||
|
||||
// Test different steepness values (both positive and negative)
|
||||
let steepness_values = [-3.0, -1.0, 0.0, 1.0, 3.0];
|
||||
|
||||
for steepness in steepness_values.iter() {
|
||||
println!("\nSteepness = {}", steepness);
|
||||
|
||||
// Generate data points
|
||||
let points: Vec<(f32, f32)> = (0..=50)
|
||||
.map(|i| {
|
||||
let x = i as f64 / 50.0;
|
||||
let y = interpolate_exponential(x, *steepness);
|
||||
(x as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create and display chart
|
||||
Chart::new(60, 60, 0.0, 1.0)
|
||||
.lineplot(&Shape::Lines(&points))
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_hinge_graph() {
|
||||
use textplots::{Chart, Plot, Shape};
|
||||
|
||||
println!("\n=== hinge Interpolation Examples ===");
|
||||
|
||||
// Test different hinge points
|
||||
let hinges = [(0.3, 0.7), (0.5, 0.5), (0.2, 0.9), (0.8, 0.2)];
|
||||
|
||||
for &(x_hinge, y_hinge) in hinges.iter() {
|
||||
println!("\nHinge at ({}, {})", x_hinge, y_hinge);
|
||||
|
||||
// Generate data points
|
||||
let points: Vec<(f32, f32)> = (0..=50)
|
||||
.map(|i| {
|
||||
let x = i as f64 / 50.0;
|
||||
let y = interpolate_hinge(x, x_hinge, y_hinge);
|
||||
(x as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create and display chart
|
||||
Chart::new(60, 60, 0.0, 1.0)
|
||||
.lineplot(&Shape::Lines(&points))
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_inflected_exponential_graph() {
|
||||
use textplots::{Chart, Plot, Shape};
|
||||
|
||||
// Test different steepness values
|
||||
let steepness_values = [0.5, 1.0, 2.0, 5.0];
|
||||
|
||||
for steepness in steepness_values.iter() {
|
||||
println!("\nSteepness = {}", steepness);
|
||||
|
||||
// Generate data points
|
||||
let points: Vec<(f32, f32)> = (0..=50)
|
||||
.map(|i| {
|
||||
let x = i as f64 / 50.0;
|
||||
let y = interpolate_inflected_exponential(x, *steepness);
|
||||
(x as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create and display chart
|
||||
Chart::new(60, 60, 0.0, 1.0)
|
||||
.lineplot(&Shape::Lines(&points))
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_normalize_invalid_range() {
|
||||
normalize(5.0, 10.0, 5.0); // min >= max should panic
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod compare_lists;
|
||||
pub mod hasher_serializer;
|
||||
pub mod hashing;
|
||||
pub mod interpolation;
|
||||
pub mod moving_time_window;
|
||||
pub mod retry;
|
||||
@@ -0,0 +1,243 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
iter::Sum,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// A helper trait to handle division for averaging
|
||||
pub trait DivideByCount {
|
||||
fn divide_by_count(self, count: usize) -> Self;
|
||||
}
|
||||
|
||||
impl DivideByCount for f64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for f32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for i32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for i64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for u32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for u64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
// Type aliases for common use cases
|
||||
pub type F64Window = MovingTimeWindow<f64>;
|
||||
pub type F32Window = MovingTimeWindow<f32>;
|
||||
pub type I32Window = MovingTimeWindow<i32>;
|
||||
pub type I64Window = MovingTimeWindow<i64>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MovingTimeWindow<T>
|
||||
where
|
||||
T: Copy + PartialOrd + Sum + DivideByCount + std::fmt::Debug + Default,
|
||||
{
|
||||
max_samples: usize,
|
||||
samples: VecDeque<(T, Instant)>,
|
||||
last_sample: Option<(T, Instant)>,
|
||||
sample_time: Duration,
|
||||
cached_average: Option<T>,
|
||||
cached_max: Option<T>,
|
||||
cached_min: Option<T>,
|
||||
}
|
||||
|
||||
/// A time-based moving window that maintains samples within a specified duration and count limit.
|
||||
///
|
||||
/// This structure efficiently tracks values over time, automatically removing samples that fall
|
||||
/// outside the configured time window or exceed the maximum sample count. It provides cached
|
||||
/// average calculation to avoid redundant computations.
|
||||
impl<T> MovingTimeWindow<T>
|
||||
where
|
||||
T: Copy + PartialOrd + Sum + DivideByCount + std::fmt::Debug + Default,
|
||||
{
|
||||
/// Creates a new moving time window with the specified duration and maximum sample count.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The time window duration; samples older than this will be automatically removed
|
||||
/// * `samples` - Maximum number of samples to retain, regardless of their age
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `MovingTimeWindow` instance with empty sample collection and initialized cache
|
||||
pub fn new(duration: Duration, max_samples: usize) -> Self {
|
||||
Self {
|
||||
max_samples,
|
||||
samples: VecDeque::with_capacity(max_samples),
|
||||
last_sample: None,
|
||||
sample_time: duration / (max_samples as u32),
|
||||
cached_average: None,
|
||||
cached_max: None,
|
||||
cached_min: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the window with a new sample value at the given timestamp.
|
||||
///
|
||||
/// This method adds the new sample to the window and automatically removes samples that:
|
||||
/// - Are older than the configured duration from the current timestamp
|
||||
/// - Exceed the maximum sample count limit
|
||||
///
|
||||
/// The cached average is invalidated after this operation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The numeric value to add to the window
|
||||
/// * `timestamp` - The time at which this sample was taken
|
||||
pub fn update(&mut self, value: T, timestamp: Instant) {
|
||||
// Chekc if we should add a new sample based on the sample time and the last sample
|
||||
if let Some((_, last_timestamp)) = self.last_sample {
|
||||
if timestamp.duration_since(last_timestamp) < self.sample_time {
|
||||
// If the new sample is too close to the last one, we skip adding it
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have reached the maximum sample count, remove the oldest sample
|
||||
if self.samples.len() >= self.max_samples {
|
||||
self.samples.pop_back();
|
||||
}
|
||||
|
||||
// Add the new sample
|
||||
self.samples.push_front((value, timestamp));
|
||||
self.last_sample = Some((value, timestamp));
|
||||
|
||||
// Invalidate all cached values since we added a new sample
|
||||
self.cached_average = None;
|
||||
self.cached_max = None;
|
||||
self.cached_min = None;
|
||||
}
|
||||
|
||||
/// Calculates and returns the average of all samples currently in the window.
|
||||
///
|
||||
/// This method uses caching to avoid redundant calculations. The average is only
|
||||
/// recalculated when the window contents have changed since the last call.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The arithmetic mean of all current samples, or T::default() if the window is empty
|
||||
pub fn average(&mut self) -> T {
|
||||
// Return cached value if available
|
||||
if let Some(cached) = self.cached_average {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let sum: T = self.samples.iter().map(|(v, _)| *v).sum();
|
||||
let count = self.samples.len();
|
||||
|
||||
let avg = if count > 0 {
|
||||
sum.divide_by_count(count)
|
||||
} else {
|
||||
T::default()
|
||||
};
|
||||
|
||||
// Cache the calculated value
|
||||
self.cached_average = Some(avg);
|
||||
avg
|
||||
}
|
||||
|
||||
/// Calculates and returns the maximum value of all samples currently in the window.
|
||||
///
|
||||
/// This method uses caching to avoid redundant calculations. The maximum is only
|
||||
/// recalculated when the window contents have changed since the last call.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The maximum value of all current samples, or T::default() if the window is empty
|
||||
pub fn max(&mut self) -> T {
|
||||
// Return cached value if available
|
||||
if let Some(cached) = self.cached_max {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let result = self
|
||||
.samples
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.fold(None, |acc, val| {
|
||||
acc.map_or(Some(val), |current_max| {
|
||||
if val > current_max {
|
||||
Some(val)
|
||||
} else {
|
||||
Some(current_max)
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Cache the calculated value
|
||||
self.cached_max = Some(result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Calculates and returns the minimum value of all samples currently in the window.
|
||||
///
|
||||
/// This method uses caching to avoid redundant calculations. The minimum is only
|
||||
/// recalculated when the window contents have changed since the last call.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The minimum value of all current samples, or T::default() if the window is empty
|
||||
pub fn min(&mut self) -> T {
|
||||
// Return cached value if available
|
||||
if let Some(cached) = self.cached_min {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let result = self
|
||||
.samples
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.fold(None, |acc, val| {
|
||||
acc.map_or(Some(val), |current_min| {
|
||||
if val < current_min {
|
||||
Some(val)
|
||||
} else {
|
||||
Some(current_min)
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Cache the calculated value
|
||||
self.cached_min = Some(result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generic() {
|
||||
// Test that the struct compiles with basic numeric types
|
||||
let _: MovingTimeWindow<f64> = MovingTimeWindow::new(Duration::from_secs(1), 1);
|
||||
let _: MovingTimeWindow<f32> = MovingTimeWindow::new(Duration::from_secs(1), 1);
|
||||
let _: MovingTimeWindow<i32> = MovingTimeWindow::new(Duration::from_secs(1), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
use std::future::Future;
|
||||
|
||||
/// Retries a fallible operation up to `n` times synchronously.
|
||||
///
|
||||
/// This function executes the provided closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - All retry attempts are exhausted
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
|
||||
/// * `f` - A mutable closure that returns a `Result<T, E>`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt if all retries are exhausted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_n_times;
|
||||
///
|
||||
/// let mut attempt = 0;
|
||||
/// let result = retry_n_times(3, || {
|
||||
/// attempt += 1;
|
||||
/// if attempt < 3 {
|
||||
/// Err("failed")
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// });
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// ```
|
||||
pub fn retry_n_times<T, E, F>(n: usize, mut f: F) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Result<T, E>,
|
||||
{
|
||||
let mut last_err: Option<E> = None;
|
||||
for _ in 0..=n {
|
||||
match f() {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap())
|
||||
}
|
||||
|
||||
/// Retries a fallible async operation up to `n` times.
|
||||
///
|
||||
/// This function executes the provided async closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - All retry attempts are exhausted
|
||||
///
|
||||
/// Each attempt is awaited before proceeding to the next retry or returning.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
|
||||
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt if all retries are exhausted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_n_times_async;
|
||||
/// use std::sync::Arc;
|
||||
/// use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
///
|
||||
/// async fn example() {
|
||||
/// let attempt = Arc::new(AtomicUsize::new(0));
|
||||
/// let attempt_clone = attempt.clone();
|
||||
/// let result = retry_n_times_async(3, move || {
|
||||
/// let attempt = attempt_clone.clone();
|
||||
/// async move {
|
||||
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
/// if current < 3 {
|
||||
/// Err("failed")
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// }
|
||||
/// }).await;
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry_n_times_async<T, E, F, Fut>(n: usize, mut f: F) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<T, E>>,
|
||||
{
|
||||
let mut last_err: Option<E> = None;
|
||||
for _ in 0..=n {
|
||||
match f().await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap())
|
||||
}
|
||||
|
||||
/// Retries a fallible operation until a closure decides to stop retrying.
|
||||
///
|
||||
/// This function executes the provided closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - The retry condition returns false
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `f` - A mutable closure that returns a `Result<T, E>`
|
||||
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt when retries stop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_conditionally;
|
||||
///
|
||||
/// let mut attempt = 0;
|
||||
/// let result = retry_conditionally(
|
||||
/// || {
|
||||
/// attempt += 1;
|
||||
/// if attempt < 3 {
|
||||
/// Err(format!("failed attempt {}", attempt))
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// },
|
||||
/// |err| {
|
||||
/// // Retry up to 3 times
|
||||
/// !err.contains("attempt 3")
|
||||
/// }
|
||||
/// );
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// ```
|
||||
pub fn retry_conditionally<T, E, F, R>(mut f: F, mut should_retry: R) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Result<T, E>,
|
||||
R: FnMut(&E) -> bool,
|
||||
{
|
||||
loop {
|
||||
match f() {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if !should_retry(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retries a fallible async operation until a closure decides to stop retrying.
|
||||
///
|
||||
/// This function executes the provided async closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - The retry condition returns false
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
|
||||
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt when retries stop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_conditionally_async;
|
||||
/// use std::sync::Arc;
|
||||
/// use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
///
|
||||
/// async fn example() {
|
||||
/// let attempt = Arc::new(AtomicUsize::new(0));
|
||||
/// let attempt_clone = attempt.clone();
|
||||
/// let result = retry_conditionally_async(
|
||||
/// move || {
|
||||
/// let attempt = attempt_clone.clone();
|
||||
/// async move {
|
||||
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
/// if current < 3 {
|
||||
/// Err(format!("failed attempt {}", current))
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// }
|
||||
/// },
|
||||
/// |err| {
|
||||
/// // Retry up to 3 times
|
||||
/// !err.contains("attempt 3")
|
||||
/// }
|
||||
/// ).await;
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry_conditionally_async<T, E, F, Fut, R>(
|
||||
mut f: F,
|
||||
mut should_retry: R,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<T, E>>,
|
||||
R: FnMut(&E) -> bool,
|
||||
{
|
||||
loop {
|
||||
match f().await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if !should_retry(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Simple blocking executor for async functions in tests
|
||||
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::thread;
|
||||
|
||||
struct DummyWaker;
|
||||
impl std::task::Wake for DummyWaker {
|
||||
fn wake(self: Arc<Self>) {}
|
||||
}
|
||||
|
||||
let waker = Waker::from(Arc::new(DummyWaker));
|
||||
let mut context = Context::from_waker(&waker);
|
||||
let mut future = Box::pin(future);
|
||||
|
||||
loop {
|
||||
match future.as_mut().poll(&mut context) {
|
||||
Poll::Ready(result) => return result,
|
||||
Poll::Pending => {
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_success_first_attempt() {
|
||||
let result = retry_n_times(3, || Ok::<i32, &str>(42));
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_success_after_failures() {
|
||||
let mut attempt = 0;
|
||||
let result = retry_n_times(3, || {
|
||||
attempt += 1;
|
||||
if attempt < 3 { Err("failed") } else { Ok(42) }
|
||||
});
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(attempt, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_all_attempts_fail() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, &str> = retry_n_times(2, || {
|
||||
attempt += 1;
|
||||
Err("always fails")
|
||||
});
|
||||
assert_eq!(result, Err("always fails"));
|
||||
assert_eq!(attempt, 3); // 0..=2 means 3 attempts
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_zero_retries() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, &str> = retry_n_times(0, || {
|
||||
attempt += 1;
|
||||
Err("failed")
|
||||
});
|
||||
assert_eq!(result, Err("failed"));
|
||||
assert_eq!(attempt, 1); // Only one attempt
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_success_first_attempt() {
|
||||
let result = block_on(retry_n_times_async(3, || async { Ok::<i32, &str>(42) }));
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_success_after_failures() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result = block_on(retry_n_times_async(3, move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
if *a < 3 { Err("failed") } else { Ok(42) }
|
||||
}
|
||||
}));
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(*attempt.borrow(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_all_attempts_fail() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result: Result<i32, &str> = block_on(retry_n_times_async(2, move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
Err("always fails")
|
||||
}
|
||||
}));
|
||||
assert_eq!(result, Err("always fails"));
|
||||
assert_eq!(*attempt.borrow(), 3); // 0..=2 means 3 attempts
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_and_success_with_condition() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, String> = retry_conditionally(
|
||||
|| {
|
||||
attempt += 1;
|
||||
if attempt < 3 {
|
||||
Err(format!("failed attempt {}", attempt))
|
||||
} else {
|
||||
Ok(42)
|
||||
}
|
||||
},
|
||||
|err| {
|
||||
// Retry as long as it's not the 3rd attempt
|
||||
!err.contains("attempt 3")
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(attempt, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_and_stop_early() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, String> = retry_conditionally(
|
||||
|| {
|
||||
attempt += 1;
|
||||
Err(format!("failed attempt {}", attempt))
|
||||
},
|
||||
|_err| {
|
||||
// Stop after first failure
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(attempt, 1);
|
||||
assert_eq!(result.unwrap_err(), "failed attempt 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_success_with_condition() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result: Result<i32, String> = block_on(retry_conditionally_async(
|
||||
move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
if *a < 3 {
|
||||
Err(format!("failed attempt {}", *a))
|
||||
} else {
|
||||
Ok(42)
|
||||
}
|
||||
}
|
||||
},
|
||||
|err| {
|
||||
// Retry as long as it's not the 3rd attempt
|
||||
!err.contains("attempt 3")
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(*attempt.borrow(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_stop_early() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result: Result<i32, String> = block_on(retry_conditionally_async(
|
||||
move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
Err(format!("failed attempt {}", *a))
|
||||
}
|
||||
},
|
||||
|_err| {
|
||||
// Stop after first failure
|
||||
false
|
||||
},
|
||||
));
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(*attempt.borrow(), 1);
|
||||
assert_eq!(result.unwrap_err(), "failed attempt 1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
use libc;
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::str;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_proc_interrupts() -> Result<String, io::Error> {
|
||||
let path = CString::new("/proc/interrupts").unwrap();
|
||||
// O_RDONLY
|
||||
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::from_raw_os_error(-fd));
|
||||
}
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut chunk = [0u8; 8192];
|
||||
|
||||
loop {
|
||||
let n = unsafe { libc::read(fd, chunk.as_mut_ptr() as *mut _, chunk.len()) };
|
||||
if n < 0 {
|
||||
let e = io::Error::from_raw_os_error(-n as i32);
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(e);
|
||||
} else if n == 0 {
|
||||
break;
|
||||
} else {
|
||||
buf.extend_from_slice(&chunk[..n as usize]);
|
||||
}
|
||||
}
|
||||
unsafe { libc::close(fd) };
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_interface_irq(proc_content: &str, interface_name: &str) -> Option<u32> {
|
||||
for line in proc_content.lines() {
|
||||
if line.contains(interface_name) {
|
||||
if let Some(colon_pos) = line.find(':') {
|
||||
let head = &line[..colon_pos].trim();
|
||||
// try parse head as integer IRQ number
|
||||
if let Ok(irq) = head.parse::<u32>() {
|
||||
return Some(irq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Since kernel 3.0 it’s possible to use the /proc/irq/<IRQ-NUMBER>/smp_affinity_list
|
||||
/// With comma seperated values
|
||||
/// This function takes the irq identifier and writes the cpu string
|
||||
/// into /proc/irq/irq_number/smp_affinity_list
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_irq_affinity_raw(irq: u32, cpu: &str) -> Result<(), io::Error> {
|
||||
let path: String = format!("/proc/irq/{}/smp_affinity_list", irq);
|
||||
let cpath = CString::new(path.clone()).unwrap();
|
||||
// We want to completely overwrite the smp affinities, so that we guarentee execution on the specified cores
|
||||
let file_descriptor = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) };
|
||||
|
||||
if file_descriptor < 0 {
|
||||
tracing::info!("Permission denied to write: {}", path);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("failed to open {}: errno {}", path, -file_descriptor),
|
||||
));
|
||||
}
|
||||
// convert to "raw bytes"
|
||||
// cpu needs to be a string because we need the raw ascii code 48 = '0' and so on as bytes
|
||||
let bytes = cpu.as_bytes();
|
||||
let mut bytes_written_total = 0usize;
|
||||
|
||||
while bytes_written_total < bytes.len() {
|
||||
let to_write = &bytes[bytes_written_total..];
|
||||
let bytes_written = unsafe {
|
||||
libc::write(
|
||||
file_descriptor,
|
||||
to_write.as_ptr() as *const _,
|
||||
to_write.len(),
|
||||
)
|
||||
};
|
||||
if bytes_written < 0 {
|
||||
let e = io::Error::from_raw_os_error(-bytes_written as i32);
|
||||
unsafe { libc::close(file_descriptor) };
|
||||
return Err(e);
|
||||
}
|
||||
bytes_written_total += bytes_written as usize;
|
||||
}
|
||||
unsafe { libc::close(file_descriptor) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is used to make sure that a given irq handler runs on the expected cpu
|
||||
/// On Realtime systems this is the make or break fix for certain usecases like ethercrab
|
||||
/// On our machines the 99.99th percentile of cycle times was very bad ~5-11 ms while ethercat is supposed to be in the microseconds
|
||||
/// After pinning the ethernet irq to the core that the ethercat code ran on 99.99th percentile went down to ~200us (microseconds)
|
||||
/// Example input: irq_name: "eno1" , cpu: 2
|
||||
/// Remember that cpu cores are counted from 0
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_irq_affinity(irq_name: &str, cpu: u32) -> Result<(), anyhow::Error> {
|
||||
let proc_contents = read_proc_interrupts()?;
|
||||
let irq = get_interface_irq(&proc_contents, irq_name);
|
||||
if irq.is_none() {
|
||||
return Err(anyhow::anyhow!("Couldnt find irq number!"));
|
||||
}
|
||||
let res = set_irq_affinity_raw(irq.unwrap(), &cpu.to_string());
|
||||
if res.is_err() {
|
||||
return Err(anyhow::anyhow!("Couldnt write affinity list!"));
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
pub mod actors;
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod machines;
|
||||
pub mod helpers;
|
||||
pub mod irq_handling;
|
||||
pub mod modbus;
|
||||
pub mod rest;
|
||||
pub mod realtime;
|
||||
pub mod serial;
|
||||
pub mod socketio;
|
||||
pub mod transmission;
|
||||
|
||||
#[cfg(feature = "video-streaming")]
|
||||
pub mod video_streaming;
|
||||
@@ -1,7 +0,0 @@
|
||||
use crate::socketio::namespace::NamespaceInterface;
|
||||
use serde_json::Value;
|
||||
|
||||
pub trait MachineApi {
|
||||
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
|
||||
fn api_event_namespace(&mut self) -> &mut dyn NamespaceInterface;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Identifies a spacifi machine
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct MachineIdentificationUnique {
|
||||
pub machine_identification: MachineIdentification,
|
||||
pub serial: u16,
|
||||
}
|
||||
|
||||
impl MachineIdentificationUnique {
|
||||
/// Check if values are non-zero
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.machine_identification.is_valid() && self.serial != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a machine
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct MachineIdentification {
|
||||
pub vendor: u16,
|
||||
pub machine: u16,
|
||||
}
|
||||
|
||||
impl MachineIdentification {
|
||||
/// Check if values are non-zero
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.vendor != 0 && self.machine != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DeviceMachineIdentification {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub role: u16,
|
||||
}
|
||||
|
||||
impl DeviceMachineIdentification {
|
||||
/// Check if values are non-zero
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.machine_identification_unique.is_valid()
|
||||
&& self
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
.machine
|
||||
!= 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DeviceIdentification {
|
||||
pub device_machine_identification: Option<DeviceMachineIdentification>,
|
||||
pub device_hardware_identification: DeviceHardwareIdentification,
|
||||
}
|
||||
|
||||
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
|
||||
fn from(value: DeviceIdentificationIdentified) -> Self {
|
||||
DeviceIdentification {
|
||||
device_machine_identification: Some(value.device_machine_identification),
|
||||
device_hardware_identification: value.device_hardware_identification,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DeviceIdentificationIdentified {
|
||||
pub device_machine_identification: DeviceMachineIdentification,
|
||||
pub device_hardware_identification: DeviceHardwareIdentification,
|
||||
}
|
||||
|
||||
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
|
||||
let device_machine_identification =
|
||||
value.device_machine_identification.ok_or(anyhow::anyhow!(
|
||||
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
|
||||
module_path!()
|
||||
))?;
|
||||
|
||||
Ok(DeviceIdentificationIdentified {
|
||||
device_machine_identification,
|
||||
device_hardware_identification: value.device_hardware_identification,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum DeviceHardwareIdentification {
|
||||
Ethercat(DeviceHardwareIdentificationEthercat),
|
||||
// UsbDevice...,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DeviceHardwareIdentificationEthercat {
|
||||
pub subdevice_index: usize,
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
use smol::lock::Mutex;
|
||||
|
||||
use crate::machines::{
|
||||
identification::MachineIdentificationUnique,
|
||||
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub serial_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl MachineManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ethercat_machines: HashMap::new(),
|
||||
serial_machines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: usize>(
|
||||
&mut self,
|
||||
device_identifications: &Vec<DeviceIdentification>,
|
||||
machine_registry: &MachineRegistry,
|
||||
hardware: &MachineNewHardwareEthercat,
|
||||
) {
|
||||
// empty ethercat machines
|
||||
self.ethercat_machines.clear();
|
||||
|
||||
// group devices by machine device identification
|
||||
let device_grouping_result = group_devices_by_identification(device_identifications);
|
||||
|
||||
log::info!(
|
||||
"[{}::set_ethercat_devices] Device Groups {:?}",
|
||||
module_path!(),
|
||||
device_grouping_result.device_groups.len()
|
||||
);
|
||||
|
||||
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
|
||||
|
||||
// iterate over all identified device groups but ignore unaffected machines
|
||||
for device_group in device_grouping_result.device_groups.iter() {
|
||||
// get the machine identification
|
||||
let machine_identification = match device_group.first() {
|
||||
Some(device_identification) => &device_identification.device_machine_identification,
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
|
||||
// create the machine
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group,
|
||||
hardware: &machine_new_hardware,
|
||||
});
|
||||
|
||||
// insert the machine into the ethercat machines map
|
||||
self.ethercat_machines.insert(
|
||||
machine_identification.machine_identification_unique.clone(),
|
||||
new_machine,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<&Result<Box<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups devices by machine identification
|
||||
///
|
||||
/// Returns a DeviceGroupingResult containing:
|
||||
/// - device_groups: a vector of devices grouped by machine identification
|
||||
/// - unidentified_devices: a vector of devices that could not be identified
|
||||
pub fn group_devices_by_identification(
|
||||
device_identifications: &Vec<DeviceIdentification>,
|
||||
) -> DeviceGroupingResult {
|
||||
let mut device_groups: Vec<Vec<DeviceIdentificationIdentified>> = Vec::new();
|
||||
let mut unidentified_devices: Vec<DeviceIdentification> = Vec::new();
|
||||
|
||||
for device_identification in device_identifications {
|
||||
// if vendor or serial or machine is 0, it is not a valid machine device
|
||||
if let Some(device_machine_identification) =
|
||||
device_identification.device_machine_identification.as_ref()
|
||||
{
|
||||
if !device_machine_identification.is_valid() {
|
||||
unidentified_devices.push(device_identification.clone());
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
unidentified_devices.push(device_identification.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// scan over all deice groups
|
||||
// get the first DeviceMachineIdentification
|
||||
// compare and append to the group
|
||||
let mut found = false;
|
||||
for check_group in device_groups.iter_mut() {
|
||||
// get first device in group
|
||||
let first_device = check_group.first().expect("group to not be empty");
|
||||
let first_device_machine_identification = &first_device
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
|
||||
// chek if it has machine identification
|
||||
if let Some(device_machine_identification) =
|
||||
device_identification.device_machine_identification.as_ref()
|
||||
{
|
||||
// compare with the current device
|
||||
if first_device_machine_identification
|
||||
== &device_machine_identification.machine_identification_unique
|
||||
{
|
||||
let device_identification_identified = device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("should have Some(DeviceMachineIdentification)");
|
||||
check_group.push(device_identification_identified);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
let device_identification_identified = device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("should have Some(DeviceMachineIdentification)");
|
||||
device_groups.push(vec![device_identification_identified]);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceGroupingResult {
|
||||
device_groups,
|
||||
unidentified_devices,
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure to hold the result of grouping devices by identification
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceGroupingResult {
|
||||
/// Devices grouped by machine identification
|
||||
pub device_groups: Vec<Vec<DeviceIdentificationIdentified>>,
|
||||
/// Devices that could not be identified
|
||||
pub unidentified_devices: Vec<DeviceIdentification>,
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use smol::lock::Mutex;
|
||||
|
||||
use super::{Machine, identification::MachineIdentificationUnique, manager::MachineManager};
|
||||
|
||||
pub enum MachineManagerIter<'a> {
|
||||
EthercatMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines:
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match &mut self.iter {
|
||||
MachineManagerIter::EthercatMachines { iter } => {
|
||||
// Try to get the next item from ethercat_machines
|
||||
if let Some(item) = iter.next() {
|
||||
return Some(item);
|
||||
} else {
|
||||
// If ethercat_machines is exhausted, move to serial_machines
|
||||
self.iter = MachineManagerIter::SerialMachines {
|
||||
iter: self.serial_machines.iter(),
|
||||
};
|
||||
// Continue to the next iteration of the loop
|
||||
continue;
|
||||
}
|
||||
}
|
||||
MachineManagerIter::SerialMachines { iter } => {
|
||||
// Return the next item from serial_machines or None if exhausted
|
||||
if let Some(item) = iter.next() {
|
||||
return Some(item);
|
||||
} else {
|
||||
// If serial_machines is also exhausted, we're done
|
||||
self.iter = MachineManagerIter::Done;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
MachineManagerIter::Done => {
|
||||
// We've exhausted both collections
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineManager {
|
||||
// Returns an iterator over all machines (both ethercat and serial)
|
||||
pub fn iter(&self) -> MachineManagerIterator {
|
||||
MachineManagerIterator {
|
||||
iter: MachineManagerIter::EthercatMachines {
|
||||
iter: self.ethercat_machines.iter(),
|
||||
},
|
||||
serial_machines: &self.serial_machines,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use crate::actors::Actor;
|
||||
use api::MachineApi;
|
||||
use new::MachineNewTrait;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub mod api;
|
||||
pub mod identification;
|
||||
pub mod manager;
|
||||
pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineNewTrait + MachineApi + Actor + Any + Debug {}
|
||||
@@ -1,237 +0,0 @@
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{devices::EthercatDevice, types::EthercrabSubDevicePreoperational};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::lock::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait MachineNewTrait {
|
||||
fn new<'maindevice, 'subdevices>(
|
||||
params: &MachineNewParams<'maindevice, 'subdevices, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub struct MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware,
|
||||
> where
|
||||
'maindevice: 'machine_new_hardware,
|
||||
'subdevices: 'machine_new_hardware,
|
||||
'ethercat_devices: 'machine_new_hardware,
|
||||
'machine_new_hardware_etehrcat: 'machine_new_hardware,
|
||||
{
|
||||
pub device_group: &'device_identifications_identified Vec<DeviceIdentificationIdentified>,
|
||||
pub hardware: &'machine_new_hardware MachineNewHardware<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
>,
|
||||
}
|
||||
|
||||
pub enum MachineNewHardware<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
> where
|
||||
'maindevice: 'machine_new_hardware_etehrcat,
|
||||
'subdevices: 'machine_new_hardware_etehrcat,
|
||||
'ethercat_devices: 'machine_new_hardware_etehrcat,
|
||||
{
|
||||
Ethercat(
|
||||
&'machine_new_hardware_etehrcat MachineNewHardwareEthercat<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
>,
|
||||
),
|
||||
}
|
||||
|
||||
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
|
||||
pub subdevices:
|
||||
&'subdevices Vec<&'subdevices SubDeviceRef<'maindevice, &'subdevices SubDevice>>,
|
||||
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
|
||||
}
|
||||
|
||||
// validates that all devices in the group have the same machine identification
|
||||
pub fn validate_same_machine_identification_unique(
|
||||
identified_device_group: &Vec<DeviceIdentificationIdentified>,
|
||||
) -> Result<(), Error> {
|
||||
let machine_identification_unique = &identified_device_group
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"[{}::validate_same_machine_identification] No devices in group",
|
||||
module_path!()
|
||||
)
|
||||
})?
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
for device in identified_device_group.iter() {
|
||||
if device
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
!= *machine_identification_unique
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::validate_same_machine_identification] Different machine identifications",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// validates that every role is unique
|
||||
pub fn validate_no_role_dublicates(
|
||||
identified_device_group: &Vec<DeviceIdentificationIdentified>,
|
||||
) -> Result<(), Error> {
|
||||
let mut roles = vec![];
|
||||
for device in identified_device_group.iter() {
|
||||
if roles.contains(&device.device_machine_identification.role) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::validate_no_role_dublicates] Role dublicate",
|
||||
module_path!(),
|
||||
));
|
||||
}
|
||||
roles.push(device.device_machine_identification.role);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Inside control_core::machines::new module:
|
||||
pub fn get_device_identification_by_role(
|
||||
identified_device_group: &Vec<DeviceIdentificationIdentified>,
|
||||
role: u16,
|
||||
) -> Result<&DeviceIdentificationIdentified, Error> {
|
||||
for device in identified_device_group.iter() {
|
||||
if device.device_machine_identification.role == role {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"[{}::get_device_identification_by_role] Role {} not found",
|
||||
module_path!(),
|
||||
role
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_device_by_index<'maindevice>(
|
||||
devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
|
||||
subdevice_index: usize,
|
||||
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
|
||||
Ok(devices
|
||||
.get(subdevice_index)
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::get_device_by_index] Index {} out of bounds for devices",
|
||||
module_path!(),
|
||||
subdevice_index
|
||||
))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub fn get_subdevice_by_index<'subdevices, 'maindevice>(
|
||||
subdevices: &'subdevices Vec<&EthercrabSubDevicePreoperational<'maindevice>>,
|
||||
subdevice_index: usize,
|
||||
) -> Result<&'subdevices EthercrabSubDevicePreoperational<'maindevice>, Error> {
|
||||
Ok(subdevices.get(subdevice_index).ok_or(anyhow::anyhow!(
|
||||
"Index {} out of bounds for subdevices",
|
||||
subdevice_index
|
||||
))?)
|
||||
}
|
||||
|
||||
pub fn get_ethercat_device_by_index<'maindevice>(
|
||||
ethercat_devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
|
||||
subdevice_index: usize,
|
||||
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
|
||||
Ok(ethercat_devices
|
||||
.get(subdevice_index)
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::get_ethercat_device_by_index] Index {} out of bounds for ethercat devices",
|
||||
module_path!(),
|
||||
subdevice_index
|
||||
))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::machines::identification::{
|
||||
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat,
|
||||
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
|
||||
};
|
||||
|
||||
pub use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_device_identification_by_role() {
|
||||
let device_identifications = vec![
|
||||
// role 0
|
||||
DeviceIdentificationIdentified {
|
||||
device_machine_identification: DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: 1,
|
||||
machine: 1,
|
||||
},
|
||||
serial: 1,
|
||||
},
|
||||
role: 0,
|
||||
},
|
||||
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
|
||||
DeviceHardwareIdentificationEthercat { subdevice_index: 0 },
|
||||
),
|
||||
},
|
||||
// role 1
|
||||
DeviceIdentificationIdentified {
|
||||
device_machine_identification: DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: 2,
|
||||
machine: 2,
|
||||
},
|
||||
serial: 2,
|
||||
},
|
||||
role: 2,
|
||||
},
|
||||
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
|
||||
DeviceHardwareIdentificationEthercat { subdevice_index: 1 },
|
||||
),
|
||||
},
|
||||
// role 2
|
||||
DeviceIdentificationIdentified {
|
||||
device_machine_identification: DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: 3,
|
||||
machine: 3,
|
||||
},
|
||||
serial: 3,
|
||||
},
|
||||
role: 1,
|
||||
},
|
||||
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
|
||||
DeviceHardwareIdentificationEthercat { subdevice_index: 2 },
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// search for role 0
|
||||
let result = get_device_identification_by_role(&device_identifications, 0);
|
||||
assert_eq!(result.unwrap().device_machine_identification.role, 0,);
|
||||
|
||||
// search for role 1
|
||||
let result = get_device_identification_by_role(&device_identifications, 1);
|
||||
assert_eq!(result.unwrap().device_machine_identification.role, 1,);
|
||||
// search for role 2
|
||||
let result = get_device_identification_by_role(&device_identifications, 2);
|
||||
assert_eq!(result.unwrap().device_machine_identification.role, 2,);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
|
||||
use anyhow::Error;
|
||||
use smol::lock::{Mutex, RwLock};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub type MachineNewClosure =
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Box<Mutex<dyn Machine>>, Error> + Send + Sync>;
|
||||
|
||||
pub struct MachineRegistry {
|
||||
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
|
||||
}
|
||||
|
||||
impl MachineRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
type_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<T: Machine + 'static>(
|
||||
&mut self,
|
||||
machine_identficiation: MachineIdentification,
|
||||
) {
|
||||
self.type_map.insert(
|
||||
TypeId::of::<T>(),
|
||||
(
|
||||
machine_identficiation,
|
||||
// create a machine construction closure
|
||||
Box::new(|machine_new_params| {
|
||||
Ok(Box::new(Mutex::new(T::new(machine_new_params)?)))
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn new_machine(
|
||||
&self,
|
||||
machine_new_params: &MachineNewParams,
|
||||
) -> Result<Box<Mutex<dyn Machine>>, anyhow::Error> {
|
||||
// get machiine identification
|
||||
let device_identification =
|
||||
&machine_new_params
|
||||
.device_group
|
||||
.first()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::MachineConstructor::new_machine] No device in group",
|
||||
module_path!()
|
||||
))?;
|
||||
|
||||
// find machine new function by comparing MachineIdentification
|
||||
let (_, machine_new_closure) = self
|
||||
.type_map
|
||||
.values()
|
||||
.find(|(mi, _)| {
|
||||
mi == &device_identification
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
})
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::MachineConstructor::new_machine] Machine not found",
|
||||
module_path!()
|
||||
))?;
|
||||
|
||||
// call machine new function by reference
|
||||
(machine_new_closure)(machine_new_params)
|
||||
}
|
||||
|
||||
pub fn downcast_arc_rwlock<T: Machine + 'static>(
|
||||
&self,
|
||||
machine: Arc<RwLock<dyn Machine>>,
|
||||
) -> Result<Arc<RwLock<T>>, Error> {
|
||||
if TypeId::of::<T>() == machine.type_id() {
|
||||
// transmute Arc
|
||||
let arc = unsafe { Arc::from_raw(Arc::into_raw(machine) as *const RwLock<T>) };
|
||||
Ok(arc)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"[{}::MachineConstructor::downcast] Machine is not of type {}",
|
||||
module_path!(),
|
||||
std::any::type_name::<T>()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn downcast_box_rwlock<T: Machine + 'static>(
|
||||
&self,
|
||||
machine: Box<RwLock<dyn Machine>>,
|
||||
) -> Result<RwLock<T>, Error> {
|
||||
if TypeId::of::<T>() == machine.type_id() {
|
||||
// transmute Box
|
||||
let box_machine = unsafe { Box::from_raw(Box::into_raw(machine) as *mut RwLock<T>) };
|
||||
Ok(*box_machine)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"[{}::MachineConstructor::downcast] Machine is not of type {}",
|
||||
module_path!(),
|
||||
std::any::type_name::<T>()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
-50
@@ -1,6 +1,9 @@
|
||||
use std::time::Duration;
|
||||
pub mod modbus_serial_interface;
|
||||
|
||||
use anyhow::Error;
|
||||
use crc::{CRC_16_MODBUS, Crc};
|
||||
use serialport::SerialPort;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParityType {
|
||||
@@ -14,12 +17,66 @@ pub enum ParityType {
|
||||
pub enum ModbusFunctionCode {
|
||||
/// Read one or more Registers
|
||||
ReadHoldingRegister,
|
||||
/// Read Input register
|
||||
ReadInputRegister,
|
||||
/// write one Register Value
|
||||
PresetHoldingRegister,
|
||||
/// The response should echo back your request
|
||||
DiagnoseFunction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum ModbusExceptionCode {
|
||||
None = 0,
|
||||
IllegalFunction = 1,
|
||||
IllegalDataAddress = 2,
|
||||
IllegalDataValue = 3,
|
||||
SlaveDeviceFailure = 4,
|
||||
Acknowledge = 5,
|
||||
SlaveDeviceBusy = 6,
|
||||
MemoryParityError = 8,
|
||||
GatewayPathUnavailable = 10,
|
||||
GatewayTargetDeviceFailedToRespond = 11,
|
||||
Unknown(u8), // For any unknown exception codes
|
||||
}
|
||||
|
||||
impl From<ModbusExceptionCode> for u8 {
|
||||
fn from(code: ModbusExceptionCode) -> Self {
|
||||
match code {
|
||||
ModbusExceptionCode::None => 0,
|
||||
ModbusExceptionCode::IllegalFunction => 1,
|
||||
ModbusExceptionCode::IllegalDataAddress => 2,
|
||||
ModbusExceptionCode::IllegalDataValue => 3,
|
||||
ModbusExceptionCode::SlaveDeviceFailure => 4,
|
||||
ModbusExceptionCode::Acknowledge => 5,
|
||||
ModbusExceptionCode::SlaveDeviceBusy => 6,
|
||||
ModbusExceptionCode::MemoryParityError => 8,
|
||||
ModbusExceptionCode::GatewayPathUnavailable => 10,
|
||||
ModbusExceptionCode::GatewayTargetDeviceFailedToRespond => 11,
|
||||
ModbusExceptionCode::Unknown(code) => code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for ModbusExceptionCode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => Self::None,
|
||||
1 => Self::IllegalFunction,
|
||||
2 => Self::IllegalDataAddress,
|
||||
3 => Self::IllegalDataValue,
|
||||
4 => Self::SlaveDeviceFailure,
|
||||
5 => Self::Acknowledge,
|
||||
6 => Self::SlaveDeviceBusy,
|
||||
8 => Self::MemoryParityError,
|
||||
10 => Self::GatewayPathUnavailable,
|
||||
11 => Self::GatewayTargetDeviceFailedToRespond,
|
||||
other => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ModbusRequest {
|
||||
pub slave_id: u8,
|
||||
@@ -35,32 +92,15 @@ pub struct ModbusResponse {
|
||||
pub crc: u16,
|
||||
}
|
||||
|
||||
impl ModbusResponse {
|
||||
fn is_exception(self) -> bool {
|
||||
if self.data.len() > 2 {
|
||||
let function_code = self.data[1];
|
||||
return (function_code & 0b10000000) > 0; // first bit is set when an exception happens in Modbus
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn get_exception_code(self) -> u8 {
|
||||
if self.data.len() > 3 {
|
||||
let exception = self.data[2];
|
||||
return exception;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ModbusFunctionCode {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x03 => Ok(ModbusFunctionCode::ReadHoldingRegister),
|
||||
0x06 => Ok(ModbusFunctionCode::PresetHoldingRegister),
|
||||
0x08 => Ok(ModbusFunctionCode::DiagnoseFunction),
|
||||
0x03 => Ok(Self::ReadHoldingRegister),
|
||||
0x04 => Ok(Self::ReadInputRegister),
|
||||
0x06 => Ok(Self::PresetHoldingRegister),
|
||||
0x08 => Ok(Self::DiagnoseFunction),
|
||||
_ => Err(anyhow::anyhow!("Error: Modbus Function Code doesnt exist!")),
|
||||
}
|
||||
}
|
||||
@@ -70,6 +110,7 @@ impl From<ModbusFunctionCode> for u8 {
|
||||
fn from(value: ModbusFunctionCode) -> Self {
|
||||
match value {
|
||||
ModbusFunctionCode::ReadHoldingRegister => 0x03,
|
||||
ModbusFunctionCode::ReadInputRegister => 0x04,
|
||||
ModbusFunctionCode::PresetHoldingRegister => 0x06,
|
||||
ModbusFunctionCode::DiagnoseFunction => 0x08,
|
||||
}
|
||||
@@ -78,7 +119,7 @@ impl From<ModbusFunctionCode> for u8 {
|
||||
|
||||
impl From<ModbusRequest> for Vec<u8> {
|
||||
fn from(request: ModbusRequest) -> Self {
|
||||
let mut buffer = Vec::new();
|
||||
let mut buffer = Self::new();
|
||||
|
||||
buffer.push(request.slave_id);
|
||||
buffer.push(request.function_code.into()); // convert functioncode into u8 and push it
|
||||
@@ -91,63 +132,89 @@ impl From<ModbusRequest> for Vec<u8> {
|
||||
// convert u16 value to le_bytes and add it to the end of the frame
|
||||
buffer.extend_from_slice(&result_crc.to_le_bytes());
|
||||
|
||||
return buffer;
|
||||
buffer
|
||||
}
|
||||
}
|
||||
/// Reads data from a Modbus device via a serial port.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `port`: A mutable reference to a serial port.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result` containing an `Option` with the raw Modbus response as a `Vec<u8>` if successful,
|
||||
/// or `None` if the response is invalid or an error occurs.
|
||||
pub fn receive_data_modbus(port: &mut dyn SerialPort) -> Result<Option<Vec<u8>>, anyhow::Error> {
|
||||
let mut buf: [u8; 256] = [0; 256];
|
||||
let data_length = port.read(&mut buf)?;
|
||||
if data_length == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
validate_modbus_response(buf[..data_length].to_vec()).map(Some)
|
||||
}
|
||||
|
||||
fn validate_modbus_response(raw_data: Vec<u8>) -> Result<(), Error> {
|
||||
if raw_data.len() == 0 {
|
||||
/// Computes Modbus CRC-16 using `crc` crate.
|
||||
pub const fn modbus_crc16(data: &[u8]) -> u16 {
|
||||
let modbus: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS);
|
||||
modbus.checksum(data)
|
||||
}
|
||||
|
||||
/// Checks if the message's CRC is correct (uses Modbus CRC-16).
|
||||
fn check_crc(raw_data: &Vec<u8>) -> Result<bool, Error> {
|
||||
let expected_crc = extract_crc(raw_data)?;
|
||||
let actual_crc = modbus_crc16(&raw_data[..raw_data.len() - 2]);
|
||||
Ok(expected_crc == actual_crc)
|
||||
}
|
||||
|
||||
fn validate_modbus_response(raw_data: Vec<u8>) -> Result<Vec<u8>, Error> {
|
||||
if raw_data.is_empty() {
|
||||
return Err(anyhow::anyhow!("Error: Response is Empty!"));
|
||||
}
|
||||
|
||||
// 5 is the smallest possible Response Size
|
||||
if raw_data.len() < 5 {
|
||||
println!("raw_data.len() < 5");
|
||||
if raw_data.len() <= 5 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error: Response is invalid, its less than 5 bytes"
|
||||
));
|
||||
}
|
||||
|
||||
// check if the slave id is valid
|
||||
if raw_data[0] < 1 || raw_data[0] > 247 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error: Response is invalid, slave_id is outside of the valid range 1-247"
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
if check_crc(&raw_data)? {
|
||||
Ok(raw_data)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Error: CRC does not match"))
|
||||
}
|
||||
}
|
||||
|
||||
// expects to be given the entire raw message
|
||||
fn extract_crc(raw_data: &Vec<u8>) -> Result<u16, Error> {
|
||||
let raw_data1 = raw_data.clone();
|
||||
let result = validate_modbus_response(raw_data1);
|
||||
if let Err(error) = result {
|
||||
return Err(error);
|
||||
if raw_data.len() < 5 {
|
||||
return Err(anyhow::anyhow!("Not enough data to extract CRC"));
|
||||
}
|
||||
|
||||
let low_byte = raw_data[raw_data.len() - 2];
|
||||
let high_byte = raw_data[raw_data.len() - 1];
|
||||
|
||||
let crc = u16::from_le_bytes([low_byte, high_byte]);
|
||||
Ok(crc)
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for ModbusResponse {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Vec<u8>) -> Result<ModbusResponse, Error> {
|
||||
let crc = match extract_crc(&value) {
|
||||
Ok(crc_value) => crc_value,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Error> {
|
||||
let crc = extract_crc(&value)?;
|
||||
|
||||
let function_code_res = ModbusFunctionCode::try_from(value[1]);
|
||||
let function_code = match function_code_res {
|
||||
Ok(code) => code,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let function_code = function_code_res?;
|
||||
|
||||
Ok(ModbusResponse {
|
||||
Ok(Self {
|
||||
slave_id: value[0],
|
||||
function_code: function_code,
|
||||
function_code,
|
||||
data: value[2..value.len() - 2].to_vec(), // get data without the crc
|
||||
crc: crc,
|
||||
crc,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -159,23 +226,23 @@ impl TryFrom<Vec<u8>> for ModbusResponse {
|
||||
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
|
||||
/// baudrate: bits per second
|
||||
/// message_size: size of original message in bytes
|
||||
pub fn calculate_modbus_rtu_timeout(
|
||||
pub const fn calculate_modbus_rtu_timeout(
|
||||
bits: u8,
|
||||
machine_operation_delay_nano: Duration,
|
||||
baudrate: u32,
|
||||
message_size: usize,
|
||||
) -> Duration {
|
||||
let nanoseconds_per_bit: u64 = (1000000 / baudrate) as u64;
|
||||
let nanoseconds_per_byte: u64 = bits as u64 * nanoseconds_per_bit as u64;
|
||||
let nanoseconds_per_byte: u64 = bits as u64 * nanoseconds_per_bit;
|
||||
|
||||
let transmission_timeout: u64 = nanoseconds_per_byte * message_size as u64;
|
||||
let silent_time: u64 = (nanoseconds_per_byte * (35)) / 10 as u64; // silent_time is 3.5x of character length,which is 11 bit for 8E1
|
||||
let silent_time: u64 = (nanoseconds_per_byte * (35)) / 10_u64; // silent_time is 3.5x of character length,which is 11 bit for 8E1
|
||||
|
||||
let mut full_timeout: u64 = transmission_timeout;
|
||||
full_timeout += machine_operation_delay_nano.as_nanos() as u64;
|
||||
full_timeout += silent_time;
|
||||
|
||||
return Duration::from_nanos(full_timeout);
|
||||
Duration::from_nanos(full_timeout)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
use crate::modbus::{ModbusRequest, ModbusResponse};
|
||||
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RequestMetaData {
|
||||
priority: u32,
|
||||
ignored_times: u32,
|
||||
/// This is used when a machine for example takes 4ms to process the request
|
||||
/// and is added ontop of the standard waiting time for a serial transfer
|
||||
extra_delay: Option<u32>,
|
||||
no_response_expected: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
/// WaitingForResponse is set after sending a request through the serial_interface
|
||||
WaitingForResponse,
|
||||
/// After Sending a Request we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has transmit accepted toggled
|
||||
/// Then we can set state = ReadyToSend
|
||||
WaitingForRequestAccept,
|
||||
/// After Receiving a Response we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has received accepted toggled
|
||||
WaitingForReceiveAccept,
|
||||
/// ReadyToSend is set after receiving the response from the serial_interface
|
||||
ReadyToSend,
|
||||
/// Initial State
|
||||
Uninitialized,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModbusSerialInterface {
|
||||
serial_interface: SerialInterface,
|
||||
baudrate: Option<u32>,
|
||||
encoding: Option<SerialEncoding>,
|
||||
response: Option<ModbusResponse>,
|
||||
state: State,
|
||||
last_message_size: usize,
|
||||
last_message_delay: u32,
|
||||
pub last_message_id: u32,
|
||||
no_response_expected: bool,
|
||||
request_map: HashMap<u32, ModbusRequest>,
|
||||
request_metadata_map: HashMap<u32, RequestMetaData>,
|
||||
last_ts: Instant,
|
||||
}
|
||||
|
||||
impl ModbusSerialInterface {
|
||||
pub fn new(serial_interface: SerialInterface) -> Self {
|
||||
Self {
|
||||
serial_interface,
|
||||
baudrate: None,
|
||||
encoding: None,
|
||||
request_map: HashMap::new(),
|
||||
request_metadata_map: HashMap::new(),
|
||||
response: None,
|
||||
last_message_size: 0,
|
||||
last_message_delay: 0,
|
||||
state: State::Uninitialized,
|
||||
last_ts: Instant::now(),
|
||||
last_message_id: 0,
|
||||
no_response_expected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_initialized(&self) -> bool {
|
||||
!matches!(self.state, State::Uninitialized)
|
||||
}
|
||||
|
||||
pub fn add_request(
|
||||
&mut self,
|
||||
request_id: u32,
|
||||
priority: u32,
|
||||
request: ModbusRequest,
|
||||
no_response_expected: bool,
|
||||
delay: Option<u32>,
|
||||
) {
|
||||
self.request_map.insert(request_id, request);
|
||||
self.request_metadata_map
|
||||
.entry(request_id)
|
||||
.or_insert_with(|| RequestMetaData {
|
||||
priority,
|
||||
ignored_times: 0,
|
||||
extra_delay: delay,
|
||||
no_response_expected,
|
||||
});
|
||||
}
|
||||
|
||||
/// This is used internally to read the receive buffer of the el6021
|
||||
async fn read_modbus_response(&mut self) -> Result<ModbusResponse, anyhow::Error> {
|
||||
let raw_response = (self.serial_interface.read_message)()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let response = ModbusResponse::try_from(raw_response)?;
|
||||
self.last_message_size = response.data.len() + 4;
|
||||
self.state = State::WaitingForReceiveAccept;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// This is used internally to fill the write buffer of the el6021 with the modbus request
|
||||
/// Decides what requests to send first by finding the one with the highest priority
|
||||
/// For example Highest Priority requests: ResetInverter StopMotor
|
||||
async fn send_modbus_request(&mut self) {
|
||||
if self.request_map.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (highest_id, delay, no_response_expected) = self
|
||||
.request_metadata_map
|
||||
.iter_mut()
|
||||
.map(|(key, value)| {
|
||||
let effective_priority = value.priority + value.ignored_times;
|
||||
(
|
||||
*key,
|
||||
effective_priority,
|
||||
value.extra_delay.unwrap_or(0),
|
||||
value.no_response_expected,
|
||||
)
|
||||
})
|
||||
.max_by_key(|(_, priority, _, _)| *priority)
|
||||
.map(|(key, _, delay, no_response)| (key, delay, no_response))
|
||||
.unwrap(); // Safe because we checked is_empty() above
|
||||
|
||||
let request = &self.request_map[&highest_id];
|
||||
let modbus_request: Vec<u8> = request.clone().into();
|
||||
|
||||
if let Err(_) = (self.serial_interface.write_message)(modbus_request.clone()).await {
|
||||
tracing::error!("ERROR: serial_interface.write_message has failed");
|
||||
} else {
|
||||
self.last_message_delay = delay;
|
||||
self.last_message_id = highest_id;
|
||||
self.no_response_expected = no_response_expected;
|
||||
}
|
||||
|
||||
self.state = State::WaitingForRequestAccept;
|
||||
self.last_message_size = modbus_request.len();
|
||||
}
|
||||
|
||||
pub async fn initialize_communication_settings(&mut self) -> bool {
|
||||
let baudrate = (self.serial_interface.get_baudrate)().await;
|
||||
let encoding = (self.serial_interface.get_serial_encoding)().await;
|
||||
|
||||
match (baudrate, encoding) {
|
||||
(Some(b), Some(e)) => {
|
||||
self.baudrate = Some(b);
|
||||
self.encoding = Some(e);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn initialize(&mut self) -> bool {
|
||||
if matches!(self.state, State::Uninitialized) {
|
||||
let success = (self.serial_interface.initialize)().await;
|
||||
if success {
|
||||
self.state = State::ReadyToSend;
|
||||
self.initialize_communication_settings().await;
|
||||
}
|
||||
success
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_ignored_times(&mut self) {
|
||||
for metadata in self.request_metadata_map.values_mut() {
|
||||
metadata.ignored_times += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Modbus RTU has silent time between frames that needs to be adhered to, if you send before silent_time is over between frames, then there will be lost frames
|
||||
/// This silent time is needed to identify the start and end of messages
|
||||
/// This function also takes into account the time that the slave we are talking to needs to process our request
|
||||
/// bits: amount of bits sent per byte -> for a 8n1 coding: 8 data bits, 0 parity, 1 stop bit (1 start,1 stop) -> 10 bits
|
||||
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
|
||||
/// baudrate: bits per second
|
||||
/// message_size: size of original message in bytes
|
||||
pub fn calculate_modbus_rtu_timeout(
|
||||
&self,
|
||||
machine_operation_delay: Duration,
|
||||
message_size: usize,
|
||||
) -> Option<Duration> {
|
||||
let baudrate = self.baudrate?;
|
||||
let encoding = self.encoding?;
|
||||
|
||||
let nanoseconds_per_bit = 1_000_000_000 / baudrate as u64;
|
||||
let nanoseconds_per_byte = encoding.total_bits() as u64 * nanoseconds_per_bit;
|
||||
|
||||
let transmission_timeout = nanoseconds_per_byte * message_size as u64;
|
||||
let silent_time = (nanoseconds_per_byte * 35) / 10; // silent_time is 3.5x of character length
|
||||
|
||||
let total_timeout =
|
||||
transmission_timeout + machine_operation_delay.as_nanos() as u64 + silent_time;
|
||||
Some(Duration::from_nanos(total_timeout))
|
||||
}
|
||||
|
||||
pub const fn get_response(&self) -> Option<&ModbusResponse> {
|
||||
self.response.as_ref()
|
||||
}
|
||||
|
||||
pub async fn act(&mut self, now_ts: Instant) {
|
||||
let elapsed = now_ts.duration_since(self.last_ts);
|
||||
|
||||
let timeout = self.calculate_modbus_rtu_timeout(
|
||||
Duration::from_nanos(self.last_message_delay as u64),
|
||||
self.last_message_size,
|
||||
);
|
||||
|
||||
let Some(timeout) = timeout else { return };
|
||||
|
||||
// Check if we need to wait for timeout (except for specific states)
|
||||
if !matches!(
|
||||
self.state,
|
||||
State::Uninitialized | State::WaitingForReceiveAccept | State::WaitingForRequestAccept
|
||||
) && elapsed < timeout
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_ts = now_ts;
|
||||
self.response = None;
|
||||
|
||||
match self.state {
|
||||
State::WaitingForResponse => match self.read_modbus_response().await {
|
||||
Ok(response) => {
|
||||
self.response = Some(response);
|
||||
}
|
||||
Err(_) => {
|
||||
self.response = None;
|
||||
if self.no_response_expected {
|
||||
self.state = State::ReadyToSend;
|
||||
}
|
||||
}
|
||||
},
|
||||
State::ReadyToSend => {
|
||||
self.send_modbus_request().await;
|
||||
|
||||
// Remove the sent request
|
||||
self.request_map.remove(&self.last_message_id);
|
||||
self.request_metadata_map.remove(&self.last_message_id);
|
||||
self.increment_ignored_times();
|
||||
}
|
||||
State::WaitingForReceiveAccept => {
|
||||
// Waste at least one Ethercat Cycle here to ensure that request/response stay in sync
|
||||
self.state = State::ReadyToSend;
|
||||
}
|
||||
State::WaitingForRequestAccept => {
|
||||
// An empty vec is used to check if we are finished with writing the message
|
||||
// This is to keep the Serialinterface more simple
|
||||
match (self.serial_interface.write_message)(vec![]).await {
|
||||
Ok(finished) if finished => {
|
||||
self.state = State::WaitingForResponse;
|
||||
}
|
||||
Err(_) => {
|
||||
self.state = State::ReadyToSend;
|
||||
}
|
||||
_ => {} // Still waiting
|
||||
}
|
||||
}
|
||||
State::Uninitialized => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Makes the current thread a real-time thread using the PREEMPT_RT capabilities of Linux.
|
||||
///
|
||||
/// This function configures the current thread with real-time scheduling properties by:
|
||||
///
|
||||
/// 1. Setting the scheduling policy to SCHED_FIFO (First-In-First-Out), which is a real-time
|
||||
/// scheduling policy that allows the thread to run until it voluntarily yields, blocks on I/O,
|
||||
/// or is preempted by a higher-priority real-time thread.
|
||||
///
|
||||
/// 2. Setting the real-time priority to 80 (on a scale of 1-99, where higher values indicate
|
||||
/// higher priority). This priority level is chosen to be higher than most IRQ handlers
|
||||
/// (typically priority 50) but lower than critical kernel tasks (priority 99).
|
||||
///
|
||||
/// When running on a PREEMPT_RT patched Linux kernel, this combination provides deterministic
|
||||
/// scheduling with minimal latency, which is essential for real-time applications.
|
||||
///
|
||||
/// # Requirements
|
||||
///
|
||||
/// - The program must run with appropriate permissions to set real-time priorities.
|
||||
/// This typically means either:
|
||||
/// - Running as root
|
||||
/// - Configuring `/etc/security/limits.conf` to allow the user to set real-time priorities
|
||||
///
|
||||
/// - The system should be running a Linux kernel with PREEMPT_RT patch applied for
|
||||
/// optimal real-time performance.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() {
|
||||
/// // Lock memory to prevent page faults
|
||||
/// lock_memory();
|
||||
///
|
||||
/// // Create a thread and make it real-time
|
||||
/// std::thread::spawn(|| {
|
||||
/// set_realtime_priority();
|
||||
///
|
||||
/// // Real-time code here
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(())` if the real-time priority was successfully set
|
||||
/// - `Err` with the error message if setting the priority failed
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
use anyhow::anyhow;
|
||||
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
|
||||
use std::mem;
|
||||
|
||||
unsafe {
|
||||
// Get the pthread_t of the current thread using pthread_self()
|
||||
let pthread_self = libc::pthread_self();
|
||||
|
||||
// Set up the scheduling parameters
|
||||
let mut param: sched_param = mem::zeroed();
|
||||
param.sched_priority = 95;
|
||||
|
||||
// Set the thread to use SCHED_FIFO scheduling policy with our priority
|
||||
let result = pthread_setschedparam(pthread_self, SCHED_FIFO, ¶m);
|
||||
|
||||
if result != 0 {
|
||||
error!(
|
||||
"Failed to set real-time scheduling policy for thread \"{:?}\": {}",
|
||||
std::thread::current().name().unwrap_or("unknown"),
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return Err(anyhow!(
|
||||
"Failed to set real-time priority: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
} else {
|
||||
debug!(
|
||||
"Successfully set real-time priority for the thread \"{:?}\"",
|
||||
std::thread::current().name().unwrap_or("unknown")
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
tracing::error!("This platform does not support realtime threads");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locks all current and future memory pages of the process into RAM to prevent page faults.
|
||||
///
|
||||
/// # When to call this function
|
||||
///
|
||||
/// This function should be called from the main thread, as early as possible in your program's
|
||||
/// execution, ideally before creating any threads and before allocating large amounts of memory.
|
||||
///
|
||||
/// Important: You only need to call this function ONCE per process, not in each thread.
|
||||
/// The memory locking applies to the entire process, including:
|
||||
/// - The main thread
|
||||
/// - All threads created after the call
|
||||
///
|
||||
/// This happens because all threads in a process share the same virtual memory space, and
|
||||
/// `mlockall()` operates on the entire process's memory space, not just the calling thread.
|
||||
///
|
||||
/// # Example: Correct usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// // Call early in main, before creating threads
|
||||
/// lock_memory()?;
|
||||
///
|
||||
/// // Now create real-time threads...
|
||||
/// std::thread::spawn(|| {
|
||||
/// set_realtime_priority();
|
||||
/// // Real-time code here
|
||||
/// });
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Example: Incorrect usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() {
|
||||
/// // Don't do this - calling in each thread is unnecessary
|
||||
/// std::thread::spawn(|| {
|
||||
/// lock_memory().unwrap(); // Redundant if already called in main
|
||||
/// set_realtime_priority();
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # How it works
|
||||
///
|
||||
/// [Rest of documentation as before...]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use libc::{MCL_FUTURE, mlockall};
|
||||
|
||||
unsafe {
|
||||
if mlockall(MCL_FUTURE) != 0 {
|
||||
error!("Failed to lock memory: {}", std::io::Error::last_os_error());
|
||||
return Err(
|
||||
format!("Failed to lock memory: {}", std::io::Error::last_os_error()).into(),
|
||||
);
|
||||
} else {
|
||||
debug!("Successfully locked memory for the process");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::error!("This platform does not support memory locking");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_core_affinity(core_id: usize) -> Result<(), anyhow::Error> {
|
||||
let cores = core_affinity::get_core_ids().unwrap_or_default();
|
||||
let core = cores.get(core_id);
|
||||
if let Some(core) = core {
|
||||
core_affinity::set_for_current(*core);
|
||||
debug!(
|
||||
"Set core affinity of thread \"{:?}\" to core {:?}",
|
||||
std::thread::current().name().unwrap_or("unknown"),
|
||||
core
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
error!("No cores available to set affinity");
|
||||
Err(anyhow::anyhow!("No cores available to set affinity"))
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod mutation;
|
||||
@@ -1,35 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct MutationResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl MutationResponse {
|
||||
pub fn success() -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
pub fn error(error: String) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct MachineMutationBody<T>
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub data: T,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*use std::{any::Any, sync::Arc};
|
||||
|
||||
use smol::lock::RwLock;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::machines::identification::DeviceIdentification;
|
||||
|
||||
pub mod serial_detection;
|
||||
|
||||
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
|
||||
|
||||
pub trait SerialDeviceNew {
|
||||
fn new_serial(
|
||||
params: &SerialDeviceNewParams,
|
||||
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait SerialDeviceThread {
|
||||
fn start_thread() -> Result<(), anyhow::Error>;
|
||||
}
|
||||
|
||||
pub struct SerialDeviceNewParams {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct SerialDeviceIdentification {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
*@author: Alisher Darmenov
|
||||
*@company: QiTech
|
||||
*@created: 20.04.2025
|
||||
*
|
||||
*@description: This module is responsible for usb detection and validation, specially made with serialport to avoid complexity and size of tokio_serial
|
||||
*/
|
||||
|
||||
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::SerialDeviceIdentification;
|
||||
/// Handles scanning and caching of connected USB serial devices.
|
||||
pub struct SerialDetection {}
|
||||
|
||||
impl SerialDetection {
|
||||
/// Returns all serial ports on the system (minus macOS "cu" noise).
|
||||
fn get_ports() -> Vec<SerialPortInfo> {
|
||||
match serialport::available_ports() {
|
||||
Ok(ports) => ports
|
||||
.into_iter()
|
||||
.filter(|port| !port.port_name.contains("cu"))
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts only USB serial devices from the port list.
|
||||
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for port in port_list {
|
||||
if let SerialPortType::UsbPort(usb_info) = port.port_type {
|
||||
map.insert(port.port_name, usb_info);
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Scans for connected USB serial devices and updates the internal map.
|
||||
/// Returns a clone of the map for quick access.
|
||||
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
|
||||
let ports = Self::get_ports();
|
||||
let usb_devices = Self::extract_usb_serial_devices(ports);
|
||||
usb_devices
|
||||
}
|
||||
|
||||
pub fn device_port_exists(device_path: &str) -> bool {
|
||||
let ports = Self::get_ports();
|
||||
return ports.iter().any(|p| p.port_name == device_path);
|
||||
}
|
||||
|
||||
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
|
||||
let ports = Self::get_ports();
|
||||
for port in ports {
|
||||
if let SerialPortType::UsbPort(usb_info) = port.port_type {
|
||||
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn get_path_by_id(
|
||||
sdevid: SerialDeviceIdentification,
|
||||
map: HashMap<String, UsbPortInfo>,
|
||||
) -> Option<String> {
|
||||
for (port, usbport) in map.iter() {
|
||||
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
|
||||
return Some(port.clone());
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,34 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use erased_serde::Serialize as ErasedSerialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Serialize)]
|
||||
pub struct GenericEvent {
|
||||
pub name: String,
|
||||
pub data: Value,
|
||||
pub data: Box<dyn ErasedSerialize + Send + Sync>,
|
||||
/// Timestamp in milliseconds
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GenericEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GenericEvent")
|
||||
.field("name", &self.name)
|
||||
.field("data", &"[erased]")
|
||||
.field("ts", &self.ts)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BuildEvent: Serialize + Send + Sync + Clone {
|
||||
fn build(&self) -> Event<Self>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Event<T>
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Send + Sync + 'static,
|
||||
{
|
||||
pub name: String,
|
||||
pub data: T,
|
||||
@@ -22,39 +36,35 @@ where
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
impl<T> TryFrom<Event<T>> for GenericEvent
|
||||
impl<T> From<Event<T>> for GenericEvent
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Send + Sync + 'static,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(event: Event<T>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
fn from(event: Event<T>) -> Self {
|
||||
Self {
|
||||
name: event.name,
|
||||
data: serde_json::to_value(event.data)?,
|
||||
data: Box::new(event.data),
|
||||
ts: event.ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryFrom<&Event<T>> for GenericEvent
|
||||
impl<T> From<&Event<T>> for GenericEvent
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(event: &Event<T>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
fn from(event: &Event<T>) -> Self {
|
||||
Self {
|
||||
name: event.name.clone(),
|
||||
data: serde_json::to_value(&event.data)?,
|
||||
data: Box::new(event.data.clone()),
|
||||
ts: event.ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Event<T>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
T: Serialize + Clone + Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(event: &str, data: T) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod event;
|
||||
pub mod namespace;
|
||||
pub mod namespace_id;
|
||||
@@ -1,25 +1,64 @@
|
||||
use crate::socketio::event::GenericEvent;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{
|
||||
alloc::System,
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
pub trait NamespaceInterface {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Namespace {
|
||||
pub sockets: Vec<SocketRef>,
|
||||
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
sockets: vec![],
|
||||
events: HashMap::new(),
|
||||
socket_queue_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
/// Adds a socket to the namespace.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - A reference to the socket to be added
|
||||
fn subscribe(&mut self, socket: SocketRef);
|
||||
#[instrument(skip_all)]
|
||||
pub fn subscribe(&mut self, socket: SocketRef) {
|
||||
// add the socket to the list
|
||||
self.sockets.push(socket);
|
||||
}
|
||||
|
||||
/// Removes a socket from the namespace.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - A reference to the socket to be removed
|
||||
fn unsubscribe(&mut self, socket: SocketRef);
|
||||
#[instrument(skip_all)]
|
||||
pub fn unsubscribe(&mut self, socket: SocketRef) {
|
||||
// remove the socket from the list
|
||||
self.sockets.retain(|s| s.id != socket.id);
|
||||
}
|
||||
|
||||
/// Disconnects all clients in the namespace.
|
||||
///
|
||||
/// This will forcefully disconnect all sockets and clear the socket list.
|
||||
#[instrument(skip_all)]
|
||||
pub fn disconnect_all(&mut self) {
|
||||
tracing::info!("Disconnecting {} sockets in namespace", self.sockets.len());
|
||||
|
||||
// Disconnect each socket
|
||||
for socket in &self.sockets {
|
||||
let _ = socket.clone().disconnect(); // Ignore errors if socket is already disconnected
|
||||
}
|
||||
|
||||
// Clear the socket list
|
||||
self.sockets.clear();
|
||||
}
|
||||
|
||||
/// Re-emits cached events to a specific socket.
|
||||
///
|
||||
@@ -29,14 +68,23 @@ pub trait NamespaceInterface {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - A reference to the socket that will receive the cached events
|
||||
fn reemit(&mut self, socket: SocketRef);
|
||||
#[instrument(skip_all)]
|
||||
pub fn reemit(&mut self, socket: SocketRef) {
|
||||
// Collect events grouped by name/kind with their counts for sorting
|
||||
let mut event_groups: Vec<(&String, &Vec<Arc<GenericEvent>>)> =
|
||||
self.events.iter().collect();
|
||||
|
||||
/// Emits an event to all sockets in the namespace.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event to be emitted
|
||||
fn emit(&mut self, event: &GenericEvent);
|
||||
// Sort by event count (ascending - lowest count first)
|
||||
event_groups.sort_by(|a, b| a.1.len().cmp(&b.1.len()));
|
||||
|
||||
// Emit events in order of lowest count first
|
||||
for (_event_name, events) in event_groups {
|
||||
for event in events {
|
||||
// Send to global queue instead of per-socket queue
|
||||
self.send_to_queue(&socket, event, "reemit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches an event with a specific key for later retrieval.
|
||||
///
|
||||
@@ -45,97 +93,81 @@ pub trait NamespaceInterface {
|
||||
/// * `cache_key` - A string key to identify the cache
|
||||
/// * `event` - The event to be cached
|
||||
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
|
||||
#[instrument(skip_all)]
|
||||
fn cache(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
);
|
||||
event: Arc<GenericEvent>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>,
|
||||
) {
|
||||
let cached_events_for_key = self.events.entry(event.name.clone()).or_default();
|
||||
buffer_fn(cached_events_for_key, &event);
|
||||
}
|
||||
|
||||
/// Emits an event to all sockets in the namespace and caches it.
|
||||
///
|
||||
/// This is a convenience method that combines the functionality of
|
||||
/// [`Namespace::emit`] and [`Namespace::cache`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event to be emitted and cached
|
||||
/// * `cache_key` - A string key to identify the cache
|
||||
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
|
||||
fn emit_cached(
|
||||
#[instrument(skip_all)]
|
||||
pub fn emit(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Namespace {
|
||||
sockets: Vec<SocketRef>,
|
||||
events: HashMap<String, Vec<GenericEvent>>,
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sockets: vec![],
|
||||
events: HashMap::new(),
|
||||
event: Arc<GenericEvent>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>,
|
||||
) {
|
||||
// cache the event
|
||||
self.cache(event.clone(), buffer_fn);
|
||||
// emit the event - inlined from emit function
|
||||
// Send to global queue for each socket in the namespace
|
||||
for socket in self.sockets.clone() {
|
||||
self.send_to_queue(&socket, &event, "emit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NamespaceInterface for Namespace {
|
||||
fn subscribe(&mut self, socket: SocketRef) {
|
||||
// add the socket to the list
|
||||
self.sockets.push(socket.clone());
|
||||
}
|
||||
|
||||
fn unsubscribe(&mut self, socket: SocketRef) {
|
||||
// remove the socket from the list
|
||||
self.sockets.retain(|s| s.id != socket.id);
|
||||
}
|
||||
|
||||
fn reemit(&mut self, socket: SocketRef) {
|
||||
for (_, events) in self.events.iter() {
|
||||
log::debug!(
|
||||
"Re-emitting {} {} events to socket {}",
|
||||
events.len(),
|
||||
events[0].name,
|
||||
socket.id
|
||||
);
|
||||
for event in events.iter() {
|
||||
let _ = socket.emit("event", &event);
|
||||
/// Sends an event to the global queue for a specific socket.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - The socket to send the event to
|
||||
/// * `event` - The event to be sent
|
||||
/// * `operation` - A description of the operation for logging purposes (e.g., "reemit", "emit")
|
||||
#[instrument(skip_all)]
|
||||
fn send_to_queue(&self, socket: &SocketRef, event: &Arc<GenericEvent>, operation: &str) {
|
||||
tracing::trace!(
|
||||
socket_id = ?socket.id,
|
||||
event = %event.name,
|
||||
operation = %operation,
|
||||
"Sending event to global queue"
|
||||
);
|
||||
match self
|
||||
.socket_queue_tx
|
||||
.try_send((socket.clone(), event.clone()))
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::trace!(
|
||||
socket_id = ?socket.id,
|
||||
event = %event.name,
|
||||
operation = %operation,
|
||||
"Successfully sent event to global queue"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
socket_id = ?socket.id,
|
||||
event = %event.name,
|
||||
operation = %operation,
|
||||
error = %e,
|
||||
"Failed to send event to global queue"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&mut self, event: &GenericEvent) {
|
||||
for socket in self.sockets.iter() {
|
||||
let _ = socket.emit("event", &event);
|
||||
}
|
||||
}
|
||||
|
||||
fn cache(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
) {
|
||||
let mut cached_events_for_key = self
|
||||
.events
|
||||
.entry(event.name.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
buffer_fn(&mut cached_events_for_key, event);
|
||||
}
|
||||
|
||||
fn emit_cached(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
) {
|
||||
// cache the event
|
||||
self.cache(event, buffer_fn);
|
||||
|
||||
// emit the event
|
||||
self.emit(event);
|
||||
impl Drop for Namespace {
|
||||
fn drop(&mut self) {
|
||||
//self.disconnect_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,17 +179,17 @@ pub trait NamespaceCacheingLogic<V>
|
||||
where
|
||||
V: CacheableEvents<V>,
|
||||
{
|
||||
fn emit_cached(&mut self, event: V);
|
||||
fn emit(&mut self, event: V);
|
||||
}
|
||||
|
||||
pub trait CacheableEvents<Events> {
|
||||
fn event_value(&self) -> Result<GenericEvent, serde_json::Error>;
|
||||
fn event_value(&self) -> GenericEvent;
|
||||
fn event_cache_fn(&self) -> CacheFn;
|
||||
}
|
||||
|
||||
pub type CacheFn = Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>;
|
||||
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>;
|
||||
|
||||
/// [BufferFn] that stores the last n events
|
||||
/// [`BufferFn`] that stores the last n events
|
||||
pub fn cache_n_events(n: usize) -> CacheFn {
|
||||
Box::new(move |events, event| {
|
||||
if events.len() >= n {
|
||||
@@ -167,21 +199,41 @@ pub fn cache_n_events(n: usize) -> CacheFn {
|
||||
})
|
||||
}
|
||||
|
||||
/// [BufferFn] that stores only one event
|
||||
/// [`BufferFn`] that stores only one event
|
||||
pub fn cache_one_event() -> CacheFn {
|
||||
cache_n_events(1)
|
||||
}
|
||||
|
||||
/// [BufferFn] that stores events for a certain duration
|
||||
/// [`BufferFn`] that stores first and last event
|
||||
///
|
||||
/// The primary use case of this function is to cache both the default state of a machine, which should be emitted first,
|
||||
/// and the last event, which is the most recent state of the machine.
|
||||
pub fn cache_first_and_last_event() -> CacheFn {
|
||||
Box::new(move |events, event| {
|
||||
// if the events length 0 or 1, we just push the event
|
||||
if events.is_empty() || events.len() == 1 {
|
||||
events.push(event.clone());
|
||||
return;
|
||||
}
|
||||
// if the event length is 2 we remove the last event and append a new one
|
||||
if events.len() == 2 {
|
||||
events.remove(1);
|
||||
events.push(event.clone());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// [`BufferFn`] that stores events for a certain duration
|
||||
pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
|
||||
Box::new(move |events, event| {
|
||||
// Use event.ts instead of system time
|
||||
let current_time = event.ts as u128;
|
||||
|
||||
// calculate current bucket & last bucket
|
||||
let bucket = current_time / bucket_size.as_millis();
|
||||
let bucket_size_ms = bucket_size.as_millis().max(1);
|
||||
let bucket = current_time / bucket_size_ms; // Avoid division by zero
|
||||
let last_bucket = match events.last() {
|
||||
Some(last_event) => last_event.ts as u128 / bucket_size.as_millis(),
|
||||
Some(last_event) => last_event.ts as u128 / bucket_size_ms,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
@@ -211,24 +263,32 @@ pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
|
||||
mod tests {
|
||||
use std::cmp::min;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct TestEventData {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_n_events() {
|
||||
// Create a cache function that stores the last 2 events
|
||||
let cache_fn = cache_n_events(2);
|
||||
|
||||
// Create namespace
|
||||
let mut namespace = Namespace::new();
|
||||
// Create namespace with dummy queue_tx
|
||||
let (queue_tx, _queue_rx) = smol::channel::unbounded();
|
||||
let mut namespace = Namespace::new(queue_tx);
|
||||
assert!(namespace.events.is_empty());
|
||||
|
||||
// Add event
|
||||
let event1 = GenericEvent {
|
||||
let event1 = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": 1}),
|
||||
data: Box::new(TestEventData { value: 1 }),
|
||||
ts: 0,
|
||||
};
|
||||
namespace.cache(&event1, &cache_fn);
|
||||
});
|
||||
namespace.cache(event1, &cache_fn);
|
||||
|
||||
// Check that we have one event name in the map
|
||||
assert_eq!(namespace.events.len(), 1);
|
||||
@@ -237,12 +297,12 @@ mod tests {
|
||||
assert_eq!(namespace.events.get("test_event").unwrap()[0].ts, 0);
|
||||
|
||||
// Add another event
|
||||
let event2 = GenericEvent {
|
||||
let event2 = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": 2}),
|
||||
data: Box::new(TestEventData { value: 2 }),
|
||||
ts: 1,
|
||||
};
|
||||
namespace.cache(&event2, &cache_fn);
|
||||
});
|
||||
namespace.cache(event2, &cache_fn);
|
||||
|
||||
// Still one event name
|
||||
assert_eq!(namespace.events.len(), 1);
|
||||
@@ -252,12 +312,12 @@ mod tests {
|
||||
assert_eq!(namespace.events.get("test_event").unwrap()[1].ts, 1);
|
||||
|
||||
// Add a third event, which should remove the first one
|
||||
let event3 = GenericEvent {
|
||||
let event3 = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": 3}),
|
||||
data: Box::new(TestEventData { value: 3 }),
|
||||
ts: 2,
|
||||
};
|
||||
namespace.cache(&event3, &cache_fn);
|
||||
});
|
||||
namespace.cache(event3, &cache_fn);
|
||||
|
||||
// Still one event name
|
||||
assert_eq!(namespace.events.len(), 1);
|
||||
@@ -276,19 +336,20 @@ mod tests {
|
||||
let duration = Duration::new(10, 0);
|
||||
let bucket_size = Duration::new(1, 0);
|
||||
let cache_fn = cache_duration(duration, bucket_size);
|
||||
let (queue_tx, _queue_rx) = smol::channel::unbounded();
|
||||
|
||||
// Create namespace
|
||||
let mut namespace = Namespace::new();
|
||||
let mut namespace = Namespace::new(queue_tx);
|
||||
assert!(namespace.events.is_empty());
|
||||
|
||||
// Add events every 100ms for 20 seconds
|
||||
for i in 0..200 {
|
||||
let event = GenericEvent {
|
||||
let event = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": i}),
|
||||
data: Box::new(TestEventData { value: i }),
|
||||
ts: (i * 100) as u64,
|
||||
};
|
||||
namespace.cache(&event, &cache_fn);
|
||||
});
|
||||
namespace.cache(event, &cache_fn);
|
||||
|
||||
let should_have_events = min(i / 10, 11);
|
||||
assert_eq!(
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use crate::transmission::Transmission;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FixedTransmission {
|
||||
ratio: f64,
|
||||
}
|
||||
|
||||
impl Transmission for FixedTransmission {
|
||||
fn get_ratio(&self) -> f64 {
|
||||
self.ratio
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedTransmission {
|
||||
/// Creates a new FixedTransmission with the specified ratio.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ratio` - The transmission ratio (output over input).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new instance of FixedTransmission.
|
||||
pub const fn new(ratio: f64) -> Self {
|
||||
Self { ratio }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use units::{
|
||||
angle::radian,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{Angle, AngularVelocity, Length, Velocity},
|
||||
length::meter,
|
||||
velocity::meter_per_second,
|
||||
};
|
||||
|
||||
pub mod fixed;
|
||||
|
||||
/// A trait representing a mechanical transmission system that converts input to output
|
||||
/// with a specific gear ratio.
|
||||
///
|
||||
/// This trait provides methods for working with transmission ratios and converting
|
||||
/// between input and output values based on the transmission's gear ratio.
|
||||
pub trait Transmission {
|
||||
/// Returns the transmission ratio of this transmission system.
|
||||
///
|
||||
/// The ratio represents the relationship between input and output speeds/torques.
|
||||
/// A ratio greater than 1.0 indicates speed reduction (torque multiplication),
|
||||
/// while a ratio less than 1.0 indicates speed increase (torque reduction).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The transmission ratio as a floating-point number.
|
||||
fn get_ratio(&self) -> f64;
|
||||
|
||||
/// Converts an input value to the corresponding output value using the transmission ratio.
|
||||
///
|
||||
/// This method multiplies the input by the transmission ratio to calculate the output.
|
||||
/// Typically used for converting input speed to output speed in a transmission system.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - The input value to be converted
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The output value after applying the transmission ratio.
|
||||
fn calculate_output(&self, input: f64) -> f64 {
|
||||
input * self.get_ratio()
|
||||
}
|
||||
|
||||
/// Converts an output value to the corresponding input value using the transmission ratio.
|
||||
///
|
||||
/// This method divides the output by the transmission ratio to calculate the required input.
|
||||
/// Typically used for determining what input is needed to achieve a desired output.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `output` - The output value to be converted back to input
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The input value required to produce the given output.
|
||||
fn calculate_input(&self, output: f64) -> f64 {
|
||||
output / self.get_ratio()
|
||||
}
|
||||
|
||||
fn calculate_angular_output(&self, input: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.calculate_output(input.get::<radian>()))
|
||||
}
|
||||
|
||||
fn calculate_angular_input(&self, output: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.calculate_input(output.get::<radian>()))
|
||||
}
|
||||
|
||||
fn calculate_angular_velocity_output(&self, input: AngularVelocity) -> AngularVelocity {
|
||||
AngularVelocity::new::<radian_per_second>(
|
||||
self.calculate_output(input.get::<radian_per_second>()),
|
||||
)
|
||||
}
|
||||
|
||||
fn calculate_angular_velocity_input(&self, output: AngularVelocity) -> AngularVelocity {
|
||||
AngularVelocity::new::<radian_per_second>(
|
||||
self.calculate_input(output.get::<radian_per_second>()),
|
||||
)
|
||||
}
|
||||
|
||||
fn calculate_linear_output(&self, input: Length) -> Length {
|
||||
Length::new::<meter>(self.calculate_output(input.get::<meter>()))
|
||||
}
|
||||
|
||||
fn calculate_linear_input(&self, output: Length) -> Length {
|
||||
Length::new::<meter>(self.calculate_input(output.get::<meter>()))
|
||||
}
|
||||
|
||||
fn calculate_linear_velocity_output(&self, input: Velocity) -> Velocity {
|
||||
Velocity::new::<meter_per_second>(self.calculate_output(input.get::<meter_per_second>()))
|
||||
}
|
||||
|
||||
fn calculate_linear_velocity_input(&self, output: Velocity) -> Velocity {
|
||||
Velocity::new::<meter_per_second>(self.calculate_input(output.get::<meter_per_second>()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use anyhow::Error;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
/// Video stream manager that handles multiple video streams per machine with true broadcasting
|
||||
/// Uses tokio broadcast channels to ensure all subscribers receive all messages without stealing
|
||||
#[derive(Debug)]
|
||||
pub struct VideoStreamManager {
|
||||
/// Each stream has its own broadcast transmitter and template receiver
|
||||
streams: HashMap<String, (broadcast::Sender<Vec<u8>>, broadcast::Receiver<Vec<u8>>)>,
|
||||
}
|
||||
|
||||
impl VideoStreamManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
streams: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a frame to all subscribers of a stream
|
||||
/// Returns Ok if the frame was sent, Err if the stream doesn't exist
|
||||
pub fn send_frame(&self, stream_id: &str, frame: Vec<u8>) -> Result<(), Error> {
|
||||
if let Some((tx, _rx)) = self.streams.get(stream_id) {
|
||||
match tx.send(frame) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Ok(()), // No receivers, but that's not an error
|
||||
}
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Stream '{}' not found", stream_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new video stream with the given ID
|
||||
pub fn add_stream(&mut self, stream_id: String) {
|
||||
let (tx, rx) = broadcast::channel::<Vec<u8>>(256); // 256 frame buffer capacity
|
||||
self.streams.insert(stream_id, (tx, rx));
|
||||
}
|
||||
|
||||
/// Get list of available stream IDs
|
||||
pub fn get_streams(&self) -> Vec<String> {
|
||||
self.streams.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Check if a stream exists
|
||||
pub fn has_stream(&self, stream_id: &str) -> bool {
|
||||
self.streams.contains_key(stream_id)
|
||||
}
|
||||
|
||||
/// Remove a stream and all its subscribers
|
||||
pub fn remove_stream(&mut self, stream_id: &str) -> bool {
|
||||
self.streams.remove(stream_id).is_some()
|
||||
}
|
||||
|
||||
/// Subscribe to a video stream and get a receiver
|
||||
/// Returns the receiver for the new subscription
|
||||
pub fn subscribe_to_stream(&self, stream_id: &str) -> Option<broadcast::Receiver<Vec<u8>>> {
|
||||
self.streams
|
||||
.get(stream_id)
|
||||
.map(|(_tx, rx)| rx.resubscribe()) // Resubscribe to create a new independent receiver
|
||||
}
|
||||
|
||||
/// Get subscriber count for a stream
|
||||
pub fn get_subscriber_count(&self, stream_id: &str) -> usize {
|
||||
self.streams
|
||||
.get(stream_id)
|
||||
.map_or(0, |(tx, _rx)| tx.receiver_count())
|
||||
}
|
||||
|
||||
/// Get a stream transmitter for sending frames (used by machines to send video)
|
||||
/// Returns None if the stream doesn't exist
|
||||
pub fn get_sender(&self, stream_id: &str) -> Option<broadcast::Sender<Vec<u8>>> {
|
||||
self.streams.get(stream_id).map(|(tx, _rx)| tx.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VideoStreamManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_video_stream_manager() {
|
||||
let mut manager = VideoStreamManager::new();
|
||||
|
||||
// Register streams
|
||||
manager.register_stream("camera1".to_string());
|
||||
manager.register_stream("camera2".to_string());
|
||||
|
||||
assert_eq!(manager.get_stream_ids().len(), 2);
|
||||
assert!(manager.has_stream("camera1"));
|
||||
assert!(manager.has_stream("camera2"));
|
||||
assert!(!manager.has_stream("camera3"));
|
||||
|
||||
// Subscribe to streams
|
||||
let mut rx1 = manager.subscribe_to_stream("camera1").unwrap();
|
||||
let mut rx2 = manager.subscribe_to_stream("camera1").unwrap();
|
||||
let mut rx3 = manager.subscribe_to_stream("camera2").unwrap();
|
||||
|
||||
assert_eq!(manager.get_subscriber_count("camera1"), 3); // Original + 2 clones
|
||||
assert_eq!(manager.get_subscriber_count("camera2"), 2); // Original + 1 clone
|
||||
assert_eq!(manager.get_subscriber_count("nonexistent"), 0);
|
||||
|
||||
// Send frames
|
||||
let frame1 = vec![1, 2, 3, 4];
|
||||
let frame2 = vec![5, 6, 7, 8];
|
||||
|
||||
assert!(manager.send_single_frame("camera1", frame1.clone()));
|
||||
assert!(manager.send_single_frame("camera2", frame2.clone()));
|
||||
assert!(!manager.send_single_frame("nonexistent", vec![]));
|
||||
|
||||
// Both camera1 subscribers should receive the frame
|
||||
assert_eq!(rx1.recv().await.unwrap(), frame1);
|
||||
assert_eq!(rx2.recv().await.unwrap(), frame1);
|
||||
|
||||
// Camera2 subscriber should receive its frame
|
||||
assert_eq!(rx3.recv().await.unwrap(), frame2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_video_frame_broadcasting() {
|
||||
let mut manager = VideoStreamManager::new();
|
||||
manager.register_stream("main_camera".to_string());
|
||||
|
||||
// Multiple clients subscribe to the same camera
|
||||
let mut rx1 = manager.subscribe_to_stream("main_camera").unwrap();
|
||||
let mut rx2 = manager.subscribe_to_stream("main_camera").unwrap();
|
||||
let mut rx3 = manager.subscribe_to_stream("main_camera").unwrap();
|
||||
|
||||
// Simulate video frames
|
||||
let frames = vec![
|
||||
vec![0xFF, 0xD8, 0xFF, 0xE0], // JPEG header
|
||||
vec![0x00, 0x01, 0x02, 0x03], // Frame 1
|
||||
vec![0x04, 0x05, 0x06, 0x07], // Frame 2
|
||||
];
|
||||
|
||||
for frame in &frames {
|
||||
assert!(manager.send_single_frame("main_camera", frame.clone()));
|
||||
}
|
||||
|
||||
// All clients should receive all frames in order
|
||||
for frame in &frames {
|
||||
assert_eq!(rx1.recv().await.unwrap(), *frame);
|
||||
assert_eq!(rx2.recv().await.unwrap(), *frame);
|
||||
assert_eq!(rx3.recv().await.unwrap(), *frame);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stream_management() {
|
||||
let mut manager = VideoStreamManager::new();
|
||||
|
||||
// Initially no streams
|
||||
assert_eq!(manager.get_stream_ids().len(), 0);
|
||||
|
||||
// Add streams
|
||||
manager.register_stream("front_camera".to_string());
|
||||
manager.register_stream("rear_camera".to_string());
|
||||
manager.register_stream("side_camera".to_string());
|
||||
|
||||
let mut stream_ids = manager.get_stream_ids();
|
||||
stream_ids.sort(); // Sort for consistent testing
|
||||
assert_eq!(
|
||||
stream_ids,
|
||||
vec!["front_camera", "rear_camera", "side_camera"]
|
||||
);
|
||||
|
||||
// Remove a stream
|
||||
assert!(manager.remove_stream("rear_camera"));
|
||||
assert!(!manager.remove_stream("rear_camera")); // Already removed
|
||||
assert!(!manager.has_stream("rear_camera"));
|
||||
|
||||
assert_eq!(manager.get_stream_ids().len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_frame_results() {
|
||||
let mut manager = VideoStreamManager::new();
|
||||
manager.register_stream("test_camera".to_string());
|
||||
|
||||
// One initial receiver (the original one created in register_stream)
|
||||
let result = manager.send_frame("test_camera", vec![1, 2, 3]).unwrap();
|
||||
assert_eq!(result, 1); // Number of subscribers
|
||||
|
||||
// Add subscribers by cloning the receiver
|
||||
let _rx1 = manager.subscribe_to_stream("test_camera").unwrap();
|
||||
let _rx2 = manager.subscribe_to_stream("test_camera").unwrap();
|
||||
|
||||
// Send frame to multiple subscribers (original + 2 clones = 3 total)
|
||||
let result = manager.send_frame("test_camera", vec![4, 5, 6]).unwrap();
|
||||
assert_eq!(result, 3); // Number of subscribers
|
||||
|
||||
// Try sending to non-existent stream
|
||||
let result = manager.send_frame("nonexistent", vec![7, 8, 9]);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Stream 'nonexistent' not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_video_stream_manager() {
|
||||
let manager = VideoStreamManager::default();
|
||||
assert_eq!(manager.get_stream_ids().len(), 0);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -0,0 +1,145 @@
|
||||
# How can I add a new Machine?
|
||||
|
||||
Adding a new machine follows a common structure in `server/machines/`.
|
||||
|
||||
|
||||
|
||||
# TLDR
|
||||
|
||||
Summary
|
||||
|
||||
When adding a new machine:
|
||||
|
||||
- Create Folder server/machines/YOUR_MACHINE_NAME/.
|
||||
- Add the 4 files (mod.rs, new.rs, act.rs, api.rs).
|
||||
- Register it in machines/mod.rs with a unique constant.
|
||||
- mod.rs: define the struct + emit functions.
|
||||
- new.rs: assemble devices/controllers into the machine.
|
||||
- act.rs: implement the machine loop (MachineAct).
|
||||
- api.rs: define request (mutations) and response (events) types.
|
||||
|
||||
Tip: Start simple (copy MockMachine) and grow complexity as needed (add EtherCAT devices, controllers, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 1. Create the folder
|
||||
|
||||
Create a new directory:
|
||||
server/machines/YOUR_MACHINE_NAME
|
||||
|
||||
At a minimum, this folder needs the following files:
|
||||
|
||||
- `mod.rs`
|
||||
- `new.rs`
|
||||
- `act.rs`
|
||||
- `api.rs`
|
||||
|
||||
---
|
||||
|
||||
## 2. Register the module
|
||||
|
||||
Add your machine to `server/machines/mod.rs`:
|
||||
|
||||
```rust
|
||||
// mod.rs
|
||||
pub mod buffer1;
|
||||
pub mod extruder1;
|
||||
pub mod laser;
|
||||
pub mod mock;
|
||||
pub mod registry;
|
||||
pub mod winder2;
|
||||
pub mod YOUR_MACHINE;
|
||||
|
||||
pub const VENDOR_QITECH: u16 = 0x0001;
|
||||
pub const MACHINE_WINDER_V1: u16 = 0x0002;
|
||||
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
|
||||
pub const MACHINE_LASER_V1: u16 = 0x0006;
|
||||
pub const MACHINE_MOCK: u16 = 0x0007;
|
||||
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
|
||||
pub const YOUR_MACHINE_V1: u16 = 0xffff; // pick a unique ID to avoid collisions also DO NOT touch any already defined ids!!
|
||||
```
|
||||
|
||||
## 3. mod.rs
|
||||
|
||||
This file defines your machine struct and the required event emitters.
|
||||
|
||||
Look at MockMachine’s mod.rs for the simplest possible version.
|
||||
|
||||
At a minimum, you need:
|
||||
- emit_state
|
||||
- emit_live_values
|
||||
- A machine struct for state(e.g. MockMachine, Extruder, Laser, Winder, …)
|
||||
|
||||
More complex machines (e.g. Extruder, Winder) encapsulate controllers that in turn manage devices like EtherCAT terminals.
|
||||
|
||||
## 4 new.rs
|
||||
|
||||
This file implements MachineNewTrait for your machine.
|
||||
Here you assemble your machine from its parts — devices, controllers, and any state.
|
||||
|
||||
Minimal example: see MockMachine → basically no devices, just returns a struct.
|
||||
|
||||
Medium complexity: see Laser → wraps a single serial USB device.
|
||||
|
||||
High complexity: see Extruder → configures multiple EtherCAT devices inside controllers.
|
||||
|
||||
Think of new.rs as the factory:
|
||||
This is the place to declare which devices make up your machine and how they’re configured.
|
||||
|
||||
## 5. act.rs
|
||||
|
||||
This file implements the machine control loop using MachineAct.
|
||||
The act() method is called repeatedly by the control runtime.
|
||||
|
||||
```rs
|
||||
impl MachineAct for YourMachine {
|
||||
fn act(&mut self, _now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let now = Instant::now();
|
||||
|
||||
// Update state, e.g. step a controller
|
||||
self.some_controller.update();
|
||||
|
||||
// Emit live values ~30 Hz if machine is in Running mode
|
||||
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.maybe_emit_state_event();
|
||||
self.emit_live_values();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. api.rs
|
||||
|
||||
Defines the frontend-facing API for your machine:
|
||||
|
||||
Mutations → requests that the machine accepts
|
||||
(e.g. start/stop commands, setpoints, configuration updates).
|
||||
|
||||
Events → responses emitted to the frontend
|
||||
(e.g. live sensor data, alarms, machine state changes).
|
||||
|
||||
Look at MockMachine’s api.rs for a minimal version.
|
||||
|
||||
```rs
|
||||
/// Mutations are API requests to control the machine
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum YourMachineMutation {
|
||||
Start,
|
||||
Stop,
|
||||
SetSpeed { rpm: f64 },
|
||||
}
|
||||
|
||||
/// Events are responses sent to the frontend
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum YourMachineEvent {
|
||||
StateChanged { state: String },
|
||||
LiveValues { rpm: f64, torque: f64 },
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Code Style
|
||||
|
||||
# General Code styling
|
||||
|
||||
Try to adhere to the rust style guide:
|
||||
https://doc.rust-lang.org/nightly/style-guide/
|
||||
|
||||
# Beckhoff terminals and Machines
|
||||
|
||||
For Beckhoff terminals you should first implement the basic functionality, then an Interpreter of the device/data is built on top.
|
||||
A good example of how to do this is by looking at the following code:
|
||||
el3021.rs , AnalogInputDevice
|
||||
|
||||
El3021 implements a so called AnalogInputDevice, which acts like an interface.
|
||||
This allows us to use AnalogInputDevice instead of having to always specify which device should be used.
|
||||
In Essence any Device that implements AnalogInputDevice can be used.
|
||||
|
||||
AnalogInputDevice is then used for creating an AnalogInput, which is plug and play for machines.
|
||||
|
||||
Then a further layer of Abstraction is used on top,
|
||||
where the data is interpreted and incorporated into a sort of control loop for certain functions, for example:
|
||||
|
||||
```rust
|
||||
// ScrewSpeedController controlls the speed (frequency) and direction of the motor
|
||||
// AnalogInput is used to collect measurements from the pressure sensor.
|
||||
// screw_speed_controller.rs
|
||||
#[derive(Debug)]
|
||||
pub struct ScrewSpeedController {
|
||||
pub pid: ClampingTimeagnosticPidController,
|
||||
pub target_pressure: Pressure,
|
||||
pub target_rpm: AngularVelocity,
|
||||
pub inverter: MitsubishiCS80,
|
||||
pressure_sensor: AnalogInput,
|
||||
last_update: Instant,
|
||||
uses_rpm: bool,
|
||||
forward_rotation: bool,
|
||||
transmission_converter: TransmissionConverter,
|
||||
frequency: Frequency,
|
||||
maximum_frequency: Frequency,
|
||||
minimum_frequency: Frequency,
|
||||
motor_on: bool,
|
||||
nozzle_pressure_limit: Pressure,
|
||||
nozzle_pressure_limit_enabled: bool,
|
||||
}
|
||||
|
||||
// Under the hood for ExtruderV2 a El3021 is used:
|
||||
// (from new.rs of extruder)
|
||||
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
|
||||
|
||||
// But the extruder does not directly use the AnalogInput, instead the logic is decoupled in the screw_speed_controller:
|
||||
let mut extruder: ExtruderV2 = Self {
|
||||
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
|
||||
last_measurement_emit: Instant::now(),
|
||||
mode: ExtruderV2Mode::Standby,
|
||||
temperature_controller_front: temperature_controller_front,
|
||||
temperature_controller_middle: temperature_controller_middle,
|
||||
temperature_controller_back: temperature_controller_back,
|
||||
temperature_controller_nozzle: temperature_controller_nozzle,
|
||||
screw_speed_controller: screw_speed_controller,
|
||||
emitted_default_state: false,
|
||||
};
|
||||
|
||||
// Then in act.rs
|
||||
// The extruder simply calls the update function of the screw_speed_controller,
|
||||
// which contains "all" of the logic needed to properly drive the motor
|
||||
self.screw_speed_controller.update(now_ts, true);
|
||||
|
||||
|
||||
```
|
||||
|
||||
This is desired behaviour for all implemented devices, the machines should stay as modular as possible.
|
||||
|
||||
# Representing physical Units (Volt,Ampere,Hz,RPM etc)
|
||||
|
||||
To represent physical units like voltage ampere etc... we use uom (units of measurement), which acts as a wrapper for units and supplies all conversions between certain units.
|
||||
For example:
|
||||
|
||||
```rust
|
||||
// Lets say a device measures voltage, you just got the raw value
|
||||
let volts : ElectricPotential = ElectricPotential::new::<volt>(raw_value_here);
|
||||
// Then you can do this to get milli_volts
|
||||
// In api responses we always use the RAW float value (64bit):
|
||||
let milli_volts : f64 = volts.get::<millivolt>();
|
||||
```
|
||||
|
||||
# General Advice
|
||||
|
||||
- Try to avoid duplicate code (DRY principle)
|
||||
- Try to avoid lifetimes if at all possible
|
||||
- Avoid excessive abstractions
|
||||
- Avoid async code unless it is required
|
||||
- Avoid taking Ownership of values, instead borrow the value
|
||||
- When implementing a Trait for a struct, do it in the same file as the struct definition
|
||||
- Split up large impl blocks into smaller impl blocks
|
||||
@@ -0,0 +1,84 @@
|
||||
# Environment Setup
|
||||
|
||||
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
|
||||
|
||||
## Ubuntu/Debian Installation
|
||||
|
||||
```bash
|
||||
# Just press enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
apt install npm
|
||||
apt install nodejs
|
||||
# assuming you dont have git already, Git setup not included in here
|
||||
apt install git
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control
|
||||
cd electron
|
||||
npm install
|
||||
```
|
||||
|
||||
# Running Backend and Frontend Linux
|
||||
|
||||
## Backend
|
||||
|
||||
To Compile backend code and run it on Linux:
|
||||
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
|
||||
The script sets capabilities on the compiled binary like raw socket access.
|
||||
Here it is required that you are connected to atleast one machine that is communicated with over ethercat or usb/serial.
|
||||
The Most minimal working setup would be to connect over ethernet to an ek1100 beckhoff terminal.
|
||||
|
||||
## Mock-Machine
|
||||
|
||||
The Mock-Machine can be used to test code that does not require an actual machine connection.
|
||||
Like Frontend Code for example.
|
||||
|
||||
Mock machines can be used if the compile feature mock-machine is enabled.
|
||||
|
||||
```bash
|
||||
# in root git folder
|
||||
./cargo_run_linux.sh mock-machine
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
To run the Frontend Code:
|
||||
|
||||
```bash
|
||||
# need to be in the electron folder and an additional terminal
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
Generally you contribute to the codebase by:
|
||||
|
||||
1. Opening or Choosing an existing issue (Bug,Feature,Task etc)
|
||||
2. Work on it locally
|
||||
3. Commit your changes locally
|
||||
4. Before pushing changes, if there were changes on the master branch, rebase your branch like so:
|
||||
|
||||
```bash
|
||||
git fetch
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
5. Push changes
|
||||
|
||||
```bash
|
||||
# IF you needed to rebase
|
||||
git push --force-with-lease
|
||||
# ELSE you just push
|
||||
git push
|
||||
```
|
||||
|
||||
6. Open a Pull Request on Github and link the issue by writing fix #issue_number, after the pull request is merged into master the branch is closed automatically
|
||||
7. Request a Review and hope for the best :)
|
||||
|
||||
# Recommended Editor Setup
|
||||
|
||||
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Performance/Stability Tweaks
|
||||
## EtherCrab Config
|
||||
We set ethercrab to default settings for everything except for RetryBehaviour, so that one unlikely failure doesnt destroy the EtherCAT network.
|
||||
|
||||
```rust
|
||||
let maindevice = MainDevice::new(
|
||||
pdu,
|
||||
Timeouts {
|
||||
// Default 5000ms
|
||||
state_transition: Duration::from_millis(5000),
|
||||
// Default 30_000us
|
||||
pdu: Duration::from_micros(30_000),
|
||||
// Default 10ms
|
||||
eeprom: Duration::from_millis(10),
|
||||
// Default 0ms
|
||||
wait_loop_delay: Duration::from_millis(0),
|
||||
// Default 100ms
|
||||
mailbox_echo: Duration::from_millis(100),
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
},
|
||||
MainDeviceConfig {
|
||||
// Default RetryBehaviour::None
|
||||
retry_behaviour: RetryBehaviour::Count(5),
|
||||
// Default 10_000
|
||||
dc_static_sync_iterations: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
```
|
||||
wait_loop_delay CAN in theory also decrease CPU usage if set exactly as the loop_delay we set, however this setting has caused us some issues, so we just leave it as is.
|
||||
|
||||
## Realtime Kernel
|
||||
In our nixOs Config we use the latest realtime kernel and isolate TWO cores out of the Four available for the machines we use.
|
||||
|
||||
```nix
|
||||
#From configuration.nix
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
|
||||
boot.kernelModules = [ "i915" ];
|
||||
```
|
||||
|
||||
Notice how we hardcode the intel gpu kernel module. This is because only the GPU driver gets blacklisted for some reason. By specifying it manually it loads like before.
|
||||
|
||||
One Core is used for the EtherCrab TXRX Thread and one is used for the machine Thread.
|
||||
```
|
||||
"isolcpus=2,3" # Isolate cpus 2 and 3 from scheduler for better latency, 2 runs ethercatthread and 3 runs server control-loop
|
||||
"nohz_full=2,3" # In this mode, the periodic scheduler tick is stopped when only one task is running, reducing kernel interruptions on those CPUs.
|
||||
"rcu_nocbs=2,3" # Moves RCU (Read-Copy Update) callback processing away from CPUs 2 and 3.
|
||||
```
|
||||
|
||||
Essentially we completely disable acces from the operating system to these two cpus, which saves scheduler overhead, which would introduce noticable latency.
|
||||
Its not entirely clear yet if what we did here is overkill. Maybe we could use only one core and get away with that?
|
||||
Processor load normally hovers around 18% for one of the cores and like 9% for the other.
|
||||
|
||||
We also have some minor cpu tweaks in configuration.nix
|
||||
```
|
||||
# Graphical
|
||||
"logo.nologo" # Remove kernel logo during boot
|
||||
|
||||
# Performance
|
||||
|
||||
# Specific Vulnerabilities Addressed by Mitigations:
|
||||
# - Spectre variants (V1, V2, V4, SWAPGS, SpectreRSB, etc.)
|
||||
# - Meltdown (Rogue Data Cache Load)
|
||||
# - Foreshadow/L1TF (L1 Terminal Fault)
|
||||
# - Microarchitectural Data Sampling (MDS, RIDL, Fallout, ZombieLoad)
|
||||
# - SRBDS (Special Register Buffer Data Sampling)
|
||||
# - TSX Asynchronous Abort (TAA)
|
||||
# - iTLB Multihit
|
||||
# - And others as they're discovered and mitigated
|
||||
#
|
||||
# With mitigations=off
|
||||
# - PROS: Maximum performance, equivalent to pre-2018 behavior
|
||||
# - CONS: Vulnerable to Spectre, Meltdown, Foreshadow, ZombieLoad, etc.
|
||||
# Should ONLY be used in completely trusted environments
|
||||
# - Improves performance by 7-43%
|
||||
"mitigation=off"
|
||||
"intel_pstate=performance" # Intel CPU-specific performance mode (if applicable)
|
||||
|
||||
# Memory Management
|
||||
"transparent_hugepage=always" # Use larger memory pages for memory intense applications
|
||||
"nmi_watchdog=0" # Disable NMI watchdog for reduced CPU overhead and realtime execution
|
||||
|
||||
# High-throughput ethernet parameters
|
||||
"pcie_aspm=off" # Disable PCIe power management for NICs
|
||||
"intel_iommu=off" # Disable IOMMU (performance gain)
|
||||
|
||||
# Reliability
|
||||
"panic=10" # Auto-reboot 10 seconds after kernel panic
|
||||
"oops=panic" # Treat kernel oops as panic for auto-recovery
|
||||
"usbcore.autosuspend=-1" # Possibly fixes dre disconnect issue?
|
||||
```
|
||||
|
||||
We should check if its really required to turn mitigations off as well.
|
||||
|
||||
## Rust Code Optimizations
|
||||
|
||||
In General we try to avoid memory allocations in our loop as much as possible, as it is not deterministic
|
||||
|
||||
One problem we found, is that due to the realtime kernel our ethernet driver caused us quite large spikes in our tx_rx cycle time for EtherCAT.
|
||||
These spikes happen when CPU Load is very high on the two usable cores, causing the interrupt handler for Ethernet to stall.
|
||||
|
||||
Which is why we reassign the interrupt handler on startup to the same core that the EtherCatTxRx Thread runs on:
|
||||
|
||||
```rust
|
||||
#[cfg(target_os = "linux")]
|
||||
match set_irq_affinity(&interface, 3) {
|
||||
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
|
||||
Err(e) => tracing::error!("set_irq_affinity failed: {:?}", e),
|
||||
}
|
||||
// Set core affinity to 4th core
|
||||
let _ = set_core_affinity(3);
|
||||
|
||||
// Set the thread to real-time priority
|
||||
let _ = set_realtime_priority();
|
||||
```
|
||||
This means that the Interrupt Handler ALWAYS has enough ressources to cope with our demand for realtime communication with EtherCAT
|
||||
|
||||
|
||||
One Problem we have at the moment (we think) is the locking and async code blocking our server loop.
|
||||
Note that this doesnt affect the EtherCAT communication at all.
|
||||
|
||||
## Electron Optimizations (Not Yet Implemented)
|
||||
|
||||
Currently we are struggling with the fact, that apparently Electron doesnt really have good support for HW acceleration on Linux.
|
||||
Electron runs with the software-renderer, which is VERY slow compared to a gpu.
|
||||
This would eliminate the weird stuttering seen on Graphs and make them run buttery smooth, while also alleviating load off of the CPU.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Presets
|
||||
|
||||
To allow the user to save the current configuration of a machine, they can create a preset in the presets page. Presets are presited in electron's local storage.
|
||||
|
||||
Presets are all handled by the `PresetsPage`. To use the `PresetsPage` for a single machine, one must implement the following.
|
||||
|
||||
Create a zod schema for the data, the presets contain.
|
||||
```tsx
|
||||
const mock1PresetDataSchema = z
|
||||
.object({
|
||||
frequency1: z.number(),
|
||||
frequency2: z.number(),
|
||||
frequency3: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof mock1PresetDataSchema;
|
||||
```
|
||||
Here, intensionally, `Mock1` is the schema type itself. It can be used as a type argument everything that uses presets.
|
||||
|
||||
Next, create a map containing all schema versions currently supported (the new schema, to be precise).
|
||||
```tsx
|
||||
const schemas = new Map([[1, mock1PresetDataSchema]]);
|
||||
```
|
||||
New, the schema will be used to parse version `1`. To create new versions, see migrations below.
|
||||
|
||||
One also needs to define a table, how the preset data should be shown to the user.
|
||||
```tsx
|
||||
const previewEntries: PresetPreviewEntries<Mock1> = [
|
||||
{
|
||||
name: "Frequency 1",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency1?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 2",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency2?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 3",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency3?.toFixed(3),
|
||||
},
|
||||
];
|
||||
```
|
||||
`renderValue` gets the data itself and renders the corresponding part of it. Returning undefined will automatically be rendered as the value is not set, showing "N/A".
|
||||
|
||||
Create a function for the new react component and use the appropreate machine.
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Here, two more functions need to be created. const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,The first takes in a preset and applies it on the machine. If data is missing, use the default defined by the backend.
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
|
||||
const applyPreset = (preset: Preset<Mock1>) => {
|
||||
const frequency1 = preset.data?.frequency1 ?? 100;
|
||||
const frequency2 = preset.data?.frequency2 ?? 200;
|
||||
const frequency3 = preset.data?.frequency3 ?? 500;
|
||||
|
||||
setFrequency1(frequency1);
|
||||
setFrequency2(frequency2);
|
||||
setFrequency3(frequency3);
|
||||
};
|
||||
// ...
|
||||
}
|
||||
```
|
||||
The later does the opposite, it turns a machine state, comming from the backend, into the preset data
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
// ...
|
||||
const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,
|
||||
});
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Having all this, we can simple pass it to the `PresetsPage`
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
// ...
|
||||
return (
|
||||
<PresetsPage
|
||||
machine_identification={mock1.machine_identification}
|
||||
currentState={toPresetData(state)}
|
||||
schemas={schemas}
|
||||
schemaVersion={1}
|
||||
applyPreset={applyPreset}
|
||||
previewEntries={previewEntries}
|
||||
defaultState={toPresetData(defaultState)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
Also mind the machine identification, that has to match the machine we are using and the schema versionen. Here we pass `1`, as we also used `1` in the schemas map.
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are handled by the `PresetsPage`, however, one still has to define what to migrate.
|
||||
To do so, first create the new schema.
|
||||
```tsx
|
||||
const fancyNewMock1PresetDataSchema = z
|
||||
.object({
|
||||
frequency1_now_with_ai: z.number(),
|
||||
frequency2: z.number(),
|
||||
frequency3: z.number(),
|
||||
frequency4: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof fancyNewMock1PresetDataSchema;
|
||||
```
|
||||
and then, also create a migration that uses the new schema, but can handle the old format.
|
||||
```tsx
|
||||
const mock1PresetDataSchemaMigration: PresetSchema = z.preprocess(
|
||||
(obj: any): PresetData<Mock1> => ({
|
||||
frequency1_now_with_ai: obj?.frequency1, // Here, a field have been renamed
|
||||
frequency2: obj?.frequency2,
|
||||
frequency3: obj?.frequency3,
|
||||
frequency4: 666.0, // A new field is added. Initialize it with default the value
|
||||
}),
|
||||
fancyNewMock1PresetDataSchema,
|
||||
);
|
||||
```
|
||||
Usings zods preprocessing step,
|
||||
we can first transform the data as needed and then validate using the new schema accordingly.
|
||||
|
||||
We have to register both schemas in our schema map.
|
||||
```tsx
|
||||
const schemas = new Map<number, Mock1>([
|
||||
[1, mock1PresetDataSchemaMigration as Mock1],
|
||||
[2, fancyNewMock1PresetDataSchema],
|
||||
]);
|
||||
```
|
||||
Mind the cast, which is ok, because `mock1PresetDataSchemaMigration` parses `fancyNewMock1PresetDataSchema` by construction.
|
||||
|
||||
Lastly, simply change the schema version being passed to the `PresetsPage`.
|
||||
```diff
|
||||
@@ -62,7 +77,7 @@ export function Mock1PresetsPage() {
|
||||
machine_identification={mock1.machine_identification}
|
||||
currentState={toPresetData(state)}
|
||||
schemas={schemas}
|
||||
- schemaVersion={1}
|
||||
+ schemaVersion={2}
|
||||
applyPreset={applyPreset}
|
||||
previewEntries={previewEntries}
|
||||
defaultState={toPresetData(defaultState)}
|
||||
```
|
||||
Make sure, that all (exsiting and new migrations) return a schema version 2 conform format.
|
||||
Of corse, one can chain migrations of needed - just the end result must be complient.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Testing Ram and Cpu (Hardware)
|
||||
|
||||
There were instability problems on one of the hmi panel pc's related to ram.
|
||||
|
||||
To test Ram and Cpu for stability problems we use memtest86.
|
||||
|
||||
You can download it here: https://www.memtest86.com/download.htm
|
||||
Then in the unzipped folder you have to flash the .img file to a USB drive with atleast 2GB of space.
|
||||
We already have USB-Sticks flashed with memtest though.
|
||||
|
||||
1. When starting up the Pc mash Del (Entf in german) on an external keyboard and enter the bios
|
||||
|
||||
2. Select the Memtest86 usb stick in the Bios
|
||||
|
||||
3. Save changes and exit
|
||||
|
||||
4. Now memtest boots up, just let it do its thing from here
|
||||
|
||||
It works if the screen looks something like this:
|
||||
|
||||

|
||||
|
||||
Simply let it do one Pass ~20 minutes and shutdown and unplug usb stick afterwards
|
||||
|
||||
If it freezes completely -> unstable
|
||||
|
||||
If it shows errors the ram should be swapped out and the test repeated
|
||||
+161
@@ -15,6 +15,9 @@ Here is a checklist of what we need to implement:
|
||||
- field `rxpdo` instance of `EL0000RxPdo` (only if the device has outputs)
|
||||
- field `configuration` instance of `EL0000Configuration` (only if configurable)
|
||||
- implement `NewDevice` trait
|
||||
- implement `EthercatDeviceProcessing` trait
|
||||
- optional: override `input_post_process()` method for custom input data processing
|
||||
- optional: override `output_pre_process()` method for custom output data preparation
|
||||
- TxPdo / RxPdo Struct `struct EL0000TxPdo` and/or `struct EL0000RxPdo`
|
||||
- derive `#[derive(Debug, Clone)]`
|
||||
- derive `#[derive(TxPdo)]` and/or `#[derive(RxPdo)]`
|
||||
@@ -46,6 +49,37 @@ These are helpful resources:
|
||||
|
||||
ESI Files are not needed but could be an alternative reference, though not a very readable one.
|
||||
|
||||
## EthercatDeviceProcessing
|
||||
|
||||
The `EthercatDeviceProcessing` trait provides hooks for custom processing of input and output data that happens between the EtherCAT data exchange and the device's IO layer. Every EtherCAT device must implement this trait, even if it doesn't need custom processing.
|
||||
|
||||
This trait provides two optional methods:
|
||||
|
||||
- `input_post_process()`: Called after the device has received input data from the EtherCAT bus, allowing transformation of the raw PDO data before it's exposed through the IO interfaces.
|
||||
- `output_pre_process()`: Called before data is sent to the EtherCAT bus, allowing preparation or transformation of data from the IO interfaces before it's written to the PDO objects.
|
||||
|
||||
For most devices, the default implementations (which do nothing) are sufficient. You only need to override these methods when the raw PDO data requires transformation before being used by the IO layer or vice versa.
|
||||
|
||||
Example implementation:
|
||||
|
||||
```rust
|
||||
impl EthercatDeviceProcessing for EL0000 {
|
||||
// Optional: Only override when needed
|
||||
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
|
||||
// Process data after it's received from EtherCAT but before it's accessed via IO interfaces
|
||||
// Example: Transform raw sensor data into engineering units
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Optional: Only override when needed
|
||||
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
|
||||
// Process data before it's sent to EtherCAT
|
||||
// Example: Apply limits or transforms to output values
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Identity
|
||||
|
||||
We need to extract the possible EtherCAT identity values matching this driver implementation and provide them as constants.
|
||||
@@ -81,3 +115,130 @@ pub fn (
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Device Implementation Guide
|
||||
|
||||
This section describes how to implement EtherCAT devices in the framework used by the QiTech Control project (via `ethercat-hal`, `server`, etc.). We include a running example of a simple digital output device (EL2004) and discuss how it fits into the larger architecture.
|
||||
|
||||
## Architecture & Context
|
||||
|
||||
The QiTech Control system is built as:
|
||||
|
||||
- `ethercat-hal` / `ethercat-hal-derive`: Hardware abstraction, PDO / CoE handling, device traits.
|
||||
- `server`: Glue code that reads device identities, instantiates the correct device types, performs CoE configuration, and runs cyclic process data.
|
||||
- The `docs` directory documents APIs and device patterns.
|
||||
:contentReference[oaicite:0]{index=0}
|
||||
|
||||
In this architecture:
|
||||
|
||||
1. During initialization, subdevice identity tuples (Vendor ID, Product ID, Revision) are read.
|
||||
2. The `server` matches identity tuples to concrete `Device` types and constructs them (via `NewEthercatDevice`).
|
||||
3. The device instance then handles PDO exchange, and optional CoE (configuration) writes.
|
||||
4. Higher-level application code interacts with devices via traits like `DigitalOutputDevice`, `DigitalInputDevice`, etc.
|
||||
|
||||
---
|
||||
|
||||
## Example: Simple Digital Output Device (EL2004)
|
||||
|
||||
Below is a minimal example of a digital-output-only EtherCAT device. It shows how to map PDOs, implement traits, and define identity constants.
|
||||
|
||||
```rust
|
||||
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
|
||||
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
|
||||
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
|
||||
use crate::pdo::{RxPdo, basic::BoolPdoObject};
|
||||
use ethercat_hal_derive::{EthercatDevice, RxPdo};
|
||||
|
||||
/// EL2004 4-channel digital output device — 24 V DC, 0.5 A per channel
|
||||
#[derive(EthercatDevice)]
|
||||
pub struct EL2004 {
|
||||
/// The Rx PDO where output bits are written by the master
|
||||
pub rxpdo: EL2004RxPdo,
|
||||
is_used: bool,
|
||||
}
|
||||
|
||||
impl EthercatDeviceProcessing for EL2004 {}
|
||||
|
||||
impl std::fmt::Debug for EL2004 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "EL2004")
|
||||
}
|
||||
}
|
||||
|
||||
impl NewEthercatDevice for EL2004 {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
rxpdo: EL2004RxPdo::default(),
|
||||
is_used: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DigitalOutputDevice<EL2004Port> for EL2004 {
|
||||
fn set_output(&mut self, port: EL2004Port, value: DigitalOutputOutput) {
|
||||
let expect_text = "All channels should be Some(_)";
|
||||
match port {
|
||||
EL2004Port::DO1 => {
|
||||
self.rxpdo.channel1.as_mut().expect(expect_text).value = value.into()
|
||||
}
|
||||
EL2004Port::DO2 => {
|
||||
self.rxpdo.channel2.as_mut().expect(expect_text).value = value.into()
|
||||
}
|
||||
EL2004Port::DO3 => {
|
||||
self.rxpdo.channel3.as_mut().expect(expect_text).value = value.into()
|
||||
}
|
||||
EL2004Port::DO4 => {
|
||||
self.rxpdo.channel4.as_mut().expect(expect_text).value = value.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_output(&self, port: EL2004Port) -> DigitalOutputOutput {
|
||||
let expect_text = "All channels should be Some(_)";
|
||||
DigitalOutputOutput(match port {
|
||||
EL2004Port::DO1 => self.rxpdo.channel1.as_ref().expect(expect_text).value,
|
||||
EL2004Port::DO2 => self.rxpdo.channel2.as_ref().expect(expect_text).value,
|
||||
EL2004Port::DO3 => self.rxpdo.channel3.as_ref().expect(expect_text).value,
|
||||
EL2004Port::DO4 => self.rxpdo.channel4.as_ref().expect(expect_text).value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EL2004Port {
|
||||
DO1,
|
||||
DO2,
|
||||
DO3,
|
||||
DO4,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, RxPdo)]
|
||||
pub struct EL2004RxPdo {
|
||||
#[pdo_object_index(0x1600)]
|
||||
pub channel1: Option<BoolPdoObject>,
|
||||
#[pdo_object_index(0x1601)]
|
||||
pub channel2: Option<BoolPdoObject>,
|
||||
#[pdo_object_index(0x1602)]
|
||||
pub channel3: Option<BoolPdoObject>,
|
||||
#[pdo_object_index(0x1603)]
|
||||
pub channel4: Option<BoolPdoObject>,
|
||||
}
|
||||
|
||||
impl Default for EL2004RxPdo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
channel1: Some(BoolPdoObject::default()),
|
||||
channel2: Some(BoolPdoObject::default()),
|
||||
channel3: Some(BoolPdoObject::default()),
|
||||
channel4: Some(BoolPdoObject::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identity constants for EL2004
|
||||
pub const EL2004_VENDOR_ID: u32 = 0x2;
|
||||
pub const EL2004_PRODUCT_ID: u32 = 131346514;
|
||||
pub const EL2004_REVISION_A: u32 = 1179648;
|
||||
pub const EL2004_IDENTITY_A: SubDeviceIdentityTuple =
|
||||
(EL2004_VENDOR_ID, EL2004_PRODUCT_ID, EL2004_REVISION_A);
|
||||
@@ -0,0 +1,126 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="66gCWvEem9su0pM2-AdM" name="Page-1">
|
||||
<mxGraphModel dx="1454" dy="512" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="44" value="Serial Device Detection and Recognition process" style="swimlane;horizontal=0;whiteSpace=wrap;html=1;swimlaneFillColor=default;" parent="1" vertex="1">
|
||||
<mxGeometry x="-790" y="60" width="710" height="880" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="ADD new Devices&nbsp;" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="44" vertex="1">
|
||||
<mxGeometry x="525" y="290" width="130" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="SerialDeviceOld-&gt; only USB" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-735" y="825" width="180" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="Serial Detect" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-705" y="735" width="120" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="10" target="11" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-660" y="755" as="sourcePoint"/>
|
||||
<mxPoint x="-610" y="705" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="SerialDeviceNewFound<span style="color: rgb(0, 0, 0);">-&gt; only USB</span>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-735" y="645" width="180" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="11" target="16" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-660" y="755" as="sourcePoint"/>
|
||||
<mxPoint x="-610" y="705" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="Compare" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-485" y="715" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="" style="endArrow=classic;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="16" target="18" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-565" y="695" as="sourcePoint"/>
|
||||
<mxPoint x="-515" y="645" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="" style="endArrow=classic;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="10" target="18" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-565" y="695" as="sourcePoint"/>
|
||||
<mxPoint x="-515" y="645" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="Tuple (Added, Removed)" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-365" y="725" width="150" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="endArrow=classic;html=1;" parent="1" source="18" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-385" y="805" as="sourcePoint"/>
|
||||
<mxPoint x="-365" y="755" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="" style="shape=flexArrow;endArrow=classic;html=1;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-645" y="925" as="sourcePoint"/>
|
||||
<mxPoint x="-645" y="865" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="BEGIN" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="23" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.5752" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="Delete Removed Devices from HashMap" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-430" y="615" width="125" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="Try to connect to detected devices" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-265" y="615" width="130" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="21" target="25" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-325" y="605" as="sourcePoint"/>
|
||||
<mxPoint x="-275" y="555" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="21" target="26" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-280" y="735" as="sourcePoint"/>
|
||||
<mxPoint x="-357" y="675" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="Vec&lt;(path: String, Result&lt;SerialDevice, Error&gt;)" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-267.5" y="495" width="135" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="26" target="29" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-325" y="465" as="sourcePoint"/>
|
||||
<mxPoint x="-275" y="415" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="29" target="38" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-325" y="325" as="sourcePoint"/>
|
||||
<mxPoint x="-200" y="415" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="Global&nbsp;&nbsp;<span style="color: rgb(0, 0, 0);">Result&lt;HashMap, Error&gt; with connected devices</span>" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="-725" y="90" width="250" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="41" value="" style="edgeStyle=elbowEdgeStyle;elbow=vertical;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.468;entryY=1.001;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="25" target="36" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-345" y="465" as="sourcePoint"/>
|
||||
<mxPoint x="-295" y="415" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="-480" y="550"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="42" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="38" target="36" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-475" y="245" as="sourcePoint"/>
|
||||
<mxPoint x="-425" y="195" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="-280" y="260"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,13 @@
|
||||
# Electrical Diagrams
|
||||
|
||||
This directory contains electrical wiring diagrams for various machines and systems supported by the control system.
|
||||
|
||||
## Available Diagrams
|
||||
|
||||
### Extruder
|
||||
- **Location**: [`extruder/`](extruder/)
|
||||
- **File**: [QiTech Pro Extruder Electrical Diagram 2025.pdf](extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025.pdf)
|
||||
|
||||
### Winder
|
||||
- **Location**: [`winder/`](winder/)
|
||||
- **File**: [QiTech Pro Winder Electrical Diagram.pdf](winder/QiTech_Pro_Winder_Electrical_Diagram.pdf)
|
||||
Binary file not shown.
Binary file not shown.
@@ -27,13 +27,13 @@ We need to know what the purpose of each device in a machine is:
|
||||
|
||||
How and where do we save the identification values? A machine does not have a central storage medium. Saving the network configuration in the application is bad when changing the EtherCAT master device. Additionally, the serial number in the EEPROM is always `0`. Not all devices have the CoE key value store.
|
||||
|
||||
The solution: Persisting the values in empty spaces inside each device's EEPROM. As seen in the [ESC Access](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_io_intro/1358008331.html&id=) documentation, there are reserved spaces. With a hex dump of the EEPROM, we can confirm that these spaces are likely not used. The EEPROM is very small, so we can fit the values as four `u32` (Big Endian) values. In case we need to customize the position of the values, we customize the addresses for each device.
|
||||
The solution: Persisting the values in empty spaces inside each device's EEPROM. As seen in the [ESC Access](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_io_intro/1358008331.html&id=) documentation, there are reserved spaces. With a hex dump of the EEPROM, we can confirm that these spaces are likely not used. The EEPROM is very small, so we can fit the values as four `u16` (Big Endian) values. In case we need to customize the position of the values, we customize the addresses for each device.
|
||||
|
||||
The default word (not byte) addresses are:
|
||||
- Vendor: `0x0028`
|
||||
- Machine: `0x0029`
|
||||
- Serial: `0x002A`
|
||||
- Machine: `0x002C`
|
||||
- Role: `0x002E`
|
||||
- Role: `0x002B`
|
||||
|
||||
## Writing values
|
||||
1. For each new device, we need to identify free space in the EEPROM and add them to `get_identification_addresses` in `control-core/src/identification/mod.rs`.
|
||||
|
||||
Loaded 100 of 606 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user