Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45c7b24420 | ||
|
|
082d5dc952 | ||
|
|
55af1120bf | ||
|
|
35864b4de7 | ||
|
|
621cb1913f | ||
|
|
3e3e3f1c0b | ||
|
|
cdf0374020 | ||
|
|
ec406889f7 | ||
|
|
b731a3bc36 | ||
|
|
27e15820f7 | ||
|
|
bb307bb9d7 | ||
|
|
39b05b056f | ||
|
|
8449045efb | ||
|
|
8a3de40501 | ||
|
|
853d0a4478 | ||
|
|
d6dccc1b1a | ||
|
|
5f636a6f15 | ||
|
|
ac8a8c37ed | ||
|
|
03e057f6a9 | ||
|
|
fb195eb7d4 | ||
|
|
874766633a | ||
|
|
047b0fbe55 | ||
|
|
e3e9412578 | ||
|
|
92ead0e775 | ||
|
|
06a6c81b55 | ||
|
|
7b858251ea | ||
|
|
04700d690c | ||
|
|
e4d950ae9b | ||
|
|
7e03148d88 | ||
|
|
5562cf5a53 | ||
|
|
8c77ea41fd | ||
|
|
ef1a2c3ca8 | ||
|
|
66e30e2085 | ||
|
|
6ca15dbebc | ||
|
|
7e5d44c329 | ||
|
|
dc287d61cc | ||
|
|
9aed1193a3 | ||
|
|
5fae6b628f | ||
|
|
88c6195d6c | ||
|
|
af0b5deff0 | ||
|
|
743a589c1d | ||
|
|
ee7550d89f | ||
|
|
ec13f715aa | ||
|
|
2af746e7a4 | ||
|
|
3af3fb9d79 | ||
|
|
ff3103bec2 | ||
|
|
5eb3c39f84 | ||
|
|
2c715d6a8e | ||
|
|
fb12001721 | ||
|
|
ad94754fcf | ||
|
|
1c0e06d4e1 | ||
|
|
b919721c3a | ||
|
|
a428a2aa02 | ||
|
|
7b479c6021 | ||
|
|
17ac24c79d | ||
|
|
8c7a04b616 | ||
|
|
fe248c79ae | ||
|
|
038df09611 | ||
|
|
9e43d8d431 | ||
|
|
d364263d6e | ||
|
|
376e4a4ea2 | ||
|
|
0b30ae9a85 | ||
|
|
528952238d | ||
|
|
8c32ce2a2b | ||
|
|
579ef0d2e2 | ||
|
|
b6263e90d6 | ||
|
|
f67f098ff3 | ||
|
|
1a3c26b789 | ||
|
|
49f0c9f289 | ||
|
|
e60daed274 | ||
|
|
9a4d41bf48 | ||
|
|
9a9120398e | ||
|
|
dec342498c | ||
|
|
bc8cc5dd3d | ||
|
|
4a1f3d125d | ||
|
|
1d1eeb3d48 | ||
|
|
c395d4e172 | ||
|
|
5009c5ca75 | ||
|
|
50dbc46b8c | ||
|
|
774e7e2f8b | ||
|
|
270872fe81 | ||
|
|
a749f59186 | ||
|
|
bae1de3c54 | ||
|
|
7681dae32c | ||
|
|
2d98d369e1 | ||
|
|
310ac42a88 | ||
|
|
cbffeff52d | ||
|
|
182df9c569 | ||
|
|
153a719d64 | ||
|
|
84603d1594 | ||
|
|
17b2e3e297 | ||
|
|
fb1cf33c7f | ||
|
|
ae2d0fa8b1 | ||
|
|
ed06b63b77 | ||
|
|
9f0f45d19c | ||
|
|
a75c0ff08a | ||
|
|
205432c926 | ||
|
|
781783464b | ||
|
|
4fa2a1a221 | ||
|
|
b734468741 | ||
|
|
1ed4ae3d2c | ||
|
|
8095222cad | ||
|
|
d19af0923c | ||
|
|
08ce5a37bd | ||
|
|
9d5e5cc7d0 | ||
|
|
78c7667d96 | ||
|
|
bef3389874 | ||
|
|
985937c227 | ||
|
|
c82d7347e2 | ||
|
|
22be4b79f0 | ||
|
|
be84edf0fd | ||
|
|
ff3e787a03 | ||
|
|
093663c4e6 | ||
|
|
1b2d6df459 | ||
|
|
cd2e15cb29 | ||
|
|
06a2284145 | ||
|
|
f1571c0daa | ||
|
|
96e0fb83ef | ||
|
|
94cffbf717 | ||
|
|
38b2e907f9 | ||
|
|
7e5498751a | ||
|
|
eef9d63de2 | ||
|
|
d9d9b9d344 | ||
|
|
2ff3d8df56 | ||
|
|
462672bb6a | ||
|
|
84d539a4fa | ||
|
|
850b2d411d | ||
|
|
008efe6ba1 | ||
|
|
3bd248b9fe | ||
|
|
d73cfe7a02 | ||
|
|
6a6c09e283 | ||
|
|
1b8b56fd02 | ||
|
|
fef727983e | ||
|
|
40239e0463 | ||
|
|
9c579a9fd4 | ||
|
|
6d2279d44b | ||
|
|
a3a343bbb8 | ||
|
|
373d97d12d | ||
|
|
e1842fa9d5 | ||
|
|
4fd8ddef51 | ||
|
|
61b9ac8a39 | ||
|
|
728dd6eb26 | ||
|
|
742fd35d5f | ||
|
|
5e1126e896 | ||
|
|
e91719f902 | ||
|
|
0efa9bcb07 | ||
|
|
37d897d240 | ||
|
|
7488e1d97a | ||
|
|
bb169e3b33 | ||
|
|
3d735f8abd | ||
|
|
0a87e45901 | ||
|
|
6205189b14 | ||
|
|
9d7da4f0af | ||
|
|
241b24be43 | ||
|
|
aa77d12879 | ||
|
|
78f183a8e5 | ||
|
|
9f6fc8184e | ||
|
|
ec3bdcb1df | ||
|
|
30292650bc | ||
|
|
1e9bcf9b19 | ||
|
|
1e88979ced | ||
|
|
a9a973e336 | ||
|
|
1112fbaae2 | ||
|
|
1abe0bbbdb | ||
|
|
3644d16cb5 | ||
|
|
cbd82ee7c3 | ||
|
|
b66a59dba9 | ||
|
|
9dbc962dbc | ||
|
|
93e2558803 | ||
|
|
8cf7480833 | ||
|
|
08d655effc | ||
|
|
c1c867fca1 | ||
|
|
1378c00e1a | ||
|
|
85f17befbe | ||
|
|
f2e764f93a | ||
|
|
9adeeda490 | ||
|
|
68bf075172 | ||
|
|
7d23190939 | ||
|
|
5c68e2c78a | ||
|
|
980e14a96e | ||
|
|
d5fb45e4e3 | ||
|
|
cdb7ab1e0c | ||
|
|
da934e0bb3 | ||
|
|
06ea89d500 | ||
|
|
0868a90513 | ||
|
|
39fb295b02 | ||
|
|
85dbae7458 | ||
|
|
6ce2174e25 | ||
|
|
3ccaa1cd26 | ||
|
|
72fec6dc9e | ||
|
|
547e4ac2bd | ||
|
|
5fbdf7b9ab | ||
|
|
85bd4b0d96 | ||
|
|
dfce760521 | ||
|
|
30c97bf9aa | ||
|
|
2c8373adcf | ||
|
|
d08972254c | ||
|
|
a76d39e0cd | ||
|
|
a74d0dbb80 | ||
|
|
17ab24739d | ||
|
|
09f0ea3454 | ||
|
|
0dd2494ea0 | ||
|
|
502c6d010e | ||
|
|
57c1cda229 | ||
|
|
8c2b20b148 | ||
|
|
12e7f245ce | ||
|
|
671e36fa0f | ||
|
|
1ffd7eade6 | ||
|
|
703bbfc9e6 | ||
|
|
057afb0b2e | ||
|
|
94bd29c734 | ||
|
|
d078b7b143 | ||
|
|
acbeb98f4f | ||
|
|
cab8bf062d | ||
|
|
9e04a7fa33 | ||
|
|
c92d4847ca | ||
|
|
aceeaa08c2 | ||
|
|
35946d0c88 | ||
|
|
e864a91caa | ||
|
|
a5b9a0b1d5 | ||
|
|
412d625138 | ||
|
|
1ec20074e9 | ||
|
|
4d796f8422 | ||
|
|
1406d6c724 | ||
|
|
33441b95a5 | ||
|
|
8a3f312675 | ||
|
|
5c327428d6 | ||
|
|
4e4d31cad1 | ||
|
|
031fdf25a9 | ||
|
|
857cda1455 | ||
|
|
de5f3a5cfc | ||
|
|
2da7af522c | ||
|
|
712284e2f0 | ||
|
|
402c47a81b | ||
|
|
54a000a280 | ||
|
|
41fe118144 | ||
|
|
aaa6ff604f | ||
|
|
a350bb378f | ||
|
|
aa2212820e | ||
|
|
578e70119d | ||
|
|
e8796e00af | ||
|
|
735f27770a | ||
|
|
700a255157 | ||
|
|
ec06275c08 | ||
|
|
11d0c8bb53 | ||
|
|
535f24f1d1 | ||
|
|
172913d8ec | ||
|
|
0f99c54587 | ||
|
|
f89d70c5f6 | ||
|
|
0fc070be69 | ||
|
|
b93eaf530e | ||
|
|
76d8504651 | ||
|
|
06acd43ff9 | ||
|
|
870090c950 | ||
|
|
65777f10ab | ||
|
|
67d145de56 | ||
|
|
d45d1ed01a | ||
|
|
0d6c7bc0a5 | ||
|
|
15f013f710 | ||
|
|
a32e26a041 | ||
|
|
41c7e1f74e | ||
|
|
5a856e6e2b | ||
|
|
8ca16d447e | ||
|
|
b99549895b | ||
|
|
07cbaa4518 | ||
|
|
c78f5ff5a0 | ||
|
|
33946a6fa3 | ||
|
|
02dc67bb85 | ||
|
|
b5ae86b19c | ||
|
|
35567b967f | ||
|
|
c45718e670 | ||
|
|
74dcd027e0 | ||
|
|
c793d3fa50 | ||
|
|
04fdc8a9d1 | ||
|
|
fd702ff0bc | ||
|
|
f25bf71753 | ||
|
|
4c33873095 | ||
|
|
8dbcb5efc3 | ||
|
|
36426aafe0 | ||
|
|
21cf9314be | ||
|
|
4f2af7b90d | ||
|
|
06795dcf78 | ||
|
|
1241d0a4a7 | ||
|
|
f36723f435 | ||
|
|
e96854a6bc | ||
|
|
04c5a673da | ||
|
|
40e17ef5f1 | ||
|
|
39200aac12 | ||
|
|
43a405eec4 | ||
|
|
0206a4ddca | ||
|
|
34da7178f8 | ||
|
|
2726c47280 | ||
|
|
63cf25b3d6 | ||
|
|
eae13207e4 | ||
|
|
f65d100cd5 | ||
|
|
13e4ea1ac7 | ||
|
|
49788246c8 | ||
|
|
d4e849d588 | ||
|
|
2689d19928 | ||
|
|
968e9327f6 | ||
|
|
3cbb74b3b0 | ||
|
|
50fcfd6e30 | ||
|
|
ce0f27787c | ||
|
|
08ab7fd404 | ||
|
|
f138b61a45 | ||
|
|
0ad0a58d46 | ||
|
|
2d2415a801 | ||
|
|
b4cce1dba4 | ||
|
|
f80cdb05d5 | ||
|
|
f24f31259b | ||
|
|
3d13e6cee2 | ||
|
|
86915c2987 | ||
|
|
6411f9ff80 | ||
|
|
ada8d0f12c | ||
|
|
735456b6eb | ||
|
|
b46a528adb | ||
|
|
80f2343a60 | ||
|
|
e030fbe23e | ||
|
|
1ef0cb608c | ||
|
|
789a84e511 | ||
|
|
28620b6a23 | ||
|
|
01609513fb | ||
|
|
379a4ec417 | ||
|
|
8b462d5a1d | ||
|
|
d8a0039db3 | ||
|
|
dec8ea481c | ||
|
|
0c8b8388d0 | ||
|
|
d3a56710d6 | ||
|
|
f9f67d3309 | ||
|
|
66122ad957 | ||
|
|
95feaf46bf | ||
|
|
88f14e0329 | ||
|
|
5bf19af844 | ||
|
|
19e2a337cf | ||
|
|
0fd47cfcb1 | ||
|
|
e0548d9a65 | ||
|
|
259ed30dd5 | ||
|
|
2e028f0c0f | ||
|
|
1328c6ce03 | ||
|
|
28d898e780 | ||
|
|
daabb170f5 | ||
|
|
adc06623cd | ||
|
|
c4803e63dd | ||
|
|
0022d86035 | ||
|
|
172ad0ab28 | ||
|
|
bfabb5aa13 | ||
|
|
94d5a844cf | ||
|
|
75c528a430 | ||
|
|
36045455db | ||
|
|
bb12f96723 | ||
|
|
8151c99944 | ||
|
|
6408010aff | ||
|
|
c96c96217a | ||
|
|
93d179a81d | ||
|
|
0f6f2240fc | ||
|
|
74505c6c22 | ||
|
|
3dfb879101 | ||
|
|
dc711a820f | ||
|
|
44d4c650cc | ||
|
|
3121679205 | ||
|
|
9fe5e582b0 | ||
|
|
3e6b838757 | ||
|
|
59cf2cea2a | ||
|
|
3d7bcfecca | ||
|
|
da69ad1c3e | ||
|
|
6ff434fb47 | ||
|
|
a63c57a0b9 | ||
|
|
21c43adcb4 | ||
|
|
afa23666e2 | ||
|
|
245ad64426 | ||
|
|
9665971150 | ||
|
|
3b056c8f26 | ||
|
|
62b35e0237 | ||
|
|
11bc62f40e | ||
|
|
116c76d218 | ||
|
|
4dcedb537d | ||
|
|
09022c9c86 | ||
|
|
80cf98b11d | ||
|
|
644e750353 | ||
|
|
aecabfde43 | ||
|
|
686678a1fe | ||
|
|
7ac0b788c2 | ||
|
|
3791ee785d | ||
|
|
7341dee976 | ||
|
|
63a472a1ba | ||
|
|
df6b9e72c0 | ||
|
|
6ad4b9c8bb | ||
|
|
4fcc8dfe12 | ||
|
|
1da64c9cea | ||
|
|
9b6912be8d | ||
|
|
3beed9478c | ||
|
|
3715728fc6 | ||
|
|
5beee78cad | ||
|
|
32fe774e62 | ||
|
|
dead77b49c | ||
|
|
d7aa976eb1 | ||
|
|
6f9e89d616 | ||
|
|
bb3f6c0d5a | ||
|
|
620974ac59 | ||
|
|
6c78ff95e4 | ||
|
|
a814236bfa | ||
|
|
2bee030410 | ||
|
|
285ad59b08 | ||
|
|
f8b861cb39 | ||
|
|
f6a3aa6458 | ||
|
|
dd5f3e8e73 | ||
|
|
f50e32af02 | ||
|
|
345e083edd | ||
|
|
5660ddd05f | ||
|
|
c4360850af | ||
|
|
ce2123c88c | ||
|
|
520f33bf29 | ||
|
|
c620f1db7f | ||
|
|
d352e88d6f | ||
|
|
cc009b31f3 | ||
|
|
b5c189ab49 | ||
|
|
64f57277f2 | ||
|
|
af8ee60fe8 | ||
|
|
beb2e3cc2d | ||
|
|
0e2bcfd7bc | ||
|
|
417ffb23c2 | ||
|
|
a57d4cdf36 | ||
|
|
b8168c6b43 | ||
|
|
6026d2a658 | ||
|
|
a301f3e228 | ||
|
|
0b6bd68a24 | ||
|
|
e7fd390263 | ||
|
|
eab675795a | ||
|
|
8c2dc9a507 | ||
|
|
f6c0744efa | ||
|
|
8c7e810359 | ||
|
|
f0fd9e8ce6 | ||
|
|
c5579a4471 | ||
|
|
de7a65bf12 | ||
|
|
e4d0b3c5be | ||
|
|
dad029ab0d | ||
|
|
9c780799d1 | ||
|
|
27a33f9def | ||
|
|
f44cd029f9 | ||
|
|
9abb7629fb | ||
|
|
a161d85a63 | ||
|
|
9df9e6df4a | ||
|
|
29d465c13e | ||
|
|
622c94f2d9 | ||
|
|
f31a2789ca | ||
|
|
e7632e8d1b | ||
|
|
2f4f3de32a | ||
|
|
c9f49198c7 | ||
|
|
e2c12a0168 | ||
|
|
cb6e066512 | ||
|
|
1dd0421f96 | ||
|
|
64855f2e7a | ||
|
|
855ae7b3cf | ||
|
|
5f9a8605c9 | ||
|
|
567730a8d4 | ||
|
|
6b2164b88b | ||
|
|
930ea10409 | ||
|
|
24b703e262 | ||
|
|
152dd3efa0 | ||
|
|
ebc5bae55f | ||
|
|
88214c46a8 | ||
|
|
c9485a92e9 | ||
|
|
9df42c4d13 | ||
|
|
fc9a439ae8 | ||
|
|
e451153406 | ||
|
|
500e2fe481 | ||
|
|
a5884d93e7 | ||
|
|
d2e32cdd78 | ||
|
|
f66c6d4f65 | ||
|
|
fb85cc6db7 | ||
|
|
4e128ded36 | ||
|
|
f50388b141 | ||
|
|
4025a68fe1 | ||
|
|
7a05fa206f | ||
|
|
8594b06c3b | ||
|
|
17523574d8 | ||
|
|
7971797178 | ||
|
|
a8b0837e58 | ||
|
|
e419974844 | ||
|
|
773d0f6c15 | ||
|
|
afc4e0d8b4 | ||
|
|
5e86988ac4 | ||
|
|
2d28c18b1a | ||
|
|
b19764281a | ||
|
|
c25d63f781 | ||
|
|
d79efc412f | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'BUG: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Navigate to '...'
|
||||
2. Select/Press '....'
|
||||
3. Execute operation '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots/Photos**
|
||||
If applicable, add screenshots from the HMI display or photos of the machine screen to help explain your problem.
|
||||
|
||||
**Machine Information (please complete the following):**
|
||||
- Machine Type: [Winder2, Laser, Aquapath...]
|
||||
- Control Software Version: [e.g. 2.4.1]
|
||||
- Settings: [e.g. Different Gear Ratios, PID Settings, …]
|
||||
|
||||
**Device Information (if issue occurs on external device):**
|
||||
- Device Type: [e.g. Tablet, PLC, Remote Control Panel]
|
||||
- OS: [e.g. Windows 10, Linux, etc.]
|
||||
- Connection Type: [e.g. Ethernet, USB]
|
||||
|
||||
**Error Messages/Codes**
|
||||
If applicable, provide exact error codes or messages displayed or found in the troubleshooting terminal.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here (e.g., recent software updates, environmental factors).
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'Feat: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. During operation, I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions, workarounds, or features you've considered.
|
||||
|
||||
**Use Case/Application:**
|
||||
- Machine Type(s): [e.g. All machines, Winder, Aquapath, ...]
|
||||
- Frequency of Use: [e.g. Daily, During setup, Occasionally]
|
||||
|
||||
**Expected Benefits:**
|
||||
- Efficiency Improvement: [e.g. Reduces setup time by X minutes, Eliminates manual steps]
|
||||
- Safety Enhancement: [e.g. Prevents operator error, Adds safety checks]
|
||||
- Production Impact: [e.g. Increases throughput, Reduces downtime, Improves quality]
|
||||
- User Experience: [e.g. Simplifies workflow, Reduces training time]
|
||||
|
||||
**Mockups/Sketches**
|
||||
If applicable, add sketches, mockups, or photos showing how the feature would appear on the HMI display or integrate with the machine.
|
||||
|
||||
**Additional context**
|
||||
Add any other context, industry standards, or examples from other control systems about the feature request here.
|
||||
@@ -0,0 +1,14 @@
|
||||
## What does this PR do / add / change?
|
||||
|
||||
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
|
||||
|
||||
## Checklist before opening the pr
|
||||
|
||||
- [ ] Tested locally
|
||||
- [ ] Tested on the machine (if hardware interaction is involved)
|
||||
- [ ] No format errors (`cargo fmt` / `npm run format`)
|
||||
- [ ] No build errors (`cargo build` / `npm run build`)
|
||||
@@ -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
|
||||
@@ -3,50 +3,76 @@ name: Electron
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
jobs:
|
||||
build-electron:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
- &checkout
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &setup-node
|
||||
name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18" # or '20' for latest LTS
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
electron/package-lock.json
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- &install
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- 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
|
||||
- name: Compile
|
||||
run: tsc
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Package
|
||||
run: npm run package
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
@@ -0,0 +1,213 @@
|
||||
name: Stale Assigned Issue Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Sunday at 9:00 AM UTC
|
||||
- cron: '0 9 * * 0'
|
||||
workflow_dispatch: # Allow manual triggering for testing
|
||||
|
||||
jobs:
|
||||
check-stale-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Check assigned issues for status updates
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
|
||||
// Helper function to check rate limits
|
||||
async function checkRateLimit() {
|
||||
try {
|
||||
const { data: rateLimit } = await github.rest.rateLimit.get();
|
||||
const remaining = rateLimit.resources.core.remaining;
|
||||
const reset = new Date(rateLimit.resources.core.reset * 1000);
|
||||
console.log(`Rate limit: ${remaining} requests remaining (resets at ${reset.toISOString()})`);
|
||||
return remaining > 10; // Stop if fewer than 10 requests remaining
|
||||
} catch (error) {
|
||||
console.log(`Could not check rate limit: ${error.message}`);
|
||||
return true; // Continue on error
|
||||
}
|
||||
}
|
||||
|
||||
// Get all open issues
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
console.log(`Found ${issues.length} open issues`);
|
||||
|
||||
// Helper function to check if issue has linked PRs using GraphQL
|
||||
async function hasLinkedPRs(issueNumber) {
|
||||
try {
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $issueNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $issueNumber) {
|
||||
timelineItems(first: 100, itemTypes: CONNECTED_EVENT) {
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
subject {
|
||||
... on PullRequest {
|
||||
number
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await github.graphql(query, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issueNumber: issueNumber
|
||||
});
|
||||
|
||||
const linkedPRs = result.repository.issue.timelineItems.nodes
|
||||
.filter(node => node.subject && node.subject.state === 'OPEN')
|
||||
.map(node => node.subject.number);
|
||||
|
||||
return linkedPRs.length > 0;
|
||||
} catch (error) {
|
||||
console.log(`Could not check linked PRs for issue #${issueNumber}: ${error.message}`);
|
||||
return false; // Assume no linked PRs on error to avoid false positives
|
||||
}
|
||||
}
|
||||
|
||||
let processedCount = 0;
|
||||
let commentedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const issue of issues) {
|
||||
try {
|
||||
// Check rate limit periodically
|
||||
if (processedCount % 10 === 0 && !(await checkRateLimit())) {
|
||||
console.log('Rate limit nearly exhausted, stopping to avoid hitting limit');
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip pull requests (they appear in issues list too)
|
||||
if (issue.pull_request) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not assigned
|
||||
if (!issue.assignees || issue.assignees.length === 0) {
|
||||
console.log(`Issue #${issue.number}: Not assigned, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Checking issue #${issue.number}: ${issue.title}`);
|
||||
processedCount++;
|
||||
|
||||
// Check if issue has linked PRs
|
||||
const hasOpenLinkedPR = await hasLinkedPRs(issue.number);
|
||||
|
||||
if (hasOpenLinkedPR) {
|
||||
console.log(`Issue #${issue.number}: Has linked open PR, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all comments on the issue
|
||||
let comments = [];
|
||||
try {
|
||||
comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100
|
||||
});
|
||||
} catch (commentError) {
|
||||
console.error(`Issue #${issue.number}: Could not fetch comments - ${commentError.message}`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the most recent comment or update
|
||||
let lastActivity = new Date(issue.created_at);
|
||||
|
||||
if (issue.updated_at) {
|
||||
lastActivity = new Date(issue.updated_at);
|
||||
}
|
||||
|
||||
if (comments.length > 0) {
|
||||
const lastComment = comments[comments.length - 1];
|
||||
const lastCommentDate = new Date(lastComment.created_at);
|
||||
if (lastCommentDate > lastActivity) {
|
||||
lastActivity = lastCommentDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if last activity was more than a week ago
|
||||
const daysSinceActivity = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysSinceActivity >= 7) {
|
||||
console.log(`Issue #${issue.number}: No activity for ${daysSinceActivity} days`);
|
||||
|
||||
// Check if we already posted a status request in the last 7 days
|
||||
const recentComments = comments.filter(comment => {
|
||||
const commentDate = new Date(comment.created_at);
|
||||
return (now - commentDate) < ONE_WEEK_MS;
|
||||
});
|
||||
|
||||
const alreadyAsked = recentComments.some(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('status update')
|
||||
);
|
||||
|
||||
if (alreadyAsked) {
|
||||
console.log(`Issue #${issue.number}: Already requested status this week, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Post a status request comment
|
||||
try {
|
||||
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `Hi ${assigneeNames}! 👋\n\n` +
|
||||
`This issue has been assigned to you for ${daysSinceActivity} days with no recent activity or linked pull requests.\n\n` +
|
||||
`Could you please provide a status update? If you're:\n` +
|
||||
`- ✅ Working on it: Let us know your progress\n` +
|
||||
`- 🔄 Need help: Tag someone or describe what's blocking you\n` +
|
||||
`- 🚫 No longer working on it: Let us know so we can reassign\n\n` +
|
||||
`Thank you!`
|
||||
});
|
||||
|
||||
console.log(`Issue #${issue.number}: Posted status request comment`);
|
||||
commentedCount++;
|
||||
} catch (commentError) {
|
||||
console.error(`Issue #${issue.number}: Could not post comment - ${commentError.message}`);
|
||||
errorCount++;
|
||||
// Continue to next issue
|
||||
}
|
||||
} else {
|
||||
console.log(`Issue #${issue.number}: Activity ${daysSinceActivity} days ago, still fresh`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Issue #${issue.number}: Error processing - ${error.message}`);
|
||||
errorCount++;
|
||||
// Continue to next issue instead of failing the entire workflow
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nStale issue check complete!`);
|
||||
console.log(`Processed: ${processedCount} issues`);
|
||||
console.log(`Commented on: ${commentedCount} issues`);
|
||||
console.log(`Errors: ${errorCount}`);
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Nix CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
jobs:
|
||||
electron:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- &checkout
|
||||
name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &nix-install
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-electron-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './electron/package-lock.json') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
purge: true
|
||||
purge-prefixes: nix-electron-${{ runner.os }}-
|
||||
|
||||
- name: Build Electron in Nix
|
||||
run: nix build ".#packages.x86_64-linux.electron"
|
||||
|
||||
rust:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-server-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
purge: true
|
||||
purge-prefixes: nix-server-${{ runner.os }}-
|
||||
|
||||
- name: Build Server in Nix
|
||||
run: nix build ".#packages.x86_64-linux.server"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [electron, rust]
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Use both Electron and Server Nix Store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock', './electron/package-lock.json') }}
|
||||
|
||||
# if no hit on the primary key, merge individual caches
|
||||
restore-prefixes-all-matches: |
|
||||
nix-electron-${{ runner.os }}-
|
||||
nix-server-${{ runner.os }}-
|
||||
|
||||
- name: Build Flake and System Configuration
|
||||
run: nix build ".#nixosConfigurations.nixos.config.system.build.toplevel" --impure
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Format
|
||||
run: nix run 'nixpkgs#nixfmt-classic' $(find . nixos -name '*.nix')
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Build Flake and Configuration.nix
|
||||
run: nix flake check "." --impure
|
||||
@@ -3,27 +3,41 @@ name: Rust
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches: "*"
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_VERSION: "1.86"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install libudev development package
|
||||
- &udev-install
|
||||
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
|
||||
|
||||
- &checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &setup-rust
|
||||
name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
profile: minimal
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- &cargo-cache
|
||||
name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
@@ -41,8 +55,36 @@ jobs:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
run: cargo build --verbose
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --check --verbose
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *udev-install
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
- *cargo-cache
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
mock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *udev-install
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
- *cargo-cache
|
||||
|
||||
- name: Mock Machine
|
||||
run: cargo build --features "development-build,mock-machine"
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Update npm deps hash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'electron/package-lock.json'
|
||||
- 'electron/package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
update-hash:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Calculate new npm deps hash
|
||||
id: calc-hash
|
||||
run: |
|
||||
# Calculate the hash using nix run with nixpkgs
|
||||
NEW_HASH=$(nix run 'nixpkgs#prefetch-npm-deps' electron/package-lock.json)
|
||||
echo "hash=$NEW_HASH" >> $GITHUB_OUTPUT
|
||||
echo "Calculated hash: $NEW_HASH"
|
||||
|
||||
- name: Update npmDepsHash in electron.nix
|
||||
run: |
|
||||
NEW_HASH="${{ steps.calc-hash.outputs.hash }}"
|
||||
sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$NEW_HASH\";|" nixos/packages/electron.nix
|
||||
|
||||
- name: Check if hash changed
|
||||
id: check-changes
|
||||
run: |
|
||||
if git diff --quiet nixos/packages/electron.nix; then
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes needed - hash is already correct"
|
||||
else
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "Hash updated in electron.nix"
|
||||
fi
|
||||
|
||||
- name: Commit and push if changed
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add nixos/packages/electron.nix
|
||||
git commit -m "chore: update npmDepsHash to ${{ steps.calc-hash.outputs.hash }}"
|
||||
git push
|
||||
|
||||
- name: Comment on PR
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Automatically updated `npmDepsHash` to `${{ steps.calc-hash.outputs.hash }}`\n\n**Next step:** Trigger the checks by pushing an empty commit:\n```bash\ngit commit --allow-empty -m "trigger ci" && git push\n```'
|
||||
})
|
||||
@@ -1,5 +1,11 @@
|
||||
target/
|
||||
dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
shell.nix
|
||||
node_modules/
|
||||
runtime_metrics.csv
|
||||
compile_metrics.csv
|
||||
.idea/
|
||||
electron/dist-electron/
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
}
|
||||
@@ -1,36 +1,210 @@
|
||||
# `2.4.0`
|
||||
_14.08.2025_
|
||||
# `2.14.1`
|
||||
_26.01.2026_
|
||||
|
||||
## Laser
|
||||
-[#1079](https://github.com/qitechgmbh/control/pull/1079) Fixed Overflowing of buffer for Graphs causing crashes
|
||||
|
||||
# `2.14.0`
|
||||
_22.01.2026_
|
||||
|
||||
## General
|
||||
- [#1040](https://github.com/qitechgmbh/control/pull/1040) Added API for reading Machine Data
|
||||
|
||||
# `2.13.0`
|
||||
_07.01.2026_
|
||||
|
||||
## General
|
||||
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
|
||||
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
|
||||
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
|
||||
|
||||
## Extruder V3
|
||||
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
|
||||
|
||||
## Laser
|
||||
- [#924](https://github.com/qitechgmbh/control/pull/924) Roundness is now displayed as a percentage
|
||||
- [#885](https://github.com/qitechgmbh/control/pull/885) Added Min and Max Diameter
|
||||
- [#849](https://github.com/qitechgmbh/control/pull/849) Added a Warning if Filament is out of Spec
|
||||
|
||||
## Aquapath
|
||||
- [#976](https://github.com/qitechgmbh/control/pull/976) Improved Stability
|
||||
|
||||
|
||||
# `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
|
||||
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
|
||||
|
||||
- [#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
|
||||
- [#568](https://github.com/qitechgmbh/control/pull/569) switched to a Realtime Linuxkernel on nixos
|
||||
- [#574](https://github.com/qitechgmbh/control/pull/587) isolated cores 2 and 3 on nixos to guarentee strict timings for ethercat
|
||||
- [#588](https://github.com/qitechgmbh/control/pull/589) limited frontend to 30 fps to reduce cpu usage
|
||||
- [#572](https://github.com/qitechgmbh/control/pull/572) switched to io_uring to reduce latency
|
||||
- [#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
|
||||
## 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_
|
||||
@@ -43,6 +217,7 @@ _15.07.2025_
|
||||
- [#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
|
||||
|
||||
@@ -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 }
|
||||
@@ -2,150 +2,76 @@
|
||||
|
||||
# QiTech Control
|
||||
|
||||
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
|
||||
QiTech Control is an open-source framework designed to bring modern software development practices to certified industrial hardware.
|
||||
|
||||
[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
|
||||
*Click here to watch a video demo of our software.*
|
||||
It frees developers from proprietary, license-heavy PLC ecosystems and rigid "point-and-click" workflows that are no longer adequate for today's complex automation challenges.
|
||||
|
||||
# Repo Structure
|
||||
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
|
||||
|
||||
Frontend
|
||||
## Documentation
|
||||
|
||||
- `/electron`: Frontend code for the control software built with React and Electron.
|
||||
**[View Full Documentation Wiki](https://github.com/qitechgmbh/control/wiki)**
|
||||
|
||||
Backend
|
||||
- [Getting Started](https://github.com/qitechgmbh/control/wiki/Getting-Started)
|
||||
- [Architecture Overview](https://github.com/qitechgmbh/control/wiki/Architecture-Overview)
|
||||
- [EtherCAT Basics](https://github.com/qitechgmbh/control/wiki/Ethercat-Basics)
|
||||
- [Device Examples](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)
|
||||
- [REST API Reference](https://github.com/qitechgmbh/control/wiki/Rest-Api)
|
||||
|
||||
- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
|
||||
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
|
||||
- `/ethercat-hal-derive`: Macros for `ethercat-hal`
|
||||
- `/control-core`: Core control logic for the server.
|
||||
## Videos
|
||||
|
||||
Operating System
|
||||
| Software Demo | Full Explainer |
|
||||
|---------------|----------------|
|
||||
| [](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Watch a demo of the control software in action* | [](https://youtu.be/55egCAkQgyM)<br/>*Watch a complete overview of QiTech Control* |
|
||||
|
||||
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
|
||||
## Repository Structure
|
||||
|
||||
Other
|
||||
- **`/electron`** - React + Electron frontend
|
||||
- **`/server`** - Rust backend implementing machine logic
|
||||
- **`/ethercat-hal`** - Hardware abstraction layer for EtherCAT devices
|
||||
- **`/control-core`** - Core control logic
|
||||
- **`/nixos`** - Custom Linux OS with realtime kernel
|
||||
|
||||
- `/docs`: Documentation for the project.
|
||||
## Technology Stack
|
||||
|
||||
# Technology Choices
|
||||
**Backend:** Rust with [Ethercrab](https://github.com/ethercrab-rs/ethercrab) for EtherCAT, [SocketIO](https://socket.io/) for real-time communication, [axum](https://docs.rs/axum/latest/axum/) for REST API
|
||||
|
||||
## Backend
|
||||
**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
|
||||
|
||||
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.
|
||||
|
||||
[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.
|
||||
## Hardware Examples
|
||||
|
||||
We use [Smol](https://github.com/smol-rs/smol) for EtherCAT IO in the control loop for it's performance and [Tokio](https://tokio.rs/) for server IO because of it's ecosystem and maturity.
|
||||
QiTech Control supports a wide range of EtherCAT hardware from WAGO and Beckhoff. We provide complete step-by-step tutorials with wiring diagrams and software configuration for many common modules.
|
||||
|
||||
## Frontend
|
||||
**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
|
||||
|
||||
We could combine the code of the frontend and backend using [Doxius](https://dioxuslabs.com/) but it lacks good Linux support. We chose [Electron](https://www.electronjs.org/) with [React](https://react.dev/) for it's maturity and ecosystem. For the UI we use [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) for styling. For routing we use [TanStack Router](https://tanstack.com/router/v1).
|
||||
Quick links to popular examples:
|
||||
- [EL2004 - LED Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El2004) (simplest setup for beginners)
|
||||
- [EL3021 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El3021)
|
||||
- [EL7031 - Stepper Motor Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El7031-Motor)
|
||||
- [WAGO 750-402 - Digital Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago750-402)
|
||||
- [WAGO 750-455 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago-750-455)
|
||||
|
||||
# Dev Setup
|
||||
## QiTech Machines
|
||||
|
||||
[Developer Documentation](./docs/developer-docs/)
|
||||
QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
|
||||
|
||||
## Backend
|
||||
**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
|
||||
|
||||
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
|
||||
- `rust-analyzer` extension for VSCode
|
||||
- Set your interface in `server/src/ethercat/init.rs` like `en10`
|
||||
- Connect a Beckhoff EK1100 to your interface
|
||||
- run `cd server && cargo run` to start the server (localhost:3001)
|
||||
|
||||
## Frontend
|
||||
## Contributing
|
||||
|
||||
- nodejs and npm installed
|
||||
- run `cd electron && npm i && npm run start` to start the frontend
|
||||
This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
|
||||
|
||||
# Machines
|
||||
|
||||
| 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) |
|
||||
| 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 | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
|
||||
|
||||
# More Docs
|
||||
## Additional Videos
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
| Episode Highlights | |
|
||||
|--------------------|--------------------|
|
||||
| [](https://www.youtube.com/watch?v=U2wNN6sF4to&t=163s)<br/>**Day in the Software Team**<br/>*Behind the scenes with Robin and how the team ships control software.* | [](https://www.youtube.com/watch?v=R5WGAY3WRWA&t=1s)<br/>**Software Stack Day**<br/>*A fast tour through the tools, stack, and daily workflow.* |
|
||||
| [](https://www.youtube.com/watch?v=xyaHwxXguw4&t=32s)<br/>**First Client**<br/>*A milestone week building the first real client project.* | [](https://www.youtube.com/watch?v=moI7z2PVdF4)<br/>**Steelworks Week**<br/>*A deep-dive week on a demanding steelworks order.* |
|
||||
|
||||
- [x] Example Winder V2
|
||||
## License
|
||||
|
||||
- [ ] Electron
|
||||
|
||||
- Folder Structure
|
||||
- Routing with TanStack Router
|
||||
- Design with Tailwind & Shadcn
|
||||
- ...
|
||||
|
||||
- [ ] Interfacing with Electron/Server
|
||||
|
||||
- [ ] SocketIO
|
||||
- Machine Namespace
|
||||
- Main Namespace
|
||||
- [ ] REST
|
||||
- Machine Mutations
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
- Maindevice
|
||||
- Group
|
||||
- Extracting Device Identifications
|
||||
- Identifying Groups
|
||||
- Validating Machines
|
||||
- Run Control Loop
|
||||
- [x] [Control Loop](./docs/control-loop.md)
|
||||
- [x] [Machine/Device Identification](./docs/identification.md)
|
||||
- [ ] Machines
|
||||
- When to create a new Machine?
|
||||
- Versioning
|
||||
- Code sharing
|
||||
- Creating/Validating a Machine
|
||||
- Validation
|
||||
- Configuration
|
||||
- [ ] Machine Implementation Guide
|
||||
- Link: How to create a Device
|
||||
- Link: How to create an Actor
|
||||
- Link: How to create a Machine
|
||||
- API (SocketIO + REST)
|
||||
- Creation/Validation Logic
|
||||
- Optional/Mandatory Devices
|
||||
- Validate Devices
|
||||
- Business Logic
|
||||
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
|
||||
- Forward `act` in winder.
|
||||
|
||||
- [ ] Control Core
|
||||
|
||||
- [x] [Actors](./docs/actors.md)
|
||||
- [ ] SocketIO
|
||||
- Namespaces & Caching
|
||||
- Joining leaving namespaces
|
||||
- NamespaceId
|
||||
- Caching
|
||||
- Serverside Caching
|
||||
- Clientside Caching
|
||||
- [ ] REST
|
||||
|
||||
- [x] Ethercat HAL
|
||||
|
||||
- [x] [Devices](./docs/devices.md)
|
||||
- [x] [Configuration (CoE)](./docs/coe.md)
|
||||
- [x] [IO](./docs/io.md)
|
||||
- [x] [PDO](./docs/pdo.md)
|
||||
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
See [LICENSE](LICENSE) file for details.
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
FEATURE=""
|
||||
|
||||
# Check if "mock-machine" was passed as an argument
|
||||
@@ -7,19 +9,25 @@ 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 $FEATURE
|
||||
cargo build $FEATURE --features development-build
|
||||
fi
|
||||
|
||||
# Only setcap if not building with mock-machine
|
||||
if [ "$1" != "mock-machine" ]; then
|
||||
echo "Setting capabilities for server executable"
|
||||
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
|
||||
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
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_DIR="${BUILD_DIR:-target}"
|
||||
CSV_FILE="${CSV_FILE:-compile_metrics.csv}"
|
||||
DEFAULT_PKG="server"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- cargo build
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "cargo" ] && [ "${2:-}" = "build" ]; then
|
||||
cargo clean
|
||||
BUILD_CMD=(cargo rustc -p "$DEFAULT_PKG")
|
||||
shift 2
|
||||
BUILD_CMD+=("$@")
|
||||
BUILD_CMD+=(-- --emit=llvm-ir -C debuginfo=0)
|
||||
else
|
||||
BUILD_CMD=("$@")
|
||||
fi
|
||||
|
||||
compile_start_ns=$(date +%s%N)
|
||||
"${BUILD_CMD[@]}"
|
||||
compile_end_ns=$(date +%s%N)
|
||||
compile_ms=$(( (compile_end_ns - compile_start_ns) / 1000000 ))
|
||||
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
llvm_ir_lines=$(
|
||||
find "$BUILD_DIR" -type f -name '*.ll' -print0 \
|
||||
| xargs -0 cat \
|
||||
| wc -l
|
||||
)
|
||||
else
|
||||
llvm_ir_lines=0
|
||||
fi
|
||||
|
||||
bin_path=""
|
||||
|
||||
if [ -n "${BINARY_PATH:-}" ] && [ -f "$BINARY_PATH" ]; then
|
||||
bin_path="$BINARY_PATH"
|
||||
elif [ -f "./target/release/server" ]; then
|
||||
bin_path="./target/release/server"
|
||||
elif [ -f "./target/debug/server" ]; then
|
||||
bin_path="./target/debug/server"
|
||||
elif [ -f "./server/target/release/server" ]; then
|
||||
bin_path="./server/target/release/server"
|
||||
elif [ -f "./server/target/debug/server" ]; then
|
||||
bin_path="./server/target/debug/server"
|
||||
fi
|
||||
|
||||
if [ -n "$bin_path" ]; then
|
||||
binary_size_bytes=$(stat -c '%s' "$bin_path")
|
||||
else
|
||||
binary_size_bytes=0
|
||||
fi
|
||||
|
||||
finished_timestamp_ms=$(($(date +%s%N) / 1000000))
|
||||
finished_datetime=$(date -Iseconds)
|
||||
|
||||
if [ ! -f "$CSV_FILE" ]; then
|
||||
echo "datetime,finished_timestamp_ms,compile_ms,llvm_ir_lines,binary_size_bytes" > "$CSV_FILE"
|
||||
fi
|
||||
|
||||
echo "${finished_datetime},${finished_timestamp_ms},${compile_ms},${llvm_ir_lines},${binary_size_bytes}" \
|
||||
>> "$CSV_FILE"
|
||||
|
||||
echo "Metrics recorded in $CSV_FILE"
|
||||
@@ -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"] }
|
||||
@@ -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)
|
||||
}
|
||||
@@ -3,28 +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.8"
|
||||
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
ethercrab = "0.6"
|
||||
units = { path = "../units" }
|
||||
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
|
||||
interfaces = "0.0.9"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serialport = "4.7.2"
|
||||
serialport = "4.7.3"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.2"
|
||||
uom = "0.36.0"
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
serial = "0.4.0"
|
||||
tracing = "0.1.41"
|
||||
erased-serde = "0.4.6"
|
||||
tracing = "0.1.44"
|
||||
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.175"
|
||||
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"]
|
||||
@@ -33,7 +33,7 @@ pub struct ClampingTimeagnosticPidController {
|
||||
}
|
||||
|
||||
impl ClampingTimeagnosticPidController {
|
||||
pub fn new(
|
||||
pub const fn new(
|
||||
kp: f64,
|
||||
ki: f64,
|
||||
kd: f64,
|
||||
@@ -65,7 +65,7 @@ impl ClampingTimeagnosticPidController {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
|
||||
pub const fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
@@ -85,26 +85,26 @@ impl ClampingTimeagnosticPidController {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kp(&self) -> f64 {
|
||||
pub const fn get_kp(&self) -> f64 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub fn get_ki(&self) -> f64 {
|
||||
pub const fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub fn get_kd(&self) -> f64 {
|
||||
pub const fn get_kd(&self) -> f64 {
|
||||
self.kd
|
||||
}
|
||||
|
||||
pub fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
|
||||
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
|
||||
self.reset();
|
||||
self.kp = kp;
|
||||
self.ki = ki;
|
||||
self.kd = kd;
|
||||
}
|
||||
|
||||
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
|
||||
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),
|
||||
@@ -114,7 +114,7 @@ impl ClampingTimeagnosticPidController {
|
||||
}
|
||||
|
||||
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
|
||||
let signal = match self.last {
|
||||
match self.last {
|
||||
// First update
|
||||
None => {
|
||||
// Calculate error
|
||||
@@ -122,15 +122,10 @@ impl ClampingTimeagnosticPidController {
|
||||
|
||||
// Calculate signal
|
||||
let signal = self.kp * ep;
|
||||
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
|
||||
signal,
|
||||
self.min_signal,
|
||||
self.max_signal,
|
||||
);
|
||||
let clamped_signal = Self::optional_clamp(signal, self.min_signal, self.max_signal);
|
||||
|
||||
// Set values
|
||||
self.ep =
|
||||
ClampingTimeagnosticPidController::optional_clamp(ep, self.min_ep, self.max_ep);
|
||||
self.ep = Self::optional_clamp(ep, self.min_ep, self.max_ep);
|
||||
self.ei = 0.0;
|
||||
self.ed = 0.0;
|
||||
self.last = Some(t);
|
||||
@@ -143,23 +138,11 @@ impl ClampingTimeagnosticPidController {
|
||||
let dt = t.duration_since(last).as_secs_f64();
|
||||
|
||||
// Calculate errors
|
||||
let ep = ClampingTimeagnosticPidController::optional_clamp(
|
||||
error,
|
||||
self.min_ep,
|
||||
self.max_ep,
|
||||
);
|
||||
let ep = Self::optional_clamp(error, self.min_ep, self.max_ep);
|
||||
|
||||
let ei = ClampingTimeagnosticPidController::optional_clamp(
|
||||
self.ei + ep * dt,
|
||||
self.min_ei,
|
||||
self.max_ei,
|
||||
);
|
||||
let ei = Self::optional_clamp(ep.mul_add(dt, self.ei), self.min_ei, self.max_ei);
|
||||
|
||||
let ed = ClampingTimeagnosticPidController::optional_clamp(
|
||||
(ep - self.ep) / dt,
|
||||
self.min_ed,
|
||||
self.max_ed,
|
||||
);
|
||||
let ed = Self::optional_clamp((ep - self.ep) / dt, self.min_ed, self.max_ed);
|
||||
|
||||
// Make factors timeagnostic
|
||||
let kp = self.kp * dt;
|
||||
@@ -167,12 +150,8 @@ impl ClampingTimeagnosticPidController {
|
||||
let kd = self.kd * dt;
|
||||
|
||||
// Calculate signal
|
||||
let signal = kp * ep + ki * ei + kd * ed;
|
||||
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
|
||||
signal,
|
||||
self.min_signal,
|
||||
self.max_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;
|
||||
@@ -182,12 +161,10 @@ impl ClampingTimeagnosticPidController {
|
||||
|
||||
clamped_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;
|
||||
|
||||
@@ -22,7 +22,7 @@ pub struct AccelerationSpeedController {
|
||||
}
|
||||
|
||||
impl AccelerationSpeedController {
|
||||
pub fn new(
|
||||
pub const fn new(
|
||||
min_speed: Option<f64>,
|
||||
max_speed: Option<f64>,
|
||||
min_acceleration: f64,
|
||||
@@ -98,16 +98,16 @@ impl AccelerationSpeedController {
|
||||
new_speed
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, initial_speed: f64) {
|
||||
pub const fn reset(&mut self, initial_speed: f64) {
|
||||
self.last_speed = initial_speed;
|
||||
self.last_t = None; // Reset the last update time
|
||||
}
|
||||
|
||||
pub fn set_max_acceleration(&mut self, acceleration: f64) {
|
||||
pub const fn set_max_acceleration(&mut self, acceleration: f64) {
|
||||
self.max_acceleration = acceleration;
|
||||
}
|
||||
|
||||
fn apply_speed_limits(&self, speed: f64) -> f64 {
|
||||
const fn apply_speed_limits(&self, speed: f64) -> f64 {
|
||||
let mut limited_speed = speed;
|
||||
|
||||
if let Some(min) = self.min_speed {
|
||||
@@ -121,23 +121,23 @@ impl AccelerationSpeedController {
|
||||
limited_speed
|
||||
}
|
||||
|
||||
pub fn get_min_speed(&self) -> Option<f64> {
|
||||
pub const fn get_min_speed(&self) -> Option<f64> {
|
||||
self.min_speed
|
||||
}
|
||||
|
||||
pub fn get_max_speed(&self) -> Option<f64> {
|
||||
pub const fn get_max_speed(&self) -> Option<f64> {
|
||||
self.max_speed
|
||||
}
|
||||
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<f64>) {
|
||||
pub const fn set_min_speed(&mut self, min_speed: Option<f64>) {
|
||||
self.min_speed = min_speed;
|
||||
}
|
||||
|
||||
pub fn set_max_speed(&mut self, max_speed: Option<f64>) {
|
||||
pub const fn set_max_speed(&mut self, max_speed: Option<f64>) {
|
||||
self.max_speed = max_speed;
|
||||
}
|
||||
|
||||
pub fn set_min_acceleration(&mut self, deceleration: f64) {
|
||||
pub const fn set_min_acceleration(&mut self, deceleration: f64) {
|
||||
self.min_acceleration = deceleration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{AngularAcceleration, AngularVelocity},
|
||||
@@ -53,7 +53,7 @@ impl AngularAccelerationSpeedController {
|
||||
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);
|
||||
return AngularVelocity::new::<radian_per_second>(new_speed);
|
||||
AngularVelocity::new::<radian_per_second>(new_speed)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, initial_speed: AngularVelocity) {
|
||||
@@ -73,13 +73,13 @@ impl AngularAccelerationSpeedController {
|
||||
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(|s| AngularVelocity::new::<radian_per_second>(s))
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(|s| AngularVelocity::new::<radian_per_second>(s))
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<AngularVelocity>) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Velocity},
|
||||
velocity::meter_per_second,
|
||||
@@ -48,7 +48,7 @@ impl LinearAccelerationLimitingController {
|
||||
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);
|
||||
return Velocity::new::<meter_per_second>(new_speed);
|
||||
Velocity::new::<meter_per_second>(new_speed)
|
||||
}
|
||||
pub fn reset(&mut self, initial_speed: Velocity) {
|
||||
let initial_speed = initial_speed.get::<meter_per_second>();
|
||||
@@ -65,12 +65,12 @@ impl LinearAccelerationLimitingController {
|
||||
pub fn get_min_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(|s| Velocity::new::<meter_per_second>(s))
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
pub fn get_max_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(|s| Velocity::new::<meter_per_second>(s))
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<Velocity>) {
|
||||
self.controller
|
||||
|
||||
@@ -21,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,
|
||||
@@ -33,27 +33,27 @@ impl PidController {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
|
||||
pub const fn configure(&mut self, ki: f64, kp: f64, kd: f64) {
|
||||
self.reset();
|
||||
self.kp = kp;
|
||||
self.ki = ki;
|
||||
self.kd = kd;
|
||||
}
|
||||
|
||||
pub fn get_kp(&self) -> f64 {
|
||||
pub const fn get_kp(&self) -> f64 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub fn get_ki(&self) -> f64 {
|
||||
pub const fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub fn get_kd(&self) -> f64 {
|
||||
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
|
||||
@@ -78,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;
|
||||
@@ -92,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;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
// Remove the anyhow::Ok import as it conflicts with std::result::Ok
|
||||
|
||||
/// Errors that can occur when creating or using the motion controller
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MotionControllerError {
|
||||
InvalidSpeedLimits,
|
||||
InvalidAccelerationLimits,
|
||||
@@ -14,25 +14,25 @@ pub enum MotionControllerError {
|
||||
impl fmt::Display for MotionControllerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
MotionControllerError::InvalidSpeedLimits => {
|
||||
Self::InvalidSpeedLimits => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid speed limits: min_speed must be ≤ 0 and max_speed must be ≥ 0"
|
||||
)
|
||||
}
|
||||
MotionControllerError::InvalidAccelerationLimits => {
|
||||
Self::InvalidAccelerationLimits => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid acceleration limits: min_acceleration must be < 0 and max_acceleration must be > 0"
|
||||
)
|
||||
}
|
||||
MotionControllerError::InvalidPositionLimits => {
|
||||
Self::InvalidPositionLimits => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid position limits: min_position must be ≤ max_position"
|
||||
)
|
||||
}
|
||||
MotionControllerError::ZeroDeceleration => {
|
||||
Self::ZeroDeceleration => {
|
||||
write!(
|
||||
f,
|
||||
"Zero deceleration rate: cannot plan motion without deceleration capability"
|
||||
@@ -148,7 +148,7 @@ struct MotionConstants {
|
||||
}
|
||||
|
||||
/// Represents the current phase of motion
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MotionPhase {
|
||||
Idle,
|
||||
IncreasingSpeed,
|
||||
@@ -170,7 +170,7 @@ pub struct ControllerBuilder {
|
||||
|
||||
impl ControllerBuilder {
|
||||
/// Create a new builder
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
min_speed: None,
|
||||
max_speed: None,
|
||||
@@ -184,28 +184,36 @@ impl ControllerBuilder {
|
||||
}
|
||||
|
||||
/// Set speed limits (min_speed ≤ 0, max_speed ≥ 0)
|
||||
pub fn speed_limits(mut self, min_speed: f64, max_speed: f64) -> Self {
|
||||
pub const fn speed_limits(mut self, min_speed: f64, max_speed: f64) -> Self {
|
||||
self.min_speed = Some(min_speed);
|
||||
self.max_speed = Some(max_speed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set acceleration limits (min_acceleration < 0, max_acceleration > 0)
|
||||
pub fn acceleration_limits(mut self, min_acceleration: f64, max_acceleration: f64) -> Self {
|
||||
pub const fn acceleration_limits(
|
||||
mut self,
|
||||
min_acceleration: f64,
|
||||
max_acceleration: f64,
|
||||
) -> Self {
|
||||
self.min_acceleration = Some(min_acceleration);
|
||||
self.max_acceleration = Some(max_acceleration);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set position limits (optional)
|
||||
pub fn position_limits(mut self, min_position: Option<f64>, max_position: Option<f64>) -> Self {
|
||||
pub const fn position_limits(
|
||||
mut self,
|
||||
min_position: Option<f64>,
|
||||
max_position: Option<f64>,
|
||||
) -> Self {
|
||||
self.min_position = min_position;
|
||||
self.max_position = max_position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set tolerances for position and speed comparisons
|
||||
pub fn tolerances(mut self, position_tolerance: f64, speed_tolerance: f64) -> Self {
|
||||
pub const fn tolerances(mut self, position_tolerance: f64, speed_tolerance: f64) -> Self {
|
||||
self.position_tolerance = position_tolerance;
|
||||
self.speed_tolerance = speed_tolerance;
|
||||
self
|
||||
@@ -322,7 +330,7 @@ impl AccelerationPositionController {
|
||||
|
||||
let motion_constants = Self::calculate_motion_constants(&config);
|
||||
|
||||
Ok(AccelerationPositionController {
|
||||
Ok(Self {
|
||||
config,
|
||||
current_position: 0.0,
|
||||
current_speed: 0.0,
|
||||
@@ -366,19 +374,19 @@ impl AccelerationPositionController {
|
||||
}
|
||||
|
||||
Self::new(
|
||||
-speed, // min_speed
|
||||
speed, // max_speed
|
||||
-acceleration, // min_acceleration
|
||||
acceleration, // max_acceleration
|
||||
position.map(|p| -p), // min_position
|
||||
position.map(|p| p), // max_position
|
||||
1e-6, // position_tolerance (default)
|
||||
1e-6, // speed_tolerance (default)
|
||||
-speed, // min_speed
|
||||
speed, // max_speed
|
||||
-acceleration, // min_acceleration
|
||||
acceleration, // max_acceleration
|
||||
position.map(|p| -p.abs()), // min_position
|
||||
position.map(|p| p.abs()), // max_position
|
||||
1e-6, // position_tolerance (default)
|
||||
1e-6, // speed_tolerance (default)
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new builder for the controller
|
||||
pub fn builder() -> ControllerBuilder {
|
||||
pub const fn builder() -> ControllerBuilder {
|
||||
ControllerBuilder::new()
|
||||
}
|
||||
|
||||
@@ -564,8 +572,10 @@ impl AccelerationPositionController {
|
||||
|
||||
// Calculate distance needed to reach cruise speed from current speed
|
||||
let speed_change_distance = if current_speed_in_direction < cruise_speed.abs() {
|
||||
(cruise_speed.abs().powi(2) - current_speed_in_direction.max(0.0).powi(2))
|
||||
/ (2.0 * acceleration_rate)
|
||||
current_speed_in_direction.max(0.0).mul_add(
|
||||
-current_speed_in_direction.max(0.0),
|
||||
cruise_speed.abs().powi(2),
|
||||
) / (2.0 * acceleration_rate)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -579,8 +589,10 @@ impl AccelerationPositionController {
|
||||
// Using corrected kinematic equation: v_peak² = v_current² + 2*a*d_total*a_accel*a_decel/(a_accel + a_decel)
|
||||
let accel_decel_factor =
|
||||
(acceleration_rate * deceleration_rate) / (acceleration_rate + deceleration_rate);
|
||||
let discriminant = current_speed_in_direction.max(0.0).powi(2)
|
||||
+ 2.0 * abs_position_change * accel_decel_factor;
|
||||
let discriminant = current_speed_in_direction.max(0.0).mul_add(
|
||||
current_speed_in_direction.max(0.0),
|
||||
2.0 * abs_position_change * accel_decel_factor,
|
||||
);
|
||||
|
||||
if discriminant >= 0.0 {
|
||||
self.peak_speed = discriminant.sqrt() * cruise_speed.signum();
|
||||
@@ -634,7 +646,7 @@ impl AccelerationPositionController {
|
||||
self.current_acceleration = self.config.max_acceleration * self.direction as f64;
|
||||
|
||||
// Update speed
|
||||
let new_speed = self.current_speed + self.current_acceleration * dt;
|
||||
let new_speed = self.current_acceleration.mul_add(dt, self.current_speed);
|
||||
|
||||
// Check if we've reached peak speed
|
||||
if (self.direction > 0 && new_speed >= self.peak_speed)
|
||||
@@ -665,7 +677,7 @@ impl AccelerationPositionController {
|
||||
self.current_acceleration = self.config.min_acceleration * self.direction as f64;
|
||||
|
||||
// Update speed
|
||||
let new_speed = self.current_speed + self.current_acceleration * dt;
|
||||
let new_speed = self.current_acceleration.mul_add(dt, self.current_speed);
|
||||
|
||||
// Check if we've reached zero speed or target
|
||||
if (self.current_speed > 0.0 && new_speed <= 0.0)
|
||||
@@ -689,7 +701,7 @@ impl AccelerationPositionController {
|
||||
}
|
||||
|
||||
// Update position based on current speed
|
||||
let new_position = self.current_position + self.current_speed * dt;
|
||||
let new_position = self.current_speed.mul_add(dt, self.current_position);
|
||||
|
||||
// Check position limits and handle violations
|
||||
let mut position_limited = false;
|
||||
@@ -742,7 +754,7 @@ impl AccelerationPositionController {
|
||||
///
|
||||
/// Returns the current position of the controlled object.
|
||||
/// This value is updated with each call to `update()`.
|
||||
pub fn get_position(&self) -> f64 {
|
||||
pub const fn get_position(&self) -> f64 {
|
||||
self.current_position
|
||||
}
|
||||
|
||||
@@ -751,7 +763,7 @@ impl AccelerationPositionController {
|
||||
/// Returns the current speed (velocity) of the controlled object.
|
||||
/// Positive values indicate motion towards increasing position,
|
||||
/// negative values indicate motion towards decreasing position.
|
||||
pub fn get_speed(&self) -> f64 {
|
||||
pub const fn get_speed(&self) -> f64 {
|
||||
self.current_speed
|
||||
}
|
||||
|
||||
@@ -759,32 +771,32 @@ impl AccelerationPositionController {
|
||||
///
|
||||
/// Returns the current acceleration of the controlled object.
|
||||
/// Positive values indicate increasing speed, negative values indicate decreasing speed.
|
||||
pub fn get_acceleration(&self) -> f64 {
|
||||
pub const fn get_acceleration(&self) -> f64 {
|
||||
self.current_acceleration
|
||||
}
|
||||
|
||||
/// Get the target position
|
||||
pub fn get_target_position(&self) -> f64 {
|
||||
pub const fn get_target_position(&self) -> f64 {
|
||||
self.target_position
|
||||
}
|
||||
|
||||
/// Get the current motion phase
|
||||
pub fn get_motion_phase(&self) -> MotionPhase {
|
||||
pub const fn get_motion_phase(&self) -> MotionPhase {
|
||||
self.motion_phase
|
||||
}
|
||||
|
||||
/// Get the direction of motion
|
||||
pub fn get_direction(&self) -> i8 {
|
||||
pub const fn get_direction(&self) -> i8 {
|
||||
self.direction
|
||||
}
|
||||
|
||||
/// Get the calculated peak speed for current motion
|
||||
pub fn get_peak_speed(&self) -> f64 {
|
||||
pub const fn get_peak_speed(&self) -> f64 {
|
||||
self.peak_speed
|
||||
}
|
||||
|
||||
/// Get the position at which deceleration will begin
|
||||
pub fn get_deceleration_position(&self) -> f64 {
|
||||
pub const fn get_deceleration_position(&self) -> f64 {
|
||||
self.deceleration_position
|
||||
}
|
||||
|
||||
@@ -837,7 +849,7 @@ impl AccelerationPositionController {
|
||||
}
|
||||
|
||||
/// Force the controller to stop at current position
|
||||
pub fn emergency_stop(&mut self) {
|
||||
pub const fn emergency_stop(&mut self) {
|
||||
self.target_position = self.current_position;
|
||||
self.current_speed = 0.0;
|
||||
self.current_acceleration = 0.0;
|
||||
@@ -1006,32 +1018,32 @@ impl AccelerationPositionController {
|
||||
}
|
||||
|
||||
/// Get the minimum speed limit
|
||||
pub fn get_min_speed(&self) -> f64 {
|
||||
pub const fn get_min_speed(&self) -> f64 {
|
||||
self.config.min_speed
|
||||
}
|
||||
|
||||
/// Get the maximum speed limit
|
||||
pub fn get_max_speed(&self) -> f64 {
|
||||
pub const fn get_max_speed(&self) -> f64 {
|
||||
self.config.max_speed
|
||||
}
|
||||
|
||||
/// Get the minimum acceleration limit
|
||||
pub fn get_min_acceleration(&self) -> f64 {
|
||||
pub const fn get_min_acceleration(&self) -> f64 {
|
||||
self.config.min_acceleration
|
||||
}
|
||||
|
||||
/// Get the maximum acceleration limit
|
||||
pub fn get_max_acceleration(&self) -> f64 {
|
||||
pub const fn get_max_acceleration(&self) -> f64 {
|
||||
self.config.max_acceleration
|
||||
}
|
||||
|
||||
/// Get the minimum position limit
|
||||
pub fn get_min_position(&self) -> Option<f64> {
|
||||
pub const fn get_min_position(&self) -> Option<f64> {
|
||||
self.config.min_position
|
||||
}
|
||||
|
||||
/// Get the maximum position limit
|
||||
pub fn get_max_position(&self) -> Option<f64> {
|
||||
pub const fn get_max_position(&self) -> Option<f64> {
|
||||
self.config.max_position
|
||||
}
|
||||
|
||||
@@ -1068,17 +1080,17 @@ impl AccelerationPositionController {
|
||||
}
|
||||
|
||||
/// Get the position tolerance
|
||||
pub fn get_position_tolerance(&self) -> f64 {
|
||||
pub const fn get_position_tolerance(&self) -> f64 {
|
||||
self.config.position_tolerance
|
||||
}
|
||||
|
||||
/// Get the speed tolerance
|
||||
pub fn get_speed_tolerance(&self) -> f64 {
|
||||
pub const fn get_speed_tolerance(&self) -> f64 {
|
||||
self.config.speed_tolerance
|
||||
}
|
||||
|
||||
/// Set tolerances for position and speed comparisons
|
||||
pub fn set_tolerances(&mut self, position_tolerance: f64, speed_tolerance: f64) {
|
||||
pub const fn set_tolerances(&mut self, position_tolerance: f64, speed_tolerance: f64) {
|
||||
self.config.position_tolerance = position_tolerance;
|
||||
self.config.speed_tolerance = speed_tolerance;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::radian,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
@@ -102,16 +102,12 @@ impl AngularAccelerationPositionController {
|
||||
|
||||
/// Get the minimum angle position limit
|
||||
pub fn get_min_position(&self) -> Option<Angle> {
|
||||
self.controller
|
||||
.get_min_position()
|
||||
.map(|angle| Angle::new::<radian>(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| Angle::new::<radian>(angle))
|
||||
self.controller.get_max_position().map(Angle::new::<radian>)
|
||||
}
|
||||
|
||||
/// Set the minimum angle position limit
|
||||
@@ -203,8 +199,8 @@ impl AngularAccelerationPositionController {
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
|
||||
/// use uom::si::f64::Angle;
|
||||
/// use uom::si::angle::degree;
|
||||
/// use units::f64::Angle;
|
||||
/// use units::angle::degree;
|
||||
///
|
||||
/// let mut controller = AngularAccelerationPositionController::new_simple(
|
||||
/// Some(Angle::new::<degree>(180.0)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_jerk::radian_per_second_cubed,
|
||||
angular_velocity::radian_per_second,
|
||||
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
@@ -65,7 +65,7 @@ impl AngularJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
@@ -127,7 +127,7 @@ impl AngularJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
@@ -185,7 +185,7 @@ impl AngularJerkSpeedController {
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use std::time::Instant;
|
||||
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
|
||||
/// use units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
|
||||
///
|
||||
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
|
||||
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
|
||||
@@ -236,7 +236,7 @@ impl AngularJerkSpeedController {
|
||||
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(|speed| AngularVelocity::new::<radian_per_second>(speed))
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
/// Get the maximum angular velocity limit
|
||||
@@ -248,7 +248,7 @@ impl AngularJerkSpeedController {
|
||||
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(|speed| AngularVelocity::new::<radian_per_second>(speed))
|
||||
.map(AngularVelocity::new::<radian_per_second>)
|
||||
}
|
||||
|
||||
/// Set the minimum angular velocity limit
|
||||
|
||||
@@ -88,7 +88,7 @@ impl JerkSpeedController {
|
||||
)
|
||||
.expect("Failed to create AccelerationPositionController");
|
||||
|
||||
JerkSpeedController { base_controller }
|
||||
Self { base_controller }
|
||||
}
|
||||
|
||||
/// Create a new speed controller with simple symmetric limits
|
||||
@@ -183,7 +183,7 @@ impl JerkSpeedController {
|
||||
///
|
||||
/// # Returns
|
||||
/// Current speed as a floating-point value
|
||||
pub fn get_speed(&self) -> f64 {
|
||||
pub const fn get_speed(&self) -> f64 {
|
||||
self.base_controller.get_position()
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl JerkSpeedController {
|
||||
///
|
||||
/// # Returns
|
||||
/// Current acceleration as a floating-point value
|
||||
pub fn get_acceleration(&self) -> f64 {
|
||||
pub const fn get_acceleration(&self) -> f64 {
|
||||
self.base_controller.get_speed()
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ impl JerkSpeedController {
|
||||
///
|
||||
/// # Returns
|
||||
/// Current jerk as a floating-point value
|
||||
pub fn get_jerk(&self) -> f64 {
|
||||
pub const fn get_jerk(&self) -> f64 {
|
||||
self.base_controller.get_acceleration()
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ impl JerkSpeedController {
|
||||
///
|
||||
/// # Returns
|
||||
/// Target speed as a floating-point value
|
||||
pub fn get_target_speed(&self) -> f64 {
|
||||
pub const fn get_target_speed(&self) -> f64 {
|
||||
self.base_controller.get_target_position()
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ impl JerkSpeedController {
|
||||
///
|
||||
/// # Returns
|
||||
/// Minimum speed limit as an Option<f64>, None if no limit is set
|
||||
pub fn get_min_speed(&self) -> Option<f64> {
|
||||
pub const fn get_min_speed(&self) -> Option<f64> {
|
||||
self.base_controller.get_min_position()
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ impl JerkSpeedController {
|
||||
///
|
||||
/// # Returns
|
||||
/// Maximum speed limit as an Option<f64>, None if no limit is set
|
||||
pub fn get_max_speed(&self) -> Option<f64> {
|
||||
pub const fn get_max_speed(&self) -> Option<f64> {
|
||||
self.base_controller.get_max_position()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Length, Velocity},
|
||||
length::meter,
|
||||
@@ -22,8 +22,8 @@ use super::acceleration_position_controller::{
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use uom::si::f64::{Length, Velocity, Acceleration};
|
||||
/// 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);
|
||||
@@ -101,8 +101,8 @@ impl LinearAccelerationPositionController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use uom::si::f64::{Length, Velocity, Acceleration};
|
||||
/// 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
|
||||
@@ -150,7 +150,7 @@ impl LinearAccelerationPositionController {
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use std::time::Instant;
|
||||
/// use uom::si::{length::meter, f64::Length};
|
||||
/// use units::{length::meter, f64::Length};
|
||||
///
|
||||
/// let mut controller = LinearAccelerationPositionController::new_simple(
|
||||
/// Some(Length::new::<meter>(10.0)),
|
||||
@@ -205,9 +205,7 @@ impl LinearAccelerationPositionController {
|
||||
/// # 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| Length::new::<meter>(length))
|
||||
self.controller.get_min_position().map(Length::new::<meter>)
|
||||
}
|
||||
|
||||
/// Get the maximum position limit
|
||||
@@ -217,9 +215,7 @@ impl LinearAccelerationPositionController {
|
||||
/// # 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| Length::new::<meter>(length))
|
||||
self.controller.get_max_position().map(Length::new::<meter>)
|
||||
}
|
||||
|
||||
/// Set the minimum position limit
|
||||
@@ -379,7 +375,7 @@ impl LinearAccelerationPositionController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
|
||||
/// use units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
|
||||
///
|
||||
/// let new_position = Length::new::<meter>(0.0);
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Jerk, Velocity},
|
||||
jerk::meter_per_second_cubed,
|
||||
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
@@ -65,7 +65,7 @@ impl LinearJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
@@ -127,7 +127,7 @@ impl LinearJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
@@ -195,14 +195,14 @@ impl LinearJerkSpeedController {
|
||||
pub fn get_min_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(|speed| Velocity::new::<meter_per_second>(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(|speed| Velocity::new::<meter_per_second>(speed))
|
||||
.map(Velocity::new::<meter_per_second>)
|
||||
}
|
||||
|
||||
/// Set the minimum velocity limit
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::f32::consts::PI;
|
||||
use units::{angle::radian, f64::Angle};
|
||||
|
||||
use uom::si::angle::radian;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[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)
|
||||
@@ -11,7 +9,7 @@ pub struct AngleConverter {
|
||||
|
||||
impl AngleConverter {
|
||||
/// Create a custom coordinate system
|
||||
pub fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
|
||||
pub const fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
|
||||
Self {
|
||||
flip_x,
|
||||
flip_y,
|
||||
@@ -20,52 +18,52 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Standard mathematical coordinate system (CCW positive, 0° at positive X)
|
||||
pub fn mathematical() -> Self {
|
||||
pub const fn mathematical() -> Self {
|
||||
Self::new(false, false, false)
|
||||
}
|
||||
|
||||
/// Screen/graphics coordinate system (CW positive, 0° at positive X, Y-flipped)
|
||||
pub fn screen() -> Self {
|
||||
pub const fn screen() -> Self {
|
||||
Self::new(false, true, false)
|
||||
}
|
||||
|
||||
/// System where 0° points up, clockwise positive
|
||||
pub fn y_up_cw() -> Self {
|
||||
pub const fn y_up_cw() -> Self {
|
||||
Self::new(false, false, true)
|
||||
}
|
||||
|
||||
/// System where 0° points down, counter-clockwise positive
|
||||
pub fn y_down_ccw() -> Self {
|
||||
pub const fn y_down_ccw() -> Self {
|
||||
Self::new(false, true, true)
|
||||
}
|
||||
|
||||
/// System where 0° points left, clockwise positive
|
||||
pub fn x_left_cw() -> Self {
|
||||
pub const fn x_left_cw() -> Self {
|
||||
Self::new(true, false, false)
|
||||
}
|
||||
|
||||
/// System where 0° points right, counter-clockwise positive (same as mathematical)
|
||||
pub fn x_right_ccw() -> Self {
|
||||
pub const fn x_right_ccw() -> Self {
|
||||
Self::mathematical()
|
||||
}
|
||||
|
||||
/// System where 0° points up, counter-clockwise positive
|
||||
pub fn y_up_ccw() -> Self {
|
||||
pub const fn y_up_ccw() -> Self {
|
||||
Self::new(false, false, false)
|
||||
}
|
||||
|
||||
/// System where 0° points down, clockwise positive
|
||||
pub fn y_down_cw() -> Self {
|
||||
pub const fn y_down_cw() -> Self {
|
||||
Self::new(false, true, false)
|
||||
}
|
||||
|
||||
/// System where 0° points left, counter-clockwise positive
|
||||
pub fn x_left_ccw() -> Self {
|
||||
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: f32) -> 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;
|
||||
@@ -75,7 +73,7 @@ impl AngleConverter {
|
||||
(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 + result, // Both flips: 180° rotation
|
||||
(true, true) => result += 180.0, // Both flips: 180° rotation
|
||||
}
|
||||
|
||||
// Handle rotation direction
|
||||
@@ -87,7 +85,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f32)
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f32) -> 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;
|
||||
@@ -102,24 +100,24 @@ impl AngleConverter {
|
||||
(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 = result - 180.0, // Both flips: inverse 180° rotation
|
||||
(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: f32) -> f32 {
|
||||
let degrees = math_angle_radians * 180.0 / PI;
|
||||
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 * PI / 180.0
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle in radians from this system to mathematical system (f32)
|
||||
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
|
||||
let degrees = system_angle_radians * 180.0 / PI;
|
||||
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 * PI / 180.0
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle from mathematical system to this system (f64)
|
||||
@@ -133,7 +131,7 @@ impl AngleConverter {
|
||||
(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 + result, // Both flips: 180° rotation
|
||||
(true, true) => result += 180.0, // Both flips: 180° rotation
|
||||
}
|
||||
|
||||
// Handle rotation direction
|
||||
@@ -160,7 +158,7 @@ impl AngleConverter {
|
||||
(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 = result - 180.0, // Both flips: inverse 180° rotation
|
||||
(true, true) => result -= 180.0, // Both flips: inverse 180° rotation
|
||||
}
|
||||
|
||||
self.normalize_angle_f64(result)
|
||||
@@ -168,20 +166,20 @@ impl AngleConverter {
|
||||
|
||||
/// 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 * 180.0 / std::f64::consts::PI;
|
||||
let degrees = math_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_encode_f64(degrees);
|
||||
converted_degrees * std::f64::consts::PI / 180.0
|
||||
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 * 180.0 / std::f64::consts::PI;
|
||||
let degrees = system_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_decode_f64(degrees);
|
||||
converted_degrees * std::f64::consts::PI / 180.0
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f32)
|
||||
fn normalize_angle(&self, angle: f32) -> f32 {
|
||||
fn normalize_angle(&self, angle: f64) -> f64 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
@@ -205,39 +203,23 @@ pub struct AngleConverterUom {
|
||||
}
|
||||
|
||||
impl AngleConverterUom {
|
||||
pub fn new(angle_converter: AngleConverter) -> Self {
|
||||
pub const fn new(angle_converter: AngleConverter) -> Self {
|
||||
Self { angle_converter }
|
||||
}
|
||||
|
||||
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
|
||||
uom::si::f32::Angle::new::<radian>(
|
||||
self.angle_converter.radians_encode(angle.get::<radian>()),
|
||||
)
|
||||
pub fn encode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
|
||||
}
|
||||
|
||||
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
|
||||
uom::si::f32::Angle::new::<radian>(
|
||||
self.angle_converter.radians_decode(angle.get::<radian>()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
|
||||
uom::si::f64::Angle::new::<radian>(
|
||||
self.angle_converter
|
||||
.radians_encode_f64(angle.get::<radian>()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
|
||||
uom::si::f64::Angle::new::<radian>(
|
||||
self.angle_converter
|
||||
.radians_decode_f64(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]
|
||||
@@ -297,8 +279,8 @@ mod tests {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Test radians_encode
|
||||
let result = converter.radians_encode(PI / 2.0); // 90° in radians
|
||||
let expected = 270.0 * PI / 180.0; // 270° in radians
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -306,10 +288,10 @@ mod tests {
|
||||
fn test_roundtrip_conversion() {
|
||||
let converter = AngleConverter::new(true, false, true); // X-flip + CW
|
||||
|
||||
let original = 75.0_f32;
|
||||
let original = 75.0_f64;
|
||||
let converted = converter.degrees_encode(original);
|
||||
let roundtrip = converter.degrees_decode(converted);
|
||||
|
||||
assert!((original - roundtrip).abs() < 1e-6);
|
||||
assert!((original - roundtrip as f64).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::revolution,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::revolution_per_second,
|
||||
@@ -18,7 +18,7 @@ pub struct AngularStepConverter {
|
||||
|
||||
impl AngularStepConverter {
|
||||
/// Create a new converter with the specified steps per revolution
|
||||
pub fn new(steps_per_revolution: i16) -> Self {
|
||||
pub const fn new(steps_per_revolution: i16) -> Self {
|
||||
Self {
|
||||
steps_per_revolution,
|
||||
}
|
||||
@@ -90,7 +90,7 @@ 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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
use uom::si::{
|
||||
acceleration::meter_per_second_squared,
|
||||
use units::{
|
||||
angle::revolution,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::revolution_per_second,
|
||||
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
|
||||
length::meter,
|
||||
velocity::meter_per_second,
|
||||
};
|
||||
|
||||
use super::angular_step_converter::AngularStepConverter;
|
||||
use super::circular_converter::CircularConverter;
|
||||
|
||||
/// Converts between linear measurements and motor steps
|
||||
///
|
||||
@@ -18,57 +15,52 @@ use super::angular_step_converter::AngularStepConverter;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LinearStepConverter {
|
||||
angular_step_converter: AngularStepConverter,
|
||||
radius: Length,
|
||||
circumference: Length,
|
||||
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 {
|
||||
let circumference =
|
||||
Length::new::<meter>(2.0 * std::f64::consts::PI * radius.get::<meter>());
|
||||
Self {
|
||||
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
|
||||
radius,
|
||||
circumference,
|
||||
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 {
|
||||
let radius = diameter / 2.0;
|
||||
Self::from_radius(steps_per_revolution, radius)
|
||||
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 {
|
||||
let radius =
|
||||
Length::new::<meter>(circumference.get::<meter>() / (2.0 * std::f64::consts::PI));
|
||||
Self {
|
||||
angular_step_converter: AngularStepConverter::new(steps_per_revolution),
|
||||
radius,
|
||||
circumference,
|
||||
circular_converter: CircularConverter::from_circumference(circumference),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the radius used by the converter
|
||||
pub fn radius(&self) -> Length {
|
||||
self.radius
|
||||
self.circular_converter.radius()
|
||||
}
|
||||
|
||||
/// Get the diameter of the system
|
||||
pub fn diameter(&self) -> Length {
|
||||
self.radius * 2.0
|
||||
self.circular_converter.diameter()
|
||||
}
|
||||
|
||||
/// Get the circumference of the system
|
||||
pub fn circumference(&self) -> Length {
|
||||
self.circumference
|
||||
self.circular_converter.circumference()
|
||||
}
|
||||
|
||||
/// Get the steps per revolution
|
||||
pub fn steps_per_revolution(&self) -> i16 {
|
||||
pub const fn steps_per_revolution(&self) -> i16 {
|
||||
self.angular_step_converter.steps_per_revolution
|
||||
}
|
||||
}
|
||||
@@ -79,8 +71,10 @@ impl LinearStepConverter {
|
||||
///
|
||||
/// Formula: steps = (distance / circumference) * steps_per_revolution
|
||||
pub fn distance_to_steps(&self, distance: Length) -> f64 {
|
||||
// Convert distance to angle: angle = distance/circumference (in revolutions)
|
||||
let revolutions = distance.get::<meter>() / self.circumference.get::<meter>();
|
||||
// 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
|
||||
@@ -94,21 +88,19 @@ impl LinearStepConverter {
|
||||
// Convert steps to angle
|
||||
let angle = self.angular_step_converter.steps_to_angle(steps);
|
||||
|
||||
// Convert angle to distance: distance = angle * circumference
|
||||
// Convert angle to distance using CircularConverter
|
||||
let revolutions = angle.get::<revolution>();
|
||||
let distance = revolutions * self.circumference.get::<meter>();
|
||||
|
||||
Length::new::<meter>(distance)
|
||||
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 angular velocity: ω = v/circumference (in rev/s)
|
||||
let linear_velocity = velocity.get::<meter_per_second>();
|
||||
let angular_velocity_rps = linear_velocity / self.circumference.get::<meter>();
|
||||
let angular_velocity = AngularVelocity::new::<revolution_per_second>(angular_velocity_rps);
|
||||
// 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
|
||||
@@ -124,23 +116,19 @@ impl LinearStepConverter {
|
||||
.angular_step_converter
|
||||
.steps_to_angular_velocity(steps_per_second);
|
||||
|
||||
// Convert angular velocity to linear velocity: v = ω * circumference
|
||||
let angular_velocity_rps = angular_velocity.get::<revolution_per_second>();
|
||||
let linear_velocity = angular_velocity_rps * self.circumference.get::<meter>();
|
||||
|
||||
Velocity::new::<meter_per_second>(linear_velocity)
|
||||
// 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: α = a/r
|
||||
let linear_acceleration = acceleration.get::<meter_per_second_squared>();
|
||||
// Calculate angular acceleration in radians per second squared
|
||||
let angular_acceleration_rad_per_s2 = linear_acceleration / self.radius.get::<meter>();
|
||||
let angular_acceleration =
|
||||
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2);
|
||||
// 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
|
||||
@@ -156,12 +144,9 @@ impl LinearStepConverter {
|
||||
.angular_step_converter
|
||||
.steps_to_angular_acceleration(steps_per_second_squared);
|
||||
|
||||
// Convert angular acceleration to linear acceleration: a = α * r
|
||||
let angular_acceleration_rad_per_s2 =
|
||||
angular_acceleration.get::<radian_per_second_squared>();
|
||||
let linear_acceleration = angular_acceleration_rad_per_s2 * self.radius.get::<meter>();
|
||||
|
||||
Acceleration::new::<meter_per_second_squared>(linear_acceleration)
|
||||
// Convert angular acceleration to linear acceleration using CircularConverter
|
||||
self.circular_converter
|
||||
.angular_to_linear_acceleration(angular_acceleration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +156,9 @@ impl LinearStepConverter {
|
||||
///
|
||||
/// Formula: angle (in revolutions) = distance / circumference
|
||||
pub fn distance_to_angle(&self, distance: Length) -> Angle {
|
||||
let revolutions = distance.get::<meter>() / self.circumference.get::<meter>();
|
||||
let revolutions = self
|
||||
.circular_converter
|
||||
.linear_distance_to_revolutions(distance);
|
||||
Angle::new::<revolution>(revolutions)
|
||||
}
|
||||
|
||||
@@ -180,26 +167,24 @@ impl LinearStepConverter {
|
||||
/// Formula: distance = angle (in revolutions) * circumference
|
||||
pub fn angle_to_distance(&self, angle: Angle) -> Length {
|
||||
let revolutions = angle.get::<revolution>();
|
||||
let distance = revolutions * self.circumference.get::<meter>();
|
||||
Length::new::<meter>(distance)
|
||||
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 linear_velocity = velocity.get::<meter_per_second>();
|
||||
let angular_velocity_rps = linear_velocity / self.circumference.get::<meter>();
|
||||
AngularVelocity::new::<revolution_per_second>(angular_velocity_rps)
|
||||
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 angular_velocity_rps = angular_velocity.get::<revolution_per_second>();
|
||||
let linear_velocity = angular_velocity_rps * self.circumference.get::<meter>();
|
||||
Velocity::new::<meter_per_second>(linear_velocity)
|
||||
let rps = angular_velocity.get::<revolution_per_second>();
|
||||
self.circular_converter.rps_to_linear_velocity(rps)
|
||||
}
|
||||
|
||||
/// Convert linear acceleration to angular acceleration
|
||||
@@ -209,9 +194,8 @@ impl LinearStepConverter {
|
||||
&self,
|
||||
acceleration: Acceleration,
|
||||
) -> AngularAcceleration {
|
||||
let linear_acceleration = acceleration.get::<meter_per_second_squared>();
|
||||
let angular_acceleration_rad_per_s2 = linear_acceleration / self.radius.get::<meter>();
|
||||
AngularAcceleration::new::<radian_per_second_squared>(angular_acceleration_rad_per_s2)
|
||||
self.circular_converter
|
||||
.linear_to_angular_acceleration(acceleration)
|
||||
}
|
||||
|
||||
/// Convert angular acceleration to linear acceleration
|
||||
@@ -221,10 +205,8 @@ impl LinearStepConverter {
|
||||
&self,
|
||||
angular_acceleration: AngularAcceleration,
|
||||
) -> Acceleration {
|
||||
let angular_acceleration_rad_per_s2 =
|
||||
angular_acceleration.get::<radian_per_second_squared>();
|
||||
let linear_acceleration = angular_acceleration_rad_per_s2 * self.radius.get::<meter>();
|
||||
Acceleration::new::<meter_per_second_squared>(linear_acceleration)
|
||||
self.circular_converter
|
||||
.angular_to_linear_acceleration(angular_acceleration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +251,10 @@ 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() {
|
||||
|
||||
@@ -1,5 +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 transmission_converter;
|
||||
@@ -1,22 +0,0 @@
|
||||
use uom::si::f64::AngularVelocity;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TransmissionConverter {
|
||||
pub transmission_ratio: f64,
|
||||
}
|
||||
|
||||
impl TransmissionConverter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
transmission_ratio: 34.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_screw_output_rpm(&self, rpm: AngularVelocity) -> AngularVelocity {
|
||||
rpm / self.transmission_ratio
|
||||
}
|
||||
|
||||
pub fn calculate_screw_input_rpm(&self, screw_rpm: AngularVelocity) -> AngularVelocity {
|
||||
screw_rpm * self.transmission_ratio
|
||||
}
|
||||
}
|
||||
@@ -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,14 +11,46 @@ 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> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
@@ -37,17 +68,17 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
} else {
|
||||
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
|
||||
@@ -56,60 +87,39 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
})
|
||||
.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::Builder::new()
|
||||
.name(format!("ethercat-test-{}", name))
|
||||
.spawn(move || {
|
||||
std::panic::catch_unwind(|| {
|
||||
smol::block_on(async {
|
||||
match test_interface(&name) {
|
||||
Ok(_) => {
|
||||
tracing::info!("Found working EtherCAT interface: {}", name);
|
||||
Some(name.clone())
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("Interface {} failed", name);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.expect("Failed to spawn thread")
|
||||
})
|
||||
.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(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete and collect successful results
|
||||
let successful_interfaces: Vec<String> = tasks
|
||||
.into_iter()
|
||||
.filter_map(|task| match task.join().expect("Should join thread") {
|
||||
Some(name) => Some(name),
|
||||
None => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Return the first successful interface (they're sorted by name)
|
||||
let result = successful_interfaces.into_iter().next();
|
||||
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> {
|
||||
tracing::trace!("Testing interface: {}", interface);
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
@@ -147,7 +157,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::None,
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
pub mod eeprom_identification;
|
||||
pub mod interface_discovery;
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use interfaces::Interface;
|
||||
|
||||
pub mod modbus_tcp_discovery;
|
||||
|
||||
/// 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");
|
||||
Ok(reports_as_ethernet && !actually_wifi)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub fn get_interfaces() -> Result<Vec<Interface>> {
|
||||
let vec = Interface::get_all()
|
||||
.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<_>>();
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::ethernet::get_interfaces;
|
||||
use crate::{futures::FutureIteratorExt, modbus::tcp::ModbusTcpDevice};
|
||||
use anyhow::{Result, bail};
|
||||
use interfaces::Interface;
|
||||
use std::cmp::min;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
pub struct ModbusTcpProbe {
|
||||
pub addr: SocketAddr,
|
||||
pub serial: u16,
|
||||
}
|
||||
|
||||
pub async fn probe_modbus_tcp() -> Vec<ModbusTcpProbe> {
|
||||
let interfaces = match get_interfaces() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
||||
interfaces
|
||||
.into_iter()
|
||||
.map(probe_modbus_tcp_addresses)
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn probe_modbus_tcp_addresses(interface: Interface) -> Vec<ModbusTcpProbe> {
|
||||
let out: Vec<ModbusTcpProbe> = interface
|
||||
.addresses
|
||||
.iter()
|
||||
.filter_map(|addr| {
|
||||
let a = addr.addr.map(|a| a.ip());
|
||||
let m = addr.mask.map(|a| a.ip());
|
||||
|
||||
match (a, m) {
|
||||
(Some(IpAddr::V4(addr)), Some(IpAddr::V4(mask))) => Some((addr, mask)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.flat_map(|(addr, mask)| {
|
||||
let a = u32::from(addr);
|
||||
let m = u32::from(mask);
|
||||
let network = a & m;
|
||||
|
||||
let prefix = min(24, m.count_ones()); // mask → prefix length
|
||||
let size = 1u32 << (32 - prefix); // number of addresses
|
||||
|
||||
(0..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
|
||||
})
|
||||
.map(|addr| smol::spawn(ping_modbus_device(addr)))
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|x| x.ok())
|
||||
.collect();
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
async fn ping_modbus_device(addr: SocketAddr) -> Result<ModbusTcpProbe> {
|
||||
let mut device = ModbusTcpDevice::new(addr).await?;
|
||||
|
||||
let module_number1 = device.get_u32(0x2).await?;
|
||||
let module_number2 = device.get_u32(0x4).await?;
|
||||
|
||||
if module_number1 != 0x2787_2144 || module_number2 != 0x0000_0000 {
|
||||
bail!("Unknown modbus tcp device!");
|
||||
}
|
||||
|
||||
let serial = device.get_u32(0x000A).await?;
|
||||
|
||||
Ok(ModbusTcpProbe {
|
||||
addr,
|
||||
serial: serial as u16,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use core::future::Future;
|
||||
|
||||
// Extension trait for iterators of futures
|
||||
pub trait FutureIteratorExt<F>: Iterator<Item = F> + Sized
|
||||
where
|
||||
F: Future,
|
||||
{
|
||||
/// Await all futures and return their outputs
|
||||
fn join_all(self) -> impl Future<Output = Vec<F::Output>>;
|
||||
}
|
||||
|
||||
impl<I, F> FutureIteratorExt<F> for I
|
||||
where
|
||||
I: Iterator<Item = F> + Sized,
|
||||
F: Future,
|
||||
{
|
||||
async fn join_all(self) -> Vec<F::Output> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for fut in self {
|
||||
out.push(fut.await);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -63,11 +63,11 @@ 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.into();
|
||||
let normalized_value: f64 = normalized_value;
|
||||
let new_min: f64 = new_min.into();
|
||||
let new_max: f64 = new_max.into();
|
||||
|
||||
let result = normalized_value * (new_max - new_min) + new_min;
|
||||
let result = normalized_value.mul_add(new_max - new_min, new_min);
|
||||
T::from(result)
|
||||
}
|
||||
|
||||
@@ -128,11 +128,11 @@ where
|
||||
// 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 {
|
||||
return normalize(value_f64, 0.0, x_f64) * y_f64;
|
||||
normalize(value_f64, 0.0, x_f64) * y_f64
|
||||
} else if value_f64 > x_f64 {
|
||||
return normalize(value_f64, x_f64, 1.0) * (1.0 - y_f64) + y_f64;
|
||||
normalize(value_f64, x_f64, 1.0).mul_add(1.0 - y_f64, y_f64)
|
||||
} else {
|
||||
return y_f64;
|
||||
y_f64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ pub fn interpolate_exponential(normalized_value: f64, steepness: f64) -> f64 {
|
||||
return normalized_value;
|
||||
}
|
||||
let numerator = steepness.exp().powf(normalized_value) - 1.0;
|
||||
let denominator = steepness.exp() - 1.0;
|
||||
let denominator = steepness.exp_m1();
|
||||
|
||||
clip(numerator / denominator)
|
||||
}
|
||||
@@ -414,7 +414,7 @@ pub fn interpolate_inflected_exponential(x: f64, steepness: f64) -> f64 {
|
||||
|
||||
// Use standard exponential interpolation
|
||||
let result = interpolate_exponential(x_norm, steepness);
|
||||
0.5 + 0.5 * result
|
||||
0.5f64.mul_add(result, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
use smol::Timer;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct LoopThrottle {
|
||||
target_cycle_time: Duration,
|
||||
update_interval_cycles: usize,
|
||||
window_start: Instant,
|
||||
total_cycles: usize,
|
||||
delay: Duration,
|
||||
last_avg_cycle_time: Duration,
|
||||
on_update: Option<Box<dyn Fn(LoopThrottleUpdate)>>,
|
||||
}
|
||||
|
||||
pub struct LoopThrottleUpdate {
|
||||
pub delay: Duration,
|
||||
pub avg_cycle_time: Duration,
|
||||
pub error: i128, // Error in nanoseconds
|
||||
}
|
||||
impl LoopThrottle {
|
||||
pub fn new(
|
||||
target_cycle_time: Duration,
|
||||
update_interval_cycles: usize,
|
||||
on_update: Option<Box<dyn Fn(LoopThrottleUpdate)>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
target_cycle_time,
|
||||
update_interval_cycles,
|
||||
window_start: Instant::now(),
|
||||
total_cycles: 0,
|
||||
delay: target_cycle_time,
|
||||
last_avg_cycle_time: target_cycle_time,
|
||||
on_update,
|
||||
}
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub async fn sleep(&mut self) {
|
||||
self.total_cycles += 1;
|
||||
|
||||
// Check if it's time to update (every N cycles)
|
||||
if self.total_cycles % self.update_interval_cycles == 0 {
|
||||
let actual_total_time = self.window_start.elapsed();
|
||||
|
||||
// Use integer arithmetic where possible
|
||||
let avg_actual_cycle_time_nanos =
|
||||
actual_total_time.as_nanos() / self.update_interval_cycles as u128;
|
||||
let target_cycle_time_nanos = self.target_cycle_time.as_nanos();
|
||||
|
||||
// Calculate error in nanoseconds (signed)
|
||||
let error_nanos = target_cycle_time_nanos as i128 - avg_actual_cycle_time_nanos as i128;
|
||||
|
||||
// Update current delay, allowing zero or negative values to become zero
|
||||
let new_delay_nanos = (self.delay.as_nanos() as i128 + error_nanos).max(0) as u64;
|
||||
|
||||
self.delay = Duration::from_nanos(new_delay_nanos);
|
||||
self.last_avg_cycle_time = Duration::from_nanos(avg_actual_cycle_time_nanos as u64);
|
||||
|
||||
// Reset for next measurement window
|
||||
self.window_start = Instant::now();
|
||||
self.total_cycles = 0; // Reset to prevent overflow
|
||||
|
||||
// if we have an update callback, call it
|
||||
if let Some(on_update) = &self.on_update {
|
||||
on_update(LoopThrottleUpdate {
|
||||
delay: self.delay,
|
||||
avg_cycle_time: self.last_avg_cycle_time,
|
||||
error: error_nanos,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer::after(self.delay).await;
|
||||
}
|
||||
|
||||
pub fn get_cycle_time(&self) -> Duration {
|
||||
self.last_avg_cycle_time
|
||||
}
|
||||
|
||||
pub fn get_delay(&self) -> Duration {
|
||||
self.delay
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smol::block_on;
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_throttle_timing() {
|
||||
block_on(async {
|
||||
let update_interval_cycles = 100;
|
||||
let target_cycle_time = Duration::from_micros(5);
|
||||
let mut throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
|
||||
|
||||
let test_start = Instant::now();
|
||||
let mut cycle_times = Vec::new();
|
||||
|
||||
// Run for several cycles to trigger multiple updates
|
||||
for _ in 0..10000 {
|
||||
let cycle_start = Instant::now();
|
||||
|
||||
// Simulate some work
|
||||
Timer::after(Duration::from_micros(5)).await;
|
||||
|
||||
throttle.sleep().await;
|
||||
|
||||
let cycle_time = cycle_start.elapsed();
|
||||
cycle_times.push(cycle_time);
|
||||
}
|
||||
|
||||
let total_test_time = test_start.elapsed();
|
||||
let avg_cycle_time = cycle_times.iter().sum::<Duration>() / cycle_times.len() as u32;
|
||||
|
||||
println!("Test completed in: {:?}", total_test_time);
|
||||
println!("Average cycle time: {:?}", avg_cycle_time);
|
||||
println!("Target cycle time: {:?}", target_cycle_time);
|
||||
println!("Last measured avg: {:?}", throttle.get_cycle_time());
|
||||
|
||||
// Assert that we're reasonably close to target timing
|
||||
let tolerance = Duration::from_millis(10);
|
||||
assert!(
|
||||
avg_cycle_time >= target_cycle_time.saturating_sub(tolerance)
|
||||
&& avg_cycle_time <= target_cycle_time + tolerance,
|
||||
"Average cycle time {:?} not within tolerance of target {:?}",
|
||||
avg_cycle_time,
|
||||
target_cycle_time
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_throttle_adaptation() {
|
||||
block_on(async {
|
||||
let target_cycle_time = Duration::from_millis(100);
|
||||
let update_interval_cycles = 3;
|
||||
let mut throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
|
||||
|
||||
let initial_delay = throttle.get_delay();
|
||||
|
||||
// Run enough cycles to trigger multiple updates
|
||||
for _ in 0..12 {
|
||||
// Add variable work to test adaptation
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
throttle.sleep().await;
|
||||
}
|
||||
|
||||
println!("Initial delay: {:?}", initial_delay);
|
||||
println!("Final delay: {:?}", throttle.get_delay());
|
||||
println!("Last avg cycle time: {:?}", throttle.get_cycle_time());
|
||||
|
||||
assert!(throttle.get_cycle_time() > Duration::ZERO);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_throttle_initialization() {
|
||||
let update_interval_cycles = 10;
|
||||
let target_cycle_time = Duration::from_millis(75);
|
||||
let throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
|
||||
|
||||
assert_eq!(throttle.get_delay(), target_cycle_time);
|
||||
assert_eq!(throttle.get_cycle_time(), target_cycle_time);
|
||||
assert_eq!(throttle.total_cycles, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overflow_protection() {
|
||||
block_on(async {
|
||||
let target_cycle_time = Duration::from_micros(1);
|
||||
let update_interval_cycles = 5;
|
||||
let mut throttle = LoopThrottle::new(target_cycle_time, update_interval_cycles, None);
|
||||
|
||||
// Run many cycles to ensure total_cycles resets properly
|
||||
for i in 0..50 {
|
||||
throttle.sleep().await;
|
||||
|
||||
// Verify that total_cycles never exceeds update_interval_cycles
|
||||
assert!(
|
||||
throttle.total_cycles <= update_interval_cycles,
|
||||
"Cycle {}: total_cycles ({}) exceeded update_interval_cycles ({})",
|
||||
i,
|
||||
throttle.total_cycles,
|
||||
update_interval_cycles
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod compare_lists;
|
||||
pub mod hasher_serializer;
|
||||
pub mod hashing;
|
||||
pub mod interpolation;
|
||||
pub mod loop_trottle;
|
||||
pub mod moving_time_window;
|
||||
pub mod retry;
|
||||
@@ -11,56 +11,40 @@ pub trait DivideByCount {
|
||||
|
||||
impl DivideByCount for f64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as f64)
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for f32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as f32)
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for i32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as i32)
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for i64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as i64)
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for u32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as u32)
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for u64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as u64)
|
||||
self / (count as Self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic moving time window that works with numeric types.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::time::{Duration, Instant};
|
||||
/// use control_core::helpers::moving_time_window::MovingTimeWindow;
|
||||
///
|
||||
/// let mut window = MovingTimeWindow::new(Duration::from_secs(10), 100);
|
||||
/// window.update(1.5, Instant::now());
|
||||
/// window.update(2.5, Instant::now());
|
||||
///
|
||||
/// let average = window.average();
|
||||
/// println!("Average: {}", average);
|
||||
/// ```
|
||||
|
||||
// Type aliases for common use cases
|
||||
pub type F64Window = MovingTimeWindow<f64>;
|
||||
pub type F32Window = MovingTimeWindow<f32>;
|
||||
@@ -194,15 +178,14 @@ where
|
||||
.samples
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.fold(None, |acc, val| match acc {
|
||||
None => Some(val),
|
||||
Some(current_max) => {
|
||||
.fold(None, |acc, val| {
|
||||
acc.map_or(Some(val), |current_max| {
|
||||
if val > current_max {
|
||||
Some(val)
|
||||
} else {
|
||||
Some(current_max)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -229,15 +212,14 @@ where
|
||||
.samples
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.fold(None, |acc, val| match acc {
|
||||
None => Some(val),
|
||||
Some(current_min) => {
|
||||
.fold(None, |acc, val| {
|
||||
acc.map_or(Some(val), |current_min| {
|
||||
if val < current_min {
|
||||
Some(val)
|
||||
} else {
|
||||
Some(current_min)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -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,14 +1,16 @@
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod ethernet;
|
||||
pub mod futures;
|
||||
pub mod helpers;
|
||||
pub mod machines;
|
||||
pub mod irq_handling;
|
||||
pub mod modbus;
|
||||
pub mod realtime;
|
||||
pub mod rest;
|
||||
pub mod serial;
|
||||
pub mod socketio;
|
||||
pub mod uom_extensions;
|
||||
pub mod transmission;
|
||||
|
||||
#[macro_use]
|
||||
extern crate uom;
|
||||
#[cfg(feature = "video-streaming")]
|
||||
pub mod video_streaming;
|
||||
@@ -1,8 +0,0 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::socketio::namespace::Namespace;
|
||||
|
||||
pub trait MachineApi {
|
||||
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MachineIdentificationUnique {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}/{}/{}",
|
||||
self.machine_identification.vendor, self.machine_identification.machine, self.serial
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
Serial(DeviceHardwareIdentificationSerial),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DeviceHardwareIdentificationEthercat {
|
||||
pub subdevice_index: usize,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DeviceHardwareIdentificationSerial {
|
||||
pub path: String,
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
use crate::{
|
||||
machines::{
|
||||
identification::MachineIdentificationUnique,
|
||||
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
|
||||
},
|
||||
serial::SerialDevice,
|
||||
socketio::event::GenericEvent,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
new::MachineNewHardwareSerial,
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
#[derive(Debug)]
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub serial_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<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,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
) {
|
||||
// empty ethercat machines
|
||||
self.ethercat_machines.clear();
|
||||
|
||||
// group devices by machine device identification
|
||||
let device_grouping_result = group_devices_by_identification(device_identifications);
|
||||
|
||||
tracing::info!(
|
||||
"Device Groups {}",
|
||||
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,
|
||||
socket_queue_tx: socket_queue_tx.clone(),
|
||||
machine_manager: machine_manager.clone(),
|
||||
});
|
||||
|
||||
// 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<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
}
|
||||
|
||||
pub fn add_serial_device(
|
||||
&mut self,
|
||||
device_identification: &DeviceIdentification,
|
||||
device: Arc<RwLock<dyn SerialDevice>>,
|
||||
machine_registry: &MachineRegistry,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
) {
|
||||
let hardware = MachineNewHardwareSerial { device };
|
||||
|
||||
let device_identification_identified: DeviceIdentificationIdentified =
|
||||
device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
machine_manager: machine_manager.clone(),
|
||||
});
|
||||
|
||||
tracing::info!("Adding serial machine {:?}", new_machine);
|
||||
|
||||
self.serial_machines.insert(
|
||||
device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique,
|
||||
new_machine,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
|
||||
let device_identification_identified: DeviceIdentificationIdentified =
|
||||
device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
tracing::info!(
|
||||
"Removing serial machine {:?}",
|
||||
device_identification_identified
|
||||
);
|
||||
|
||||
self.serial_machines.remove(
|
||||
&device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_machine_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.ethercat_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
return Some(machine_weak);
|
||||
}
|
||||
|
||||
pub fn get_serial_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.serial_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
return Some(machine_weak);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, sync::Arc};
|
||||
|
||||
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<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines:
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Result<Arc<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,80 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use api::MachineApi;
|
||||
use new::MachineNewTrait;
|
||||
use serde::Serialize;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::new::MachineAct;
|
||||
pub mod api;
|
||||
pub mod identification;
|
||||
pub mod manager;
|
||||
pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
/// Casts a `dyn Machine` to a specific machine type
|
||||
pub async fn downcast_machine<T: Machine>(
|
||||
machine: Arc<Mutex<dyn Machine>>,
|
||||
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let read_lock = machine.lock().await;
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
if read_lock.as_any().is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned_machine = Arc::clone(&machine);
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
Ok(Arc::from_raw(
|
||||
Arc::into_raw(cloned_machine) as *const Mutex<T>
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"[{}::downcast_machine] Downcast failed",
|
||||
module_path!()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to get strong count to see if Weak has reference
|
||||
pub trait GetStrongCount {
|
||||
fn get_strong_count(&self) -> usize;
|
||||
}
|
||||
|
||||
impl<T> GetStrongCount for Weak<Mutex<T>> {
|
||||
fn get_strong_count(&self) -> usize {
|
||||
self.strong_count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectedMachine<T: GetStrongCount> {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub machine: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct ConnectedMachineData {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
|
||||
where
|
||||
T: GetStrongCount,
|
||||
{
|
||||
fn from(value: &ConnectedMachine<T>) -> Self {
|
||||
Self {
|
||||
machine_identification_unique: value.machine_identification_unique.clone(),
|
||||
is_available: value.machine.get_strong_count() != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
use crate::{machines::manager::MachineManager, serial::SerialDevice};
|
||||
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{
|
||||
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::{channel::Sender, lock::RwLock};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::socketio::event::GenericEvent;
|
||||
|
||||
pub trait MachineNewTrait {
|
||||
fn new<'maindevice, 'subdevices>(
|
||||
params: &MachineNewParams<'maindevice, 'subdevices, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait MachineAct {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
|
||||
}
|
||||
|
||||
pub struct MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
'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,
|
||||
'machine_new_hardware_serial,
|
||||
>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
pub machine_manager: Weak<RwLock<MachineManager>>,
|
||||
}
|
||||
|
||||
pub enum MachineNewHardware<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
> 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,
|
||||
>,
|
||||
),
|
||||
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
|
||||
}
|
||||
|
||||
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>>>,
|
||||
}
|
||||
|
||||
pub struct MachineNewHardwareSerial {
|
||||
pub device: Arc<RwLock<dyn SerialDevice>>,
|
||||
}
|
||||
|
||||
// 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,68 +0,0 @@
|
||||
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
|
||||
use anyhow::Error;
|
||||
use smol::lock::Mutex;
|
||||
use std::{any::TypeId, collections::HashMap, sync::Arc};
|
||||
|
||||
pub type MachineNewClosure =
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Arc<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(Arc::new(Mutex::new(T::new(machine_new_params)?)))
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn new_machine(
|
||||
&self,
|
||||
machine_new_params: &MachineNewParams,
|
||||
) -> Result<Arc<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)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
pub mod modbus_serial_interface;
|
||||
pub mod tcp;
|
||||
|
||||
use anyhow::Error;
|
||||
use crc::{CRC_16_MODBUS, Crc};
|
||||
use serial;
|
||||
use serialport::SerialPort;
|
||||
use std::time::Duration;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParityType {
|
||||
Even,
|
||||
Odd,
|
||||
@@ -62,17 +63,17 @@ impl From<ModbusExceptionCode> for u8 {
|
||||
impl From<u8> for ModbusExceptionCode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => ModbusExceptionCode::None,
|
||||
1 => ModbusExceptionCode::IllegalFunction,
|
||||
2 => ModbusExceptionCode::IllegalDataAddress,
|
||||
3 => ModbusExceptionCode::IllegalDataValue,
|
||||
4 => ModbusExceptionCode::SlaveDeviceFailure,
|
||||
5 => ModbusExceptionCode::Acknowledge,
|
||||
6 => ModbusExceptionCode::SlaveDeviceBusy,
|
||||
8 => ModbusExceptionCode::MemoryParityError,
|
||||
10 => ModbusExceptionCode::GatewayPathUnavailable,
|
||||
11 => ModbusExceptionCode::GatewayTargetDeviceFailedToRespond,
|
||||
other => ModbusExceptionCode::Unknown(other),
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,10 +98,10 @@ impl TryFrom<u8> for ModbusFunctionCode {
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x03 => Ok(ModbusFunctionCode::ReadHoldingRegister),
|
||||
0x04 => Ok(ModbusFunctionCode::ReadInputRegister),
|
||||
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!")),
|
||||
}
|
||||
}
|
||||
@@ -119,7 +120,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
|
||||
@@ -132,7 +133,7 @@ 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.
|
||||
@@ -143,9 +144,7 @@ impl From<ModbusRequest> for Vec<u8> {
|
||||
/// # 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 serial::SerialPort,
|
||||
) -> Result<Option<Vec<u8>>, anyhow::Error> {
|
||||
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 {
|
||||
@@ -155,7 +154,7 @@ pub fn receive_data_modbus(
|
||||
}
|
||||
|
||||
/// Computes Modbus CRC-16 using `crc` crate.
|
||||
pub fn modbus_crc16(data: &[u8]) -> u16 {
|
||||
pub const fn modbus_crc16(data: &[u8]) -> u16 {
|
||||
let modbus: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS);
|
||||
modbus.checksum(data)
|
||||
}
|
||||
@@ -206,23 +205,17 @@ fn extract_crc(raw_data: &Vec<u8>) -> Result<u16, Error> {
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -234,23 +227,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)]
|
||||
|
||||
@@ -66,7 +66,7 @@ impl ModbusSerialInterface {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
pub const fn is_initialized(&self) -> bool {
|
||||
!matches!(self.state, State::Uninitialized)
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ impl ModbusSerialInterface {
|
||||
Some(Duration::from_nanos(total_timeout))
|
||||
}
|
||||
|
||||
pub fn get_response(&self) -> Option<&ModbusResponse> {
|
||||
pub const fn get_response(&self) -> Option<&ModbusResponse> {
|
||||
self.response.as_ref()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use smol::Timer;
|
||||
use smol::future::FutureExt;
|
||||
use smol::io;
|
||||
use smol::io::AsyncReadExt;
|
||||
use smol::io::AsyncWriteExt;
|
||||
use smol::net::TcpStream;
|
||||
|
||||
const PROTOCOL_ID: u16 = 0;
|
||||
const UNIT_ID: u8 = 0;
|
||||
const READ_HOLDING_FUNCTION_CODE: u8 = 3;
|
||||
const WRITE_HOLDING_FUNCTION_CODE: u8 = 16;
|
||||
const READ_HOLDING_LENGTH: u16 = 6; // Unit ID 00 + Func Code 03 + Start Address XXXX + Quantity XXXX = 6 bytes
|
||||
const WRITE_HOLDING_LENGTH_WITHOUT_DATA: u16 = 7; // Unit ID 00 + Func Code 16 + Start Address XXXX + Quantity XXXX + Num Data Bytes XX = 7 bytes
|
||||
|
||||
struct Packet {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
pub const fn new() -> Self {
|
||||
Self { buf: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_u8(&mut self, x: u8) {
|
||||
self.buf.push(x);
|
||||
}
|
||||
|
||||
pub fn add_u16(&mut self, x: u16) {
|
||||
let buf = x.to_be_bytes();
|
||||
self.buf.push(buf[0]);
|
||||
self.buf.push(buf[1]);
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModbusTcpDevice {
|
||||
stream: TcpStream,
|
||||
transactions: u16,
|
||||
}
|
||||
|
||||
impl Debug for ModbusTcpDevice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.stream.peer_addr() {
|
||||
Ok(addr) => write!(f, "ModbusTcpDevice({:?})", addr),
|
||||
Err(_) => std::fmt::Result::Err(std::fmt::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModbusTcpDevice {
|
||||
pub async fn new(addr: SocketAddr) -> Result<Self> {
|
||||
let timeout = async {
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
Err(io::Error::from(ErrorKind::TimedOut))
|
||||
};
|
||||
|
||||
let stream = TcpStream::connect(addr)
|
||||
.or(timeout)
|
||||
.await
|
||||
.context("Could not connect to modbus device!")?;
|
||||
|
||||
Ok(Self {
|
||||
stream,
|
||||
transactions: 0,
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_and_check_responce_header(&mut self, func_code: u8) -> Result<()> {
|
||||
if self.read_u16().await? != self.transactions {
|
||||
bail!("Modbus device sent unexpected transaction id!");
|
||||
}
|
||||
|
||||
if self.read_u16().await? != PROTOCOL_ID {
|
||||
bail!("Modbus device sent unexpected protocol id!");
|
||||
}
|
||||
|
||||
let length = self.read_u16().await?;
|
||||
if length < 2 {
|
||||
bail!("Modbus device will send too few bytes to understand the response!");
|
||||
}
|
||||
|
||||
if self.read_u8().await? != UNIT_ID {
|
||||
bail!("Modbus device sent unexpected unit id!");
|
||||
}
|
||||
|
||||
let func_code_res = self.read_u8().await?;
|
||||
if func_code != func_code_res {
|
||||
let error_code = self.read_u8().await?;
|
||||
bail!(
|
||||
"Modbus device sent unexpected function code 0x{:x}! It probably encountered an error. Error code: 0x{:x}",
|
||||
func_code_res,
|
||||
error_code
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_read_holding_request(&mut self, addr: u16, count: u16) -> Result<u16> {
|
||||
let mut packet = Packet::new();
|
||||
|
||||
self.transactions += 1;
|
||||
packet.add_u16(self.transactions);
|
||||
packet.add_u16(PROTOCOL_ID);
|
||||
packet.add_u16(READ_HOLDING_LENGTH);
|
||||
|
||||
packet.add_u8(UNIT_ID);
|
||||
packet.add_u8(READ_HOLDING_FUNCTION_CODE);
|
||||
|
||||
packet.add_u16(addr);
|
||||
packet.add_u16(count);
|
||||
|
||||
self.send(packet).await?;
|
||||
self.read_and_check_responce_header(READ_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
let num_data_bytes = self.read_u8().await? as u16;
|
||||
if count * 2 != num_data_bytes {
|
||||
bail!(
|
||||
"Modbus device wants to send only a portion of the requested range - UNIMPLEMENTED!"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(num_data_bytes)
|
||||
}
|
||||
|
||||
pub async fn get_holding_registers(&mut self, addr: u16, count: u16) -> Result<Vec<u16>> {
|
||||
self.send_read_holding_request(addr, count).await?;
|
||||
|
||||
let mut res: Vec<u16> = Vec::new();
|
||||
while res.len() < count as usize {
|
||||
res.push(self.read_u16().await?);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn set_holding_registers(&mut self, addr: u16, values: &[u16]) -> Result<()> {
|
||||
let count = values.len();
|
||||
let num_bytes = count * 2;
|
||||
assert!(
|
||||
count <= 128,
|
||||
"Cannot send that many bytes to modbus device in one go! Trying to send {} bytes of data.",
|
||||
num_bytes
|
||||
);
|
||||
|
||||
let mut packet = Packet::new();
|
||||
|
||||
self.transactions += 1;
|
||||
packet.add_u16(self.transactions);
|
||||
packet.add_u16(PROTOCOL_ID);
|
||||
packet.add_u16(WRITE_HOLDING_LENGTH_WITHOUT_DATA + num_bytes as u16);
|
||||
|
||||
packet.add_u8(UNIT_ID);
|
||||
packet.add_u8(WRITE_HOLDING_FUNCTION_CODE);
|
||||
|
||||
packet.add_u16(addr);
|
||||
packet.add_u16(count as u16);
|
||||
|
||||
packet.add_u8(num_bytes as u8);
|
||||
|
||||
for value in values {
|
||||
packet.add_u16(value.to_owned());
|
||||
}
|
||||
|
||||
self.send(packet).await?;
|
||||
self.read_and_check_responce_header(WRITE_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
if self.read_u16().await? != addr {
|
||||
bail!("Modbus device wrote to wrong register address!");
|
||||
}
|
||||
|
||||
if self.read_u16().await? != count as u16 {
|
||||
bail!("Modbus device wrote wrong number of registers!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_string<const N: usize>(&mut self, addr: u16) -> Result<String> {
|
||||
assert!(N % 2 == 0, "Strings are always of even length!");
|
||||
assert!(N <= 256, "Cannot read long strings in a single swoop!");
|
||||
let count = N / 2;
|
||||
|
||||
self.send_read_holding_request(addr, count as u16).await?;
|
||||
let buf = self.read_bytes::<N>().await?;
|
||||
|
||||
let s = String::from_utf8_lossy(&buf);
|
||||
let s = match s.split_once('\0') {
|
||||
None => s.to_string(),
|
||||
Some((l, _)) => l.to_string(),
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub async fn set_string<const N: usize>(&mut self, _addr: u16, s: &str) -> Result<()> {
|
||||
assert!(s.len() <= N, "String is too long to fit!");
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_u16(&mut self, addr: u16) -> Result<u16> {
|
||||
let length = self.send_read_holding_request(addr, 1).await?;
|
||||
assert_eq!(
|
||||
length, 2,
|
||||
"Expected modbus device to send exactly 2 bytes of data!"
|
||||
);
|
||||
|
||||
self.read_u16().await
|
||||
}
|
||||
|
||||
pub async fn get_u32(&mut self, addr: u16) -> Result<u32> {
|
||||
let length = self.send_read_holding_request(addr, 2).await?;
|
||||
assert_eq!(
|
||||
length, 4,
|
||||
"Expected modbus device to send exactly 4 bytes of data!"
|
||||
);
|
||||
|
||||
let buf = self.read_bytes::<4>().await?;
|
||||
Ok(u32::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
// We have to send the request in a single packet,
|
||||
// otherwise the device may fail to respond correctly.
|
||||
async fn send(&mut self, packet: Packet) -> Result<()> {
|
||||
self.stream
|
||||
.write_all(packet.as_bytes())
|
||||
.await
|
||||
.context("Could write to modbus device!")
|
||||
}
|
||||
|
||||
async fn read_u8(&mut self) -> Result<u8> {
|
||||
let buf = self.read_bytes::<1>().await?;
|
||||
Ok(u8::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_u16(&mut self) -> Result<u16> {
|
||||
let buf = self.read_bytes::<2>().await?;
|
||||
Ok(u16::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
let mut buf = [0; N];
|
||||
self.stream
|
||||
.read_exact(&mut buf)
|
||||
.await
|
||||
.context("Could not read from modbus device!")?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[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:
|
||||
@@ -47,7 +47,7 @@ use tracing::error;
|
||||
///
|
||||
/// - `Ok(())` if the real-time priority was successfully set
|
||||
/// - `Err` with the error message if setting the priority failed
|
||||
#[cfg(unix)]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
use anyhow::anyhow;
|
||||
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
|
||||
@@ -84,9 +84,9 @@ pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
log::error!("This platform does not support realtime threads");
|
||||
tracing::error!("This platform does not support realtime threads");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
/// # How it works
|
||||
///
|
||||
/// [Rest of documentation as before...]
|
||||
#[cfg(unix)]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use libc::{MCL_FUTURE, mlockall};
|
||||
|
||||
@@ -154,9 +154,9 @@ pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
log::error!("This platform does not support memory locking");
|
||||
tracing::error!("This platform does not support memory locking");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
/*use std::{any::Any, sync::Arc};
|
||||
|
||||
use smol::lock::RwLock;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::machines::identification::DeviceIdentification;
|
||||
|
||||
pub mod panic;
|
||||
pub mod registry;
|
||||
pub mod serial_detection;
|
||||
|
||||
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
|
||||
@@ -25,11 +23,11 @@ pub trait SerialDeviceThread {
|
||||
|
||||
pub struct SerialDeviceNewParams {
|
||||
pub path: String,
|
||||
pub device_thread_panic_tx: smol::channel::Sender<(String, anyhow::Error)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct SerialDeviceIdentification {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
}
|
||||
*/
|
||||
@@ -1,52 +0,0 @@
|
||||
use smol::channel::Sender;
|
||||
|
||||
/// Device-level panic handler for individual device crashes
|
||||
/// Sends device identifier and error information for cleanup
|
||||
/// Used by serial devices and other individual components that need to be removed on panic
|
||||
pub fn send_serial_device_panic<T>(
|
||||
device_identifier: T,
|
||||
thread_panic_tx: Sender<(T, anyhow::Error)>,
|
||||
) where
|
||||
T: Send + Sync + Clone + 'static,
|
||||
{
|
||||
// Ensure backtrace is enabled for panics
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
}
|
||||
}
|
||||
|
||||
let device_id_clone = device_identifier.clone();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
let thread_name = std::thread::current()
|
||||
.name()
|
||||
.unwrap_or("unnamed")
|
||||
.to_string();
|
||||
let location = panic_info.location().map_or("unknown".to_string(), |loc| {
|
||||
format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
|
||||
});
|
||||
let message = panic_info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.unwrap_or(&"Box<dyn Any>")
|
||||
.to_string();
|
||||
|
||||
let panic_message = format!(
|
||||
"Device panicked in thread '{}' at {}: {}",
|
||||
thread_name, location, message
|
||||
);
|
||||
|
||||
// Send device panic info through channel
|
||||
smol::block_on(async {
|
||||
let _ = thread_panic_tx
|
||||
.send((
|
||||
device_id_clone.clone(),
|
||||
anyhow::anyhow!("{}", panic_message),
|
||||
))
|
||||
.await;
|
||||
});
|
||||
|
||||
// Note: We don't call old_hook to avoid duplicate logging
|
||||
// All logging is handled in the main thread when receiving panic details
|
||||
}));
|
||||
}
|
||||
@@ -7,175 +7,70 @@
|
||||
*/
|
||||
|
||||
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
|
||||
use smol::{
|
||||
channel::{Receiver, Sender, unbounded},
|
||||
lock::RwLock,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
helpers::compare_lists::compare_lists, machines::identification::DeviceIdentification,
|
||||
};
|
||||
use super::SerialDeviceIdentification;
|
||||
/// Handles scanning and caching of connected USB serial devices.
|
||||
pub struct SerialDetection {}
|
||||
|
||||
use super::{
|
||||
SerialDevice, SerialDeviceIdentification, SerialDeviceNewParams, registry::SerialDeviceRegistry,
|
||||
};
|
||||
|
||||
pub struct SerialDetection<'serialdeviceregistry> {
|
||||
pub serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry,
|
||||
pub ports: HashMap<
|
||||
String,
|
||||
(
|
||||
UsbPortInfo,
|
||||
DeviceIdentification,
|
||||
Arc<RwLock<dyn SerialDevice>>,
|
||||
),
|
||||
>,
|
||||
pub device_removal_signal_rx: Receiver<(String, anyhow::Error)>,
|
||||
pub device_removal_signal_tx: Sender<(String, anyhow::Error)>,
|
||||
}
|
||||
|
||||
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
|
||||
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
|
||||
let (device_removal_signal_tx, device_removal_signal_rx) =
|
||||
unbounded::<(String, anyhow::Error)>();
|
||||
SerialDetection {
|
||||
serial_device_registry,
|
||||
ports: HashMap::new(),
|
||||
device_removal_signal_rx,
|
||||
device_removal_signal_tx,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
*@return: Option::None if there are no available ports on device
|
||||
*@return: Option::Some(Vec) with all available ports
|
||||
*
|
||||
*@description: detects all available ports and returns them as list
|
||||
*/
|
||||
fn get_ports() -> Vec<SerialPortInfo> {
|
||||
let all = match serialport::available_ports() {
|
||||
Ok(ports) => ports,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
// exclude ports that contai "cu" in their name
|
||||
all.into_iter()
|
||||
.filter(|port| !port.port_name.contains("cu"))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/* @param: port_list -> list of available ports
|
||||
*
|
||||
* @return: Vec<(String, UsbPortInfo)> -> list of ports with their names and usb information
|
||||
*
|
||||
* @description: This function extracts the USB serial devices from the given list of ports.
|
||||
*/
|
||||
fn extract_usb_serial_devices(
|
||||
&self,
|
||||
port_list: Vec<SerialPortInfo>,
|
||||
) -> Vec<(String, UsbPortInfo)> {
|
||||
let mut usb_ports: Vec<(String, UsbPortInfo)> = 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 {
|
||||
match &port.port_type {
|
||||
SerialPortType::UsbPort(usb_info) => {
|
||||
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
if let SerialPortType::UsbPort(usb_info) = port.port_type {
|
||||
map.insert(port.port_name, usb_info);
|
||||
}
|
||||
}
|
||||
|
||||
return usb_ports;
|
||||
map
|
||||
}
|
||||
|
||||
pub async fn check_ports(&mut self) -> CheckPortsResult {
|
||||
// get available ports
|
||||
/// 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();
|
||||
|
||||
// extract all ports that are usb ports
|
||||
let usb_ports = self.extract_usb_serial_devices(ports);
|
||||
|
||||
let last_usb_ports = self
|
||||
.ports
|
||||
.iter()
|
||||
.map(|last_usb_port| (last_usb_port.0.clone(), last_usb_port.1.0.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let usb_ports_diff = compare_lists(&last_usb_ports, &usb_ports);
|
||||
|
||||
let mut result = CheckPortsResult {
|
||||
added: Vec::new(),
|
||||
removed: Vec::new(),
|
||||
};
|
||||
|
||||
// reamove ports
|
||||
for removed in usb_ports_diff.removed {
|
||||
if let Some((_, device_identification, _)) = self.ports.remove(&removed.0) {
|
||||
result.removed.push(device_identification);
|
||||
}
|
||||
|
||||
tracing::trace!("Removed port: {}", removed.0);
|
||||
}
|
||||
|
||||
// add new ports
|
||||
for added in usb_ports_diff.added {
|
||||
// add the port to the list
|
||||
let serial_device_identification = SerialDeviceIdentification {
|
||||
vendor_id: added.1.vid,
|
||||
product_id: added.1.pid,
|
||||
};
|
||||
|
||||
let device_result = self.serial_device_registry.new_serial_device(
|
||||
&SerialDeviceNewParams {
|
||||
path: added.0.clone(),
|
||||
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
|
||||
},
|
||||
&serial_device_identification,
|
||||
);
|
||||
|
||||
// only if created device driver sucessfully
|
||||
if let Ok((device_identification, device)) = device_result {
|
||||
// add the device to the ports list
|
||||
self.ports.insert(
|
||||
added.0.clone(),
|
||||
(
|
||||
added.1.clone(),
|
||||
device_identification.clone(),
|
||||
device.clone(),
|
||||
),
|
||||
);
|
||||
|
||||
// add to result list
|
||||
result.added.push((device_identification, device.clone()));
|
||||
|
||||
tracing::trace!("Added port: {}", added.0);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
let usb_devices = Self::extract_usb_serial_devices(ports);
|
||||
usb_devices
|
||||
}
|
||||
|
||||
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
|
||||
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
|
||||
match self.device_removal_signal_rx.try_recv() {
|
||||
Ok((path, error)) => {
|
||||
// remove the device when the tuple positon 1 equals signal
|
||||
if let Some(info) = self.ports.get(&path.clone()) {
|
||||
removed_signals.push(info.1.clone());
|
||||
};
|
||||
self.ports.remove(&path);
|
||||
pub fn device_port_exists(device_path: &str) -> bool {
|
||||
let ports = Self::get_ports();
|
||||
return ports.iter().any(|p| p.port_name == device_path);
|
||||
}
|
||||
|
||||
tracing::trace!("Removed device: {:?}: {}", path, error);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
removed_signals
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CheckPortsResult {
|
||||
pub added: Vec<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>)>,
|
||||
pub removed: Vec<DeviceIdentification>,
|
||||
}
|
||||
@@ -21,6 +21,10 @@ impl std::fmt::Debug for GenericEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BuildEvent: Serialize + Send + Sync + Clone {
|
||||
fn build(&self) -> Event<Self>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Event<T>
|
||||
where
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod event;
|
||||
pub mod namespace;
|
||||
pub mod namespace_id;
|
||||
@@ -4,7 +4,7 @@ use socketioxide::extract::SocketRef;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Namespace {
|
||||
pub sockets: Vec<SocketRef>,
|
||||
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
|
||||
@@ -30,7 +30,7 @@ impl Namespace {
|
||||
#[instrument(skip_all)]
|
||||
pub fn subscribe(&mut self, socket: SocketRef) {
|
||||
// add the socket to the list
|
||||
self.sockets.push(socket.clone());
|
||||
self.sockets.push(socket);
|
||||
}
|
||||
|
||||
/// Removes a socket from the namespace.
|
||||
@@ -81,7 +81,7 @@ impl Namespace {
|
||||
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");
|
||||
self.send_to_queue(&socket, event, "reemit");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,13 +97,10 @@ impl Namespace {
|
||||
fn cache(
|
||||
&mut self,
|
||||
event: Arc<GenericEvent>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<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);
|
||||
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.
|
||||
@@ -117,11 +114,10 @@ impl Namespace {
|
||||
pub fn emit(
|
||||
&mut self,
|
||||
event: Arc<GenericEvent>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>,
|
||||
) {
|
||||
// cache the event
|
||||
self.cache(event.clone(), &buffer_fn);
|
||||
|
||||
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() {
|
||||
@@ -171,7 +167,7 @@ impl Namespace {
|
||||
|
||||
impl Drop for Namespace {
|
||||
fn drop(&mut self) {
|
||||
self.disconnect_all();
|
||||
//self.disconnect_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +187,7 @@ pub trait CacheableEvents<Events> {
|
||||
fn event_cache_fn(&self) -> CacheFn;
|
||||
}
|
||||
|
||||
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>;
|
||||
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>)>;
|
||||
|
||||
/// [`BufferFn`] that stores the last n events
|
||||
pub fn cache_n_events(n: usize) -> CacheFn {
|
||||
@@ -223,7 +219,6 @@ pub fn cache_first_and_last_event() -> CacheFn {
|
||||
if events.len() == 2 {
|
||||
events.remove(1);
|
||||
events.push(event.clone());
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>()))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::acceleration;
|
||||
|
||||
@meter_per_minute_per_second: 0.016_666_666_666_666_67; "m/min/s", "meters per minute per second",
|
||||
"meters per minute per second";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::angular_acceleration;
|
||||
|
||||
@revolution_per_minute_per_second: 0.104_719_755_119_659_77; "rev/min/s", "revolutions per minute per second",
|
||||
"revolutions per minute per second";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::angular_jerk;
|
||||
|
||||
@revolution_per_minute_per_second_squared: 0.104_719_755_119_659_77; "rev/min/s²", "revolution per minute per second squared",
|
||||
"revolution per minute per second squared";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::jerk;
|
||||
|
||||
@meter_per_minute_per_second_squared: 0.016_666_666_666_666_666_67; "m/min/s²", "meters per minute per second squared",
|
||||
"meters per minute per second squared";
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod acceleration;
|
||||
pub mod angular_acceleration;
|
||||
pub mod angular_jerk;
|
||||
pub mod jerk;
|
||||
pub mod velocity;
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::velocity;
|
||||
|
||||
@meter_per_minute: 0.016_666_666_666_666_67; "m/min", "meters per minute",
|
||||
"meters per minute";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Generate 18 random alphanumeric characters base64 encode It
|
||||
password=$(head -c 18 /dev/urandom | base64)
|
||||
|
||||
echo "Basic Authentication; user=machine password=$password"
|
||||
echo $password > /tmp/machine_password
|
||||
|
||||
caddy_hash=$(caddy hash-password --plaintext $password)
|
||||
|
||||
echo "(machine_basic_auth) {
|
||||
basic_auth {
|
||||
machine $caddy_hash
|
||||
}
|
||||
}" | sudo tee /var/lib/caddy/auth_snippet.conf
|
||||
|
||||
# Transfer ownership to caddy user
|
||||
sudo chown caddy:caddy /var/lib/caddy/auth_snippet.conf
|
||||
|
||||
# Tigther permissions (Only readable by root or caddy user)
|
||||
sudo chmod 600 /var/lib/caddy/auth_snippet.conf
|
||||
|
||||
sudo systemctl restart caddy.service
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 557 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 68 KiB |