Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45aa4e6d79 | ||
|
|
11deedb75d | ||
|
|
50a61f9e21 | ||
|
|
2df5a06930 | ||
|
|
0ae30ba8fb | ||
|
|
2b80d6c806 | ||
|
|
fd2ff77b62 | ||
|
|
46e699b745 | ||
|
|
5e0dbf9d7e | ||
|
|
eefc9b3834 | ||
|
|
bbc9783961 | ||
|
|
3c998d2f5c | ||
|
|
52cd1ce050 | ||
|
|
5140410006 | ||
|
|
852beb5a06 | ||
|
|
2cf150a541 | ||
|
|
4f2f1b227c | ||
|
|
7c1f77ff7b | ||
|
|
8b2b769435 | ||
|
|
5350ca8f53 | ||
|
|
14f4ac1248 | ||
|
|
548ecf5723 | ||
|
|
99e74759b5 | ||
|
|
ec177906c8 | ||
|
|
afdf38eaf2 | ||
|
|
d8d509812b | ||
|
|
3eeeadab5c | ||
|
|
657c3cff85 | ||
|
|
7ea7721ea3 | ||
|
|
b5ccf10128 | ||
|
|
a5a0b1a5e5 | ||
|
|
1e8ef812e9 | ||
|
|
4965e73b0c | ||
|
|
6db2b91052 | ||
|
|
a930d058c2 | ||
|
|
5b7acbc7f3 | ||
|
|
344d319d12 | ||
|
|
d5534e0967 | ||
|
|
680cb71dde | ||
|
|
a26ccb0a1f | ||
|
|
9fa037a9f9 | ||
|
|
f07dcbf8f8 | ||
|
|
783ee38a3a | ||
|
|
4a7be41dfe | ||
|
|
8b6960ae64 | ||
|
|
cf0b71bb69 | ||
|
|
e5608d061f | ||
|
|
7eba9523d4 | ||
|
|
5ad85b1a8c | ||
|
|
064a904975 | ||
|
|
ccbab4afba | ||
|
|
79870d995a | ||
|
|
0d95f1c44f | ||
|
|
62aabaa36a | ||
|
|
b57b746379 | ||
|
|
d1dbe7550d | ||
|
|
38d2364ac4 | ||
|
|
581b4f7773 | ||
|
|
0550ecec7b | ||
|
|
3071482a87 | ||
|
|
0c926a5acd | ||
|
|
59387eb214 | ||
|
|
9693af313f | ||
|
|
df1beabfa8 | ||
|
|
9630142921 | ||
|
|
fcef922ab1 | ||
|
|
edd8c83402 | ||
|
|
09d9801482 | ||
|
|
f50997ec23 | ||
|
|
5524e9f10a | ||
|
|
7e73e2f879 | ||
|
|
96719dae15 | ||
|
|
e78e9ba9e3 | ||
|
|
a730ed3228 | ||
|
|
c3b0a8643d | ||
|
|
422d98bc7c | ||
|
|
f1b61dad9b | ||
|
|
c97768975b | ||
|
|
0b882e8bde | ||
|
|
6752b503bc | ||
|
|
847963fb1c | ||
|
|
4441b60d4a | ||
|
|
cb74f4fb6a | ||
|
|
fae9c7151c | ||
|
|
7aa36c0361 | ||
|
|
eb4b4e948d | ||
|
|
13af890e29 | ||
|
|
d366f52127 | ||
|
|
8f8535b6a3 | ||
|
|
83fa27c7be | ||
|
|
8295496671 | ||
|
|
9277521dbd | ||
|
|
3797fc6469 | ||
|
|
9700e47358 | ||
|
|
8ee1c63e8d | ||
|
|
dae3def188 | ||
|
|
fa3dc0aa8c | ||
|
|
69fcdb969d | ||
|
|
9b03b4d909 | ||
|
|
5e9fb93a21 | ||
|
|
b6842a71fb | ||
|
|
f89645a516 | ||
|
|
d928a4ba81 | ||
|
|
c457b21e64 | ||
|
|
c63be3688b | ||
|
|
21c40ac7ad | ||
|
|
0938f68d8d | ||
|
|
87e903781b | ||
|
|
6753f92128 | ||
|
|
59353ff06f | ||
|
|
938bbc97a6 | ||
|
|
bb02eeb2b0 | ||
|
|
7bc91d73a6 | ||
|
|
59783bc2c3 | ||
|
|
d93ff842ab | ||
|
|
78417edcb8 | ||
|
|
f0f1c2c69e | ||
|
|
b4043d1a75 | ||
|
|
3614f812b9 | ||
|
|
26d3af414f | ||
|
|
ec8eb6a835 | ||
|
|
69f0f73087 | ||
|
|
0b3e4c8c96 | ||
|
|
b8777ee4da | ||
|
|
0bc415271e | ||
|
|
efa32e91ee | ||
|
|
f0927513cd | ||
|
|
3599c26c54 | ||
|
|
aa97091b98 | ||
|
|
ea62de3fdc | ||
|
|
b42089e992 | ||
|
|
ebffbb69ec | ||
|
|
c48b5b2292 | ||
|
|
8abd6558d1 | ||
|
|
dc0b4fada3 | ||
|
|
92cacbc6c8 | ||
|
|
4bad318ce7 | ||
|
|
0eeb7f372e | ||
|
|
1a72f3f8bb | ||
|
|
522ace8a4f | ||
|
|
7e776dc360 | ||
|
|
6487ddc6d2 | ||
|
|
4ebd09d2eb | ||
|
|
330b000b6e | ||
|
|
7a817945ee | ||
|
|
47b1b7fc82 | ||
|
|
7f2ea72ad3 | ||
|
|
62553d1a2e | ||
|
|
cff28876ee | ||
|
|
56032ed59c | ||
|
|
73245a31c6 | ||
|
|
f423ac2a14 | ||
|
|
82377c2c25 | ||
|
|
08bd2c8cee | ||
|
|
7205fdea22 | ||
|
|
c1c420ce8f | ||
|
|
576e80cc39 | ||
|
|
a51a871b70 | ||
|
|
f686420e1e | ||
|
|
2237a84d88 | ||
|
|
5c00d28396 | ||
|
|
2b0cff358e | ||
|
|
c3b7f9d050 | ||
|
|
a7fce331a0 | ||
|
|
6dd8573584 | ||
|
|
124418dc7a | ||
|
|
59e8ddbf82 | ||
|
|
366f58f072 | ||
|
|
7a12950c22 | ||
|
|
668000adf9 | ||
|
|
f2a2fee8f6 | ||
|
|
b412fb9d4e | ||
|
|
73de9f2e4c | ||
|
|
f4e5570ad5 | ||
|
|
2d1da2bbf5 | ||
|
|
23cb7ce992 | ||
|
|
941a1edb0c | ||
|
|
19af948deb | ||
|
|
63ea9a0f7d | ||
|
|
784e0f8652 | ||
|
|
e5f6f3f8e6 | ||
|
|
7c47ef0dee | ||
|
|
2a668d8602 | ||
|
|
efe18e9d81 | ||
|
|
cf20bb57b9 | ||
|
|
284f427da0 | ||
|
|
9f55171286 | ||
|
|
d427f50037 | ||
|
|
5bfaf8178f | ||
|
|
d57866e1a6 | ||
|
|
401f1bf7db | ||
|
|
a46dcbd0ae | ||
|
|
d2fea5f23b | ||
|
|
11b78638ec | ||
|
|
d6b6f76b55 | ||
|
|
bfba0b1d62 | ||
|
|
3b93e6ddc6 | ||
|
|
2a0ba7f72a | ||
|
|
ff9fbe6a6e | ||
|
|
7c712faff9 | ||
|
|
5a4ca867a8 | ||
|
|
88633798e7 | ||
|
|
86df807b01 | ||
|
|
3ae04f5547 | ||
|
|
c1ad22266d | ||
|
|
ec4f2517f6 | ||
|
|
4c73710665 | ||
|
|
fcea61979f | ||
|
|
bc68644468 | ||
|
|
652af28c76 | ||
|
|
31d6c5c4b9 | ||
|
|
28fdbd8bce | ||
|
|
314f5b3eed | ||
|
|
8983a67390 | ||
|
|
74d67cc261 | ||
|
|
2c4c64c0b7 | ||
|
|
758816815e | ||
|
|
bf52030506 | ||
|
|
29cf495dd0 | ||
|
|
500917937b | ||
|
|
812080341c | ||
|
|
221fa7edb3 | ||
|
|
fd58786524 | ||
|
|
a3398cbe34 | ||
|
|
b4cb5916bd | ||
|
|
b7e9c3c1eb | ||
|
|
c291c532ec | ||
|
|
edded9e5a6 | ||
|
|
eb2f0f34a4 | ||
|
|
a1fca74d66 | ||
|
|
36089c3e75 | ||
|
|
d27eecd54c | ||
|
|
183166d8d5 | ||
|
|
e01a8268aa | ||
|
|
d4d1969ec3 | ||
|
|
3dc371ce68 | ||
|
|
fba882adb8 | ||
|
|
e8ca268f1c | ||
|
|
3a0840d758 | ||
|
|
409460736d | ||
|
|
879de6ffcc | ||
|
|
d513f1ad84 | ||
|
|
98b65fadb6 | ||
|
|
832bafa217 | ||
|
|
769dc6220f | ||
|
|
a15af859d4 | ||
|
|
9a4d1e288d | ||
|
|
e20791aa6b | ||
|
|
19c4a48150 | ||
|
|
fba5c5c4f3 | ||
|
|
1f05edc79f | ||
|
|
f79c7ba58d | ||
|
|
5e7a6f1523 | ||
|
|
cc41a2e696 | ||
|
|
32ba1b0f75 | ||
|
|
d38b7e8051 | ||
|
|
353be9a4f8 | ||
|
|
ff14618ec0 | ||
|
|
208306cba5 | ||
|
|
7ef2c95b32 | ||
|
|
a41c822618 | ||
|
|
69a3cdbef3 | ||
|
|
ff51018d35 | ||
|
|
a228906eed | ||
|
|
a238fc9246 | ||
|
|
82a7e2fbfa | ||
|
|
23b2fa5faf | ||
|
|
1d78bacbe8 | ||
|
|
043e781049 | ||
|
|
2d23d9db2f | ||
|
|
c260403370 | ||
|
|
8ac9b73bd5 | ||
|
|
609cce0a42 | ||
|
|
ec304649bd | ||
|
|
217edd7db0 | ||
|
|
5d441b2b9a | ||
|
|
157989316e | ||
|
|
7fc5a35b6a | ||
|
|
a93c863688 | ||
|
|
7735a76b1f | ||
|
|
58b0412c68 | ||
|
|
7ade900f46 | ||
|
|
ad678e9c67 | ||
|
|
6455a8b6f1 | ||
|
|
b1b26fb5ae | ||
|
|
d68be75be1 | ||
|
|
f40a5ac3af | ||
|
|
0c2cf6f460 | ||
|
|
b5a989b7fe | ||
|
|
ab9fad2514 | ||
|
|
e3e7cbb20f | ||
|
|
7b1395b803 | ||
|
|
d67db133ea | ||
|
|
d74f3aad96 | ||
|
|
7f2b24f075 | ||
|
|
d609d41628 | ||
|
|
50c241dd59 | ||
|
|
1a6e7da996 | ||
|
|
3a0eaf53f3 | ||
|
|
16a952349f | ||
|
|
c40f256df5 | ||
|
|
8f0cc645ef | ||
|
|
1f436a6abe | ||
|
|
477b44c8bb | ||
|
|
4147528d0e | ||
|
|
2b533f2c57 | ||
|
|
afaf8c919e | ||
|
|
2e87efdb63 | ||
|
|
65fe3626ba | ||
|
|
36bae74613 | ||
|
|
2e34fa11f8 | ||
|
|
3410493c62 | ||
|
|
f13ad8fd0a | ||
|
|
8208762da5 | ||
|
|
1beb01da98 | ||
|
|
e3dcfaec3b | ||
|
|
efbf7bc350 | ||
|
|
6cf769891f | ||
|
|
840e4b8566 | ||
|
|
38341f6968 | ||
|
|
80e9726ba3 | ||
|
|
8afce68864 | ||
|
|
5f7b7544c3 | ||
|
|
27897182f9 | ||
|
|
2af1f0d79c | ||
|
|
4eeaebe355 | ||
|
|
1dc85828d9 | ||
|
|
bf5408cd18 | ||
|
|
d80b06256a | ||
|
|
aa0fb88fd2 | ||
|
|
fa85501578 | ||
|
|
500390e6dd | ||
|
|
bf73f996ca | ||
|
|
cb7fb902be | ||
|
|
8fabc46ad4 | ||
|
|
38a7da7ee2 | ||
|
|
bbae14a727 | ||
|
|
f4609bfb3c | ||
|
|
afa4663176 | ||
|
|
9b806ee8b3 | ||
|
|
4975d2f6a7 | ||
|
|
1a675db872 | ||
|
|
0230ff4f81 | ||
|
|
d8039156ae | ||
|
|
e7105334c4 | ||
|
|
ad04ea0a62 | ||
|
|
0128268719 | ||
|
|
7873975980 | ||
|
|
57883fcb7e | ||
|
|
5979d9497e | ||
|
|
5dade5a633 | ||
|
|
694b46afbe | ||
|
|
5f81b2df5b | ||
|
|
ff47ed3261 | ||
|
|
da84e2a562 | ||
|
|
a272fd57bb | ||
|
|
e2fb623cbb | ||
|
|
d797584637 | ||
|
|
4549d806c9 | ||
|
|
a22d1d0ff1 | ||
|
|
adde259aa0 | ||
|
|
2768609286 | ||
|
|
1973d8841d | ||
|
|
b337031272 | ||
|
|
507b7d507a | ||
|
|
2f231b203c | ||
|
|
6e7a528d6f | ||
|
|
43dd37e8e0 | ||
|
|
11e965ddeb | ||
|
|
eb08493fdc | ||
|
|
27b0386eac | ||
|
|
c4314a45dd | ||
|
|
02a436f053 | ||
|
|
fea487c74e | ||
|
|
7e317c4b23 | ||
|
|
2c02d98890 | ||
|
|
bdbad1dca2 | ||
|
|
202ae24eea | ||
|
|
38db16468e | ||
|
|
1f272f4bce | ||
|
|
ed2b94ce05 | ||
|
|
146365fae2 | ||
|
|
3bf9956c41 | ||
|
|
7d8f833063 | ||
|
|
fcdac437eb | ||
|
|
cb74dbc22b | ||
|
|
a749f98e81 | ||
|
|
634c00cdd1 | ||
|
|
5524be3ce0 | ||
|
|
77bdb2f92c | ||
|
|
163fae21eb | ||
|
|
f58226aa27 | ||
|
|
2742a296ef | ||
|
|
bb204d6efb | ||
|
|
3a7c99cbbd | ||
|
|
7796d20984 | ||
|
|
9b3223c014 | ||
|
|
a5c5ba4b87 | ||
|
|
b53fea1812 | ||
|
|
b71a3456ce | ||
|
|
81cb44ed2b | ||
|
|
8c1fe6df1f | ||
|
|
91fadd1c3d | ||
|
|
28660a0873 | ||
|
|
5171ef5ef4 | ||
|
|
0cfd7eec9f | ||
|
|
2ed2361fdf | ||
|
|
7c8b05241a | ||
|
|
80e6edc474 | ||
|
|
9f13765db6 | ||
|
|
465f8e9f07 | ||
|
|
ebbd4a5d20 | ||
|
|
727a87eb99 | ||
|
|
dd754bdb60 | ||
|
|
830a58874f | ||
|
|
2459d8236e | ||
|
|
35c38dc28c | ||
|
|
45d1bed990 | ||
|
|
6c4f8ac37c | ||
|
|
22d131e9c4 | ||
|
|
232e74c659 | ||
|
|
85f90676e9 | ||
|
|
6a46997f24 | ||
|
|
69b34adb7f | ||
|
|
f7546058bf | ||
|
|
daf45d5990 | ||
|
|
fa4db9c66d | ||
|
|
9ce47bdfd1 | ||
|
|
b0c43e9260 | ||
|
|
6f567a24b6 | ||
|
|
2680a59080 | ||
|
|
0b3479bc16 | ||
|
|
87112035fb | ||
|
|
fbcae62e25 | ||
|
|
a15d708138 | ||
|
|
d4794055e1 | ||
|
|
eb273f928f | ||
|
|
f497a343fb | ||
|
|
ecb27ea068 | ||
|
|
45c56e7d6e | ||
|
|
8c7a71a3f6 | ||
|
|
40b167757a | ||
|
|
83c405abb6 | ||
|
|
4ea2d3bee2 | ||
|
|
4ac533548d | ||
|
|
7b8525f795 | ||
|
|
a6c65bab22 | ||
|
|
eeb0733c63 | ||
|
|
62a10ace1c | ||
|
|
fec87f1ba3 | ||
|
|
b1cfe581f6 | ||
|
|
7993481d0f | ||
|
|
7c74cedf32 | ||
|
|
3ec3d0bbc7 | ||
|
|
1fca8821b5 | ||
|
|
59b0cde17f | ||
|
|
b2e1019222 | ||
|
|
66be2a143a | ||
|
|
1db27548fb | ||
|
|
cba84b93f0 | ||
|
|
8dbd6a3a91 | ||
|
|
0bab7e53a7 | ||
|
|
fa0611b1bb | ||
|
|
156e69eb0d | ||
|
|
626115b284 | ||
|
|
54c264d03f | ||
|
|
f1399332bb | ||
|
|
943f1081a8 | ||
|
|
db039c7ccc | ||
|
|
7f7984ebe4 | ||
|
|
3752a69c78 | ||
|
|
42867bf41c | ||
|
|
b3f06b4973 | ||
|
|
710c6cb2dd | ||
|
|
d0a98557d3 | ||
|
|
416475aaea | ||
|
|
b2c7f6e0b1 | ||
|
|
7e26bb787d | ||
|
|
e7d3581e94 | ||
|
|
7ca76accc4 | ||
|
|
2f41582e9d | ||
|
|
7b09d1b42f | ||
|
|
17056d61e9 | ||
|
|
49b25a017a | ||
|
|
2b9dc727ba | ||
|
|
229ac7cace | ||
|
|
73ec7ef2ab | ||
|
|
0558b9a125 | ||
|
|
bd373024f1 | ||
|
|
22eb39522b | ||
|
|
93c42078a9 | ||
|
|
29bf15a266 | ||
|
|
a3541e7906 | ||
|
|
fcfeb448d6 | ||
|
|
4d8c15df31 | ||
|
|
9ff63b7389 | ||
|
|
1df8504c4e | ||
|
|
886a5a2a2a | ||
|
|
34142b8063 | ||
|
|
c8eb44b697 | ||
|
|
f0dd0c5a74 | ||
|
|
a108384305 | ||
|
|
37a82b1bb8 | ||
|
|
df03e4c2a2 | ||
|
|
4b0321975b | ||
|
|
43d495db38 | ||
|
|
46aff0e080 | ||
|
|
a65ec0b920 | ||
|
|
a3ef0f5bbe | ||
|
|
d91d413aaf | ||
|
|
4e7cb845d6 | ||
|
|
677276a2b7 | ||
|
|
a846928ff5 | ||
|
|
f760d3567a | ||
|
|
e2ebaf69a0 | ||
|
|
0b160581b9 | ||
|
|
1a7fe8b93d | ||
|
|
b745524257 | ||
|
|
14cd458d60 | ||
|
|
224a3cb66f | ||
|
|
25be1678dc | ||
|
|
7a05ce93b5 | ||
|
|
d556f0225b | ||
|
|
4845036256 | ||
|
|
0cf761f529 | ||
|
|
3b414192e9 | ||
|
|
736f75f413 | ||
|
|
db5046f7e1 | ||
|
|
eb6758bf3b | ||
|
|
7a77d3ba73 | ||
|
|
eba54a4acb | ||
|
|
1049dd22b3 | ||
|
|
cee244da23 | ||
|
|
fcf6000e8d | ||
|
|
74ce4ad340 | ||
|
|
73c5de46a5 | ||
|
|
36bbd92ee1 | ||
|
|
30cab528f9 | ||
|
|
3dfe362983 | ||
|
|
a711542863 | ||
|
|
bc00962d8e | ||
|
|
0776506e30 | ||
|
|
c8fdfc79ee | ||
|
|
51b797f9dd | ||
|
|
6f50515144 | ||
|
|
1911604fc3 | ||
|
|
f507f27d13 | ||
|
|
68260bc379 | ||
|
|
b63adbb601 | ||
|
|
754533834b | ||
|
|
0d8fb2789f | ||
|
|
680ecd9483 | ||
|
|
92414e42b1 | ||
|
|
74fbb8ee4b | ||
|
|
0361e04e2e | ||
|
|
8f3894f68e | ||
|
|
cd9b034ef2 | ||
|
|
5d79eaff5b | ||
|
|
2c8f1e6989 | ||
|
|
17934bdf08 | ||
|
|
0438a6e641 | ||
|
|
0019db3394 |
No files matched your search
@@ -36,6 +36,9 @@ jobs:
|
||||
- name: Install
|
||||
run: npm i
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
node_modules/
|
||||
@@ -1 +1,88 @@
|
||||
# Unreleased
|
||||
# `Unreleased`
|
||||
|
||||
## 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
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# `2.2.0`
|
||||
|
||||
_15.07.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- [#455](https://github.com/qitechgmbh/control/pull/458), Greatly improved stability and precision of pressure-based regulation for the motor
|
||||
- [#467](https://github.com/qitechgmbh/control/pull/468), [#448](https://github.com/qitechgmbh/control/pull/449), [#485](https://github.com/qitechgmbh/control/pull/486) Improved the Responsiveness for motor control and monitoring
|
||||
- [#138](https://github.com/qitechgmbh/control/pull/495), Added a Warning in case the Pressure sensor is disconnected or incorrectly wired
|
||||
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
|
||||
|
||||
## Winder V2
|
||||
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
|
||||
|
||||
## General
|
||||
|
||||
- [#121](https://github.com/qitechgmbh/control/pull/121), Use Linux realtime threads for EtherCAT and control loop (PREEMPT_RT)
|
||||
- [#138](https://github.com/qitechgmbh/control/pull/495), Updated Quick Start Guide
|
||||
|
||||
# `2.1.0`
|
||||
|
||||
_30.06.2025_
|
||||
|
||||
## Extruder V2
|
||||
|
||||
- Intial Stable Release of **Extruder V2**
|
||||
|
||||
## Winder V2
|
||||
|
||||
- [#420](https://github.com/qitechgmbh/control/pull/429), [#401](https://github.com/qitechgmbh/control/pull/401) Add _Adaptive_ spool speed algorithm, the old algorithm is now called _Minmax_ ans tillavailable.
|
||||
- [#427](https://github.com/qitechgmbh/control/pull/427) Add _Estimated Spool Diameter_ graph
|
||||
- [#419](https://github.com/qitechgmbh/control/pull/419) Fix Issue where traverse won't work at low spool speed
|
||||
|
||||
## Laser V1
|
||||
|
||||
- Initial Stable Release of **Laser V1**
|
||||
|
||||
## HMI Panel
|
||||
|
||||
- [#339](https://github.com/qitechgmbh/control/pull/338) Don't go into sleep Mmde
|
||||
- [#410](https://github.com/qitechgmbh/control/pull/410) Ship with Wireshark
|
||||
- [#424](https://github.com/qitechgmbh/control/pull/424) Updates can be canceled
|
||||
|
||||
## General
|
||||
|
||||
- [#269](https://github.com/qitechgmbh/control/pull/269), [#370](https://github.com/qitechgmbh/control/pull/370) Improved performace of live values
|
||||
- [#358](https://github.com/qitechgmbh/control/pull/358), [#343](https://github.com/qitechgmbh/control/pull/343), [#324](https://github.com/qitechgmbh/control/pull/324), [#390](https://github.com/qitechgmbh/control/pull/390), [#299](https://github.com/qitechgmbh/control/pull/299) Improved functionality for graphs
|
||||
- [#405](https://github.com/qitechgmbh/control/pull/405) Remove weird color behaviour on tocuhscreens
|
||||
- [#369](https://github.com/qitechgmbh/control/pull/369) More failsafe EtherCAT loop
|
||||
|
||||
# `2.0.1`
|
||||
|
||||
_10.06.2025_
|
||||
|
||||
# `2.0.0`
|
||||
|
||||
_10.06.2025_
|
||||
|
||||
## General
|
||||
|
||||
- Initial stable release of QiTech Control.
|
||||
|
||||
## Winder V2
|
||||
|
||||
- Initial stable release of **Winder V2**
|
||||
|
||||
## Features
|
||||
|
||||
- Stabilized **Winder V2**
|
||||
@@ -41,6 +41,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
|
||||
# Dev Setup
|
||||
|
||||
[Developer Documentation](./docs/developer-docs/)
|
||||
|
||||
## Backend
|
||||
|
||||
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
|
||||
@@ -63,10 +65,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
| 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 | |
|
||||
| DRE | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | 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
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
@@ -90,7 +91,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
- [X] [Threading](./docs/control-loop.md)
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $1 == "release" ]; then
|
||||
FEATURE=""
|
||||
|
||||
# Check if "mock-machine" was passed as an argument
|
||||
if [[ "$1" == "mock-machine" ]]; then
|
||||
echo "Building Debug Code with mock-machine feature"
|
||||
FEATURE="--features mock-machine"
|
||||
fi
|
||||
if [[ "$1" == "release" ]]; then
|
||||
echo "building Release Code"
|
||||
cargo build --release
|
||||
else
|
||||
echo "building Debug Code"
|
||||
cargo build
|
||||
echo "Building Debug Code"
|
||||
cargo build $FEATURE
|
||||
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
|
||||
fi
|
||||
# set cap for server executable
|
||||
sudo setcap cap_net_raw=eip ./target/debug/server
|
||||
# run
|
||||
./target/debug/server
|
||||
|
||||
@@ -9,15 +9,22 @@ axum = "0.8.4"
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
ethercrab = "0.6"
|
||||
interfaces = "0.0.9"
|
||||
log = "0.4.27"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serialport = "4.7.1"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.0"
|
||||
socketioxide = "0.17.1"
|
||||
uom = "0.36.0"
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.2.1"
|
||||
crc = "3.3.0"
|
||||
serial = "0.4.0"
|
||||
tracing = "0.1.41"
|
||||
erased-serde = "0.4.6"
|
||||
core_affinity = "0.8.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.172"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
textplots = "0.8"
|
||||
@@ -1,91 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::analog_output::AnalogOutput;
|
||||
use std::{
|
||||
f64::consts::PI,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use uom::si::{angle::radian, f64::Angle, ratio::ratio};
|
||||
pub type AnalogFunction = Box<dyn Fn(Duration) -> f64 + Send + Sync>;
|
||||
|
||||
/// Can module analog output with a function
|
||||
pub struct AnalogFunctionGenerator {
|
||||
output: AnalogOutput,
|
||||
function: AnalogFunction,
|
||||
start_ts: Instant,
|
||||
}
|
||||
|
||||
impl AnalogFunctionGenerator {
|
||||
pub fn new(output: AnalogOutput, function: AnalogFunction) -> Self {
|
||||
Self {
|
||||
output,
|
||||
function,
|
||||
start_ts: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AnalogFunctionGenerator {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let diff = now - self.start_ts;
|
||||
let value = (self.function)(diff);
|
||||
(self.output.write)((value as f32).into()).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn analog_sine(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
// Convert to seconds as f64 for calculation
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let angle = Angle::new::<radian>(2.0 * PI * phase_secs / period_secs);
|
||||
let value = angle.sin().get::<ratio>();
|
||||
value * (amplitude / 2.0) + normal + (amplitude / 2.0)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn analog_multiply<const N: usize>(functions: [AnalogFunction; N]) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| functions.iter().fold(1.0, |acc, func| acc * func(dt)))
|
||||
}
|
||||
|
||||
pub fn analog_sawtooth(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let value = phase_secs / period_secs;
|
||||
value * amplitude + normal
|
||||
})
|
||||
}
|
||||
|
||||
pub fn analog_square(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let value = if phase_secs < period_secs / 2.0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
value * amplitude + normal
|
||||
})
|
||||
}
|
||||
|
||||
pub fn analog_triangle(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
|
||||
Box::new(move |dt: Duration| {
|
||||
let dt_secs = dt.as_secs_f64();
|
||||
let period_secs = period.as_secs_f64();
|
||||
let phase_secs = dt_secs % period_secs;
|
||||
let value = if phase_secs < period_secs / 2.0 {
|
||||
phase_secs / period_secs * 2.0
|
||||
} else {
|
||||
2.0 - phase_secs / period_secs * 2.0
|
||||
};
|
||||
value * amplitude + normal
|
||||
})
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a analog input
|
||||
#[derive(Debug)]
|
||||
pub struct AnalogInputGetter {
|
||||
input: AnalogInput,
|
||||
normalized: Option<f32>,
|
||||
}
|
||||
|
||||
impl AnalogInputGetter {
|
||||
pub fn new(input: AnalogInput) -> Self {
|
||||
Self {
|
||||
input,
|
||||
normalized: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Value from -1.0 to 1.0
|
||||
pub fn get_normalized(&self) -> Option<f32> {
|
||||
self.normalized
|
||||
}
|
||||
|
||||
pub fn get_physical(&self) -> Option<AnalogInputValue> {
|
||||
match self.get_normalized() {
|
||||
Some(normalized) => Some(self.input.range.normalized_to_physical(normalized)),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AnalogInputGetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)().await;
|
||||
self.normalized = Some(state.input.normalized);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a digital input
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalInputGetter {
|
||||
input: DigitalInput,
|
||||
value: bool,
|
||||
}
|
||||
|
||||
impl DigitalInputGetter {
|
||||
pub fn new(input: DigitalInput) -> Self {
|
||||
Self {
|
||||
input,
|
||||
value: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current value of the digital input
|
||||
pub fn value(&self) -> bool {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalInputGetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)()
|
||||
.await
|
||||
.expect("Failed to get digital input state");
|
||||
self.value = state.input.value;
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a digital input
|
||||
pub struct DigitalInputLogger {
|
||||
input: DigitalInput,
|
||||
}
|
||||
|
||||
impl DigitalInputLogger {
|
||||
pub fn new(input: DigitalInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalInputLogger {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)()
|
||||
.await
|
||||
.expect("Failed to get digital input state");
|
||||
println!("DigitalInputLogger: {}", state.input.value);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalOutputBlinker {
|
||||
last_toggle: Instant,
|
||||
output: DigitalOutput,
|
||||
interval: Duration,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl DigitalOutputBlinker {
|
||||
pub fn new(output: DigitalOutput, interval: Duration) -> Self {
|
||||
Self {
|
||||
last_toggle: Instant::now(),
|
||||
output,
|
||||
interval,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
pub fn set_interval(&mut self, interval: Duration) {
|
||||
self.interval = interval;
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalOutputBlinker {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
{
|
||||
let state = (self.output.state)().await;
|
||||
if now - self.last_toggle > self.interval {
|
||||
match state.output.into() {
|
||||
true => (self.output.write)(false.into()).await,
|
||||
false => (self.output.write)(true.into()).await,
|
||||
}
|
||||
self.last_toggle = now;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Set a series of digital outputs high in sequence with a given interval
|
||||
pub struct DigitalOutputBlinkers {
|
||||
last_toggle: Instant,
|
||||
outputs: Vec<Option<DigitalOutput>>,
|
||||
index: usize,
|
||||
interval: Duration,
|
||||
enabled: bool,
|
||||
amount: usize,
|
||||
}
|
||||
|
||||
impl DigitalOutputBlinkers {
|
||||
pub fn new(outputs: Vec<Option<DigitalOutput>>, interval: Duration, amount: usize) -> Self {
|
||||
Self {
|
||||
last_toggle: Instant::now(),
|
||||
outputs,
|
||||
interval,
|
||||
enabled: true,
|
||||
index: 0,
|
||||
amount,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_interval(&mut self, interval: Duration) {
|
||||
self.interval = interval;
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalOutputBlinkers {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if now - self.last_toggle > self.interval {
|
||||
if let Some(output) = &self.outputs[self.index] {
|
||||
(output.write)(true.into()).await;
|
||||
}
|
||||
let index_end =
|
||||
(self.index + self.outputs.len() - self.amount) % self.outputs.len();
|
||||
if let Some(output) = &self.outputs[index_end] {
|
||||
(output.write)(false.into()).await;
|
||||
}
|
||||
self.last_toggle = now;
|
||||
self.index = (self.index + 1) % self.outputs.len();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalOutputSetter {
|
||||
output: DigitalOutput,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl DigitalOutputSetter {
|
||||
pub fn new(output: DigitalOutput) -> Self {
|
||||
Self {
|
||||
output,
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
pub fn set(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
pub fn get(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
pub fn toggle(&mut self) {
|
||||
self.enabled = !self.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for DigitalOutputSetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
{
|
||||
(self.output.write)(self.enabled.into()).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,525 +0,0 @@
|
||||
use super::Actor;
|
||||
use crate::modbus::{
|
||||
ModbusFunctionCode, ModbusRequest, ModbusResponse, calculate_modbus_rtu_timeout,
|
||||
};
|
||||
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use uom::{
|
||||
ConstZero,
|
||||
si::{f64::Frequency, frequency::centihertz},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
/// WaitingForResponse is set after sending a request through the serial_interface
|
||||
WaitingForResponse,
|
||||
/// After Sending a Resuest we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has transmit accepted toggled
|
||||
/// Then we can set state = ReadyToSend
|
||||
WaitingForRequestAccept,
|
||||
/// After Receiving a Response we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has received accepted toggled
|
||||
WaitingForReceiveAccept,
|
||||
/// ReadyToSend is set after receiving the response from the serial_interface
|
||||
ReadyToSend,
|
||||
/// Initial State
|
||||
Uninitialized,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OperationMode {
|
||||
PU,
|
||||
EXT,
|
||||
NET,
|
||||
}
|
||||
|
||||
impl MitsubishiInverterRS485Actor {
|
||||
pub fn convert_hz_float_to_word(&mut self, value: Frequency) -> u16 {
|
||||
let scaled = value.get::<centihertz>(); // Convert Hz to 0.01 Hz units
|
||||
scaled.round() as u16
|
||||
}
|
||||
|
||||
pub fn set_frequency_target(&mut self, frequency: Frequency) {
|
||||
let mut request: MitsubishiModbusRequest =
|
||||
MitsubishiControlRequests::WriteRunningFrequency.into();
|
||||
let result: u16 = self.convert_hz_float_to_word(frequency); // convert hz float to short
|
||||
request.request.data[2] = result.to_le_bytes()[1];
|
||||
request.request.data[3] = result.to_le_bytes()[0];
|
||||
self.add_request(request);
|
||||
}
|
||||
|
||||
pub fn read_running_frequency() {
|
||||
// Check if the current element pushed to front is freq event
|
||||
}
|
||||
|
||||
pub fn switch_operation_mode() {}
|
||||
}
|
||||
|
||||
/// Specifies all System environmet Variables
|
||||
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> address:0x1
|
||||
enum MitsubishiSystemRegister {
|
||||
/// Register 40002
|
||||
InverterReset,
|
||||
/// Register 40003
|
||||
ParameterClear,
|
||||
/// Register 40004
|
||||
AllParameterClear,
|
||||
/// Register 40006
|
||||
ParamClearNonCommunication,
|
||||
/// Register 40007
|
||||
AllParameterClearNonCommunication,
|
||||
/// Register 40009
|
||||
InverterStatusAndControl,
|
||||
/// Register 40010
|
||||
OperationModeAndSetting,
|
||||
/// Register 40014
|
||||
RunningFrequencyRAM,
|
||||
/// Register 40015
|
||||
RunningFrequencyEEPROM,
|
||||
/// Register 40201
|
||||
MotorFrequency,
|
||||
}
|
||||
|
||||
impl MitsubishiControlRequests {
|
||||
fn get_system_register(value: MitsubishiSystemRegister) -> u16 {
|
||||
match value {
|
||||
MitsubishiSystemRegister::InverterReset => 0x1,
|
||||
MitsubishiSystemRegister::ParameterClear => 0x2,
|
||||
MitsubishiSystemRegister::AllParameterClear => 0x3,
|
||||
MitsubishiSystemRegister::ParamClearNonCommunication => 0x5,
|
||||
MitsubishiSystemRegister::AllParameterClearNonCommunication => 0x6,
|
||||
MitsubishiSystemRegister::InverterStatusAndControl => 0x8,
|
||||
MitsubishiSystemRegister::OperationModeAndSetting => 0x9,
|
||||
MitsubishiSystemRegister::RunningFrequencyRAM => 0x0d,
|
||||
MitsubishiSystemRegister::RunningFrequencyEEPROM => 0x0e,
|
||||
MitsubishiSystemRegister::MotorFrequency => 0x00C8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MitsubishiControlRequests> for MitsubishiModbusRequest {
|
||||
fn from(request: MitsubishiControlRequests) -> Self {
|
||||
match request {
|
||||
MitsubishiControlRequests::WriteRunningFrequency => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::RunningFrequencyRAM,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x0],
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::WriteFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ReadInverterStatus => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::ReadHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterStatus,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::StopMotor => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Value 1 to stop
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterControl,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::StartForwardRotation => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000010], // Value 2 for forward rotation
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterControl,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::StartReverseRotation => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterStatusAndControl,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000100], // Value 4 for reverse rotation
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::InverterControl,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ReadRunningFrequency => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::RunningFrequencyRAM,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::ReadHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::ReadFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ReadMotorFrequency => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::MotorFrequency,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::ReadHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x1],
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::ReadMotorFrequency,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ResetInverter => {
|
||||
let reg: u16 = MitsubishiControlRequests::get_system_register(
|
||||
MitsubishiSystemRegister::InverterReset,
|
||||
);
|
||||
let reg_bytes = reg.to_be_bytes();
|
||||
MitsubishiModbusRequest {
|
||||
request: ModbusRequest {
|
||||
slave_id: 1,
|
||||
function_code: ModbusFunctionCode::PresetHoldingRegister,
|
||||
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0b00000001],
|
||||
},
|
||||
request_type: RequestType::OperationCommand,
|
||||
expected_response_type: ResponseType::NoResponse,
|
||||
}
|
||||
}
|
||||
MitsubishiControlRequests::ClearAllParameters => todo!(),
|
||||
MitsubishiControlRequests::ClearNonCommunicationParameter => todo!(),
|
||||
MitsubishiControlRequests::ClearNonCommunicationParameters => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// These Requests Serve as Templates for controling the inverter
|
||||
#[derive(Debug)]
|
||||
pub enum MitsubishiControlRequests {
|
||||
/// Register 40002, Reset/Restart the Inverter
|
||||
ResetInverter,
|
||||
/// Register 40004, Clear ALL parameters
|
||||
ClearAllParameters,
|
||||
/// Register 40006, Clear a non communication parameter
|
||||
ClearNonCommunicationParameter,
|
||||
/// Register 40007, Clear all Non Communication related Parameters
|
||||
ClearNonCommunicationParameters,
|
||||
/// Register 40009, Read Inverter Status
|
||||
ReadInverterStatus,
|
||||
/// Register 40009, Stops the Motor
|
||||
StopMotor,
|
||||
/// Register 40009, Starts the Motor in Forward Rotation
|
||||
StartForwardRotation,
|
||||
/// Register 40009, Starts the Motor in Reverse Rotation
|
||||
StartReverseRotation,
|
||||
/// Register 40014, Read the current frequency the motor runs at (RAM)
|
||||
ReadRunningFrequency,
|
||||
/// Register 40014, Write the frequency
|
||||
WriteRunningFrequency,
|
||||
/// Read Register 40201, This contains the actual output frequency
|
||||
ReadMotorFrequency,
|
||||
}
|
||||
|
||||
// We need to know from the request queue which events are of what operation type, so that the correct timeout can be used
|
||||
#[derive(Debug)]
|
||||
pub struct MitsubishiModbusRequest {
|
||||
request: ModbusRequest,
|
||||
request_type: RequestType,
|
||||
expected_response_type: ResponseType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RotationDirection {
|
||||
Forward,
|
||||
Backwards,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResponseType {
|
||||
NoResponse,
|
||||
ReadFrequency,
|
||||
ReadMotorFrequency, // Motor Frequency is the actual frequency, that the motor is running at right now
|
||||
ReadInverterStatus,
|
||||
WriteFrequency,
|
||||
InverterStatus,
|
||||
InverterControl,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MitsubishiInverterRS485Actor {
|
||||
// Communication
|
||||
pub serial_interface: SerialInterface,
|
||||
pub baudrate: Option<u32>,
|
||||
pub encoding: Option<SerialEncoding>,
|
||||
pub request_queue: VecDeque<MitsubishiModbusRequest>,
|
||||
pub response_queue: VecDeque<ModbusResponse>,
|
||||
|
||||
// State
|
||||
pub last_ts: Instant,
|
||||
pub last_message_size: usize,
|
||||
pub last_request_type: RequestType,
|
||||
pub state: State,
|
||||
pub next_response_type: ResponseType,
|
||||
pub frequency: Frequency,
|
||||
}
|
||||
|
||||
impl MitsubishiInverterRS485Actor {
|
||||
pub fn new(serial_interface: SerialInterface) -> Self {
|
||||
Self {
|
||||
serial_interface,
|
||||
last_ts: Instant::now(),
|
||||
state: State::Uninitialized,
|
||||
request_queue: VecDeque::new(),
|
||||
response_queue: VecDeque::new(),
|
||||
next_response_type: ResponseType::ReadMotorFrequency,
|
||||
last_request_type: RequestType::OperationCommand,
|
||||
last_message_size: 0,
|
||||
baudrate: None,
|
||||
encoding: None,
|
||||
frequency: Frequency::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// This would get called by the api to add a new request to the inverter
|
||||
pub fn add_request(&mut self, request: MitsubishiModbusRequest) {
|
||||
self.request_queue.push_front(request);
|
||||
}
|
||||
|
||||
/// This is used by the Api to pop off the Response of our Request
|
||||
pub fn get_response(&mut self) -> Option<ModbusResponse> {
|
||||
if self.response_queue.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
self.response_queue.pop_back()
|
||||
}
|
||||
|
||||
/// This is used internally to read the receive buffer of the el6021
|
||||
fn read_modbus_response(
|
||||
&mut self,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ModbusResponse, anyhow::Error>> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if !(self.serial_interface.has_message)().await {
|
||||
// return;
|
||||
}
|
||||
|
||||
let res: Option<Vec<u8>> = (self.serial_interface.read_message)().await;
|
||||
let raw_response = match res {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let response: Result<ModbusResponse, _> =
|
||||
ModbusResponse::try_from(raw_response.clone());
|
||||
|
||||
match response {
|
||||
Ok(result) => {
|
||||
self.last_message_size = result.clone().data.len() + 4;
|
||||
self.state = State::ReadyToSend;
|
||||
Ok(result)
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Error Parsing ModbusResponse!");
|
||||
self.state = State::ReadyToSend;
|
||||
Err(anyhow::anyhow!("error"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// This is used internally to fill the write buffer of the el6021
|
||||
fn send_modbus_request(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
if self.request_queue.len() == 0 {
|
||||
return;
|
||||
};
|
||||
let request: MitsubishiModbusRequest = self.request_queue.pop_back().unwrap();
|
||||
self.next_response_type = request.expected_response_type;
|
||||
self.last_request_type = self.last_request_type;
|
||||
let modbus_request: Vec<u8> = request.request.into();
|
||||
let res = (self.serial_interface.write_message)(modbus_request.clone()).await;
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(_) => log::error!("ERROR: serial_interface.write_message has failed"),
|
||||
}
|
||||
self.state = State::WaitingForRequestAccept;
|
||||
self.last_message_size = modbus_request.len();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestType {
|
||||
/// Monitoring, Operation (start,stop etc) command, frequency setting (RAM), less than 12 milliseconds timeout for Response
|
||||
OperationCommand,
|
||||
/// Parameter Read/Write and Frequency (EEPROM), Less than 30 milliseconds timeout for Response
|
||||
ReadWrite,
|
||||
/// Less than 5 seconds timeout for Response
|
||||
ParamClear,
|
||||
/// no Timeout for Response
|
||||
Reset,
|
||||
}
|
||||
|
||||
impl RequestType {
|
||||
fn timeout_duration(self) -> Duration {
|
||||
match self {
|
||||
RequestType::OperationCommand => Duration::from_millis(12),
|
||||
RequestType::ReadWrite => Duration::from_millis(30),
|
||||
RequestType::ParamClear => Duration::from_millis(5000),
|
||||
RequestType::Reset => Duration::from_millis(30),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MitsubishiModbusExceptionCode {
|
||||
IllegalFunction,
|
||||
IllegalDataAddress,
|
||||
IllegalDataValue,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<MitsubishiModbusExceptionCode> for u8 {
|
||||
fn from(value: MitsubishiModbusExceptionCode) -> Self {
|
||||
match value {
|
||||
MitsubishiModbusExceptionCode::None => 0,
|
||||
MitsubishiModbusExceptionCode::IllegalFunction => 1,
|
||||
MitsubishiModbusExceptionCode::IllegalDataAddress => 2,
|
||||
MitsubishiModbusExceptionCode::IllegalDataValue => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for MitsubishiModbusExceptionCode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => MitsubishiModbusExceptionCode::IllegalFunction,
|
||||
2 => MitsubishiModbusExceptionCode::IllegalDataAddress,
|
||||
3 => MitsubishiModbusExceptionCode::IllegalDataValue,
|
||||
_ => MitsubishiModbusExceptionCode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different Response types
|
||||
impl MitsubishiInverterRS485Actor {
|
||||
// When we get respone from Pr. 40014 (Running Frequency) Convert to rpm and save it
|
||||
fn handle_motor_frequency(&mut self, resp: ModbusResponse) {
|
||||
let freq_bytes = &resp.data[1..3]; // bytes 1 and 2 are needed
|
||||
let raw_frequency = u16::from_be_bytes([freq_bytes[0], freq_bytes[1]]) as f64;
|
||||
self.frequency = Frequency::new::<centihertz>(raw_frequency);
|
||||
}
|
||||
|
||||
// Technically we could verify that every request also was successful with this match and return an Error, or not
|
||||
fn handle_response(&mut self, resp: ModbusResponse) {
|
||||
match self.next_response_type {
|
||||
ResponseType::ReadFrequency => (),
|
||||
ResponseType::ReadInverterStatus => (),
|
||||
ResponseType::WriteFrequency => (),
|
||||
ResponseType::ReadMotorFrequency => self.handle_motor_frequency(resp),
|
||||
ResponseType::InverterStatus => (),
|
||||
ResponseType::InverterControl => (),
|
||||
ResponseType::NoResponse => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for MitsubishiInverterRS485Actor {
|
||||
fn act(&mut self, now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let elapsed: Duration = now_ts.duration_since(self.last_ts);
|
||||
// State is uninitialized until serial interface init returns true, which takes a few cycles on the el6021
|
||||
if let State::Uninitialized = self.state {
|
||||
let res = (self.serial_interface.initialize)().await;
|
||||
if res == true {
|
||||
self.state = State::ReadyToSend;
|
||||
// every time when our inverter is "Uninitialzed" reset it first to clear any error states it may have
|
||||
self.add_request(MitsubishiControlRequests::ResetInverter.into());
|
||||
self.baudrate = (self.serial_interface.get_baudrate)().await;
|
||||
self.encoding = (self.serial_interface.get_serial_encoding)().await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let encoding = match self.encoding {
|
||||
Some(encoding) => encoding,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let baudrate = match self.baudrate {
|
||||
Some(baudrate) => baudrate,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let timeout = calculate_modbus_rtu_timeout(
|
||||
encoding.total_bits(),
|
||||
self.last_request_type.timeout_duration(),
|
||||
baudrate,
|
||||
self.last_message_size,
|
||||
);
|
||||
|
||||
// if we have no requests add ReadMotorFrequency
|
||||
if self.request_queue.is_empty() {
|
||||
self.add_request(MitsubishiControlRequests::ReadMotorFrequency.into());
|
||||
}
|
||||
|
||||
if elapsed < timeout {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_ts = now_ts;
|
||||
match self.state {
|
||||
State::WaitingForResponse => {
|
||||
let ret = self.read_modbus_response().await;
|
||||
match ret {
|
||||
Ok(ret) => self.handle_response(ret),
|
||||
Err(_) => (), // Do nothing for now
|
||||
}
|
||||
}
|
||||
State::ReadyToSend => self.send_modbus_request().await,
|
||||
State::WaitingForReceiveAccept => self.state = State::ReadyToSend,
|
||||
State::WaitingForRequestAccept => self.state = State::WaitingForResponse,
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
pub mod analog_function_generator;
|
||||
pub mod analog_input_getter;
|
||||
pub mod digital_input_getter;
|
||||
pub mod digital_input_logger;
|
||||
pub mod digital_output_blinker;
|
||||
pub mod digital_output_blinkers;
|
||||
pub mod digital_output_setter;
|
||||
pub mod mitsubishi_inverter_rs485;
|
||||
pub mod stepper_driver_el70x1;
|
||||
pub mod stepper_driver_max_speed;
|
||||
pub mod stepper_driver_pulse_train;
|
||||
pub mod temperature_input_getter;
|
||||
pub mod temperature_input_logger;
|
||||
|
||||
use std::{pin::Pin, time::Instant};
|
||||
|
||||
pub trait Actor: Send + Sync {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use super::Actor;
|
||||
use core::panic;
|
||||
use ethercat_hal::{
|
||||
helpers::el70xx_velocity_converter::EL70x1VelocityConverter,
|
||||
io::stepper_velocity_el70x1::StepperVelocityEL70x1, shared_config::el70x1::EL70x1SpeedRange,
|
||||
};
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct StepperDriverEL70x1 {
|
||||
stepper: StepperVelocityEL70x1,
|
||||
enabled: bool,
|
||||
velocity: i16,
|
||||
position: i128,
|
||||
set_position: Option<i128>,
|
||||
pub converter: EL70x1VelocityConverter,
|
||||
}
|
||||
|
||||
impl StepperDriverEL70x1 {
|
||||
pub fn new(stepper: StepperVelocityEL70x1, speed_range: &EL70x1SpeedRange) -> Self {
|
||||
Self {
|
||||
stepper,
|
||||
enabled: false,
|
||||
velocity: 0,
|
||||
converter: EL70x1VelocityConverter::new(speed_range),
|
||||
position: 0,
|
||||
set_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the speed in steps per second
|
||||
pub fn set_speed(&mut self, steps_per_second: i32) {
|
||||
self.velocity = self.converter.steps_to_velocity(steps_per_second, true)
|
||||
}
|
||||
|
||||
/// Get the speed in steps per second
|
||||
pub fn get_speed(&self) -> i32 {
|
||||
self.converter.velocity_to_steps(self.velocity, true) as i32
|
||||
}
|
||||
|
||||
/// Enable or disable the stepper
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
/// Get the enabled state of the stepper
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Get the current position of the stepper
|
||||
pub fn get_position(&self) -> i128 {
|
||||
self.position
|
||||
}
|
||||
|
||||
/// Set the position of the stepper
|
||||
pub fn set_position(&mut self, position: i128) {
|
||||
self.set_position = Some(position);
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for StepperDriverEL70x1 {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = match (self.stepper.state)().await {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
panic!("Error while reading StepperVelocity {:?}", e);
|
||||
}
|
||||
};
|
||||
// set the input
|
||||
self.position = state.input.counter_value;
|
||||
|
||||
let mut output = state.output.clone();
|
||||
|
||||
// set the output
|
||||
output.enable = self.enabled;
|
||||
output.velocity = self.velocity;
|
||||
output.set_counter = self.set_position.take();
|
||||
|
||||
// write the output
|
||||
match (self.stepper.write)(output).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
panic!("Error while writing StepperVelocity {:?}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
pub struct StepperDriverMaxSpeed {
|
||||
pulse: DigitalOutput,
|
||||
}
|
||||
|
||||
impl StepperDriverMaxSpeed {
|
||||
pub fn new(output: DigitalOutput) -> Self {
|
||||
Self { pulse: output }
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for StepperDriverMaxSpeed {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.pulse.state)().await;
|
||||
match state.output.into() {
|
||||
true => (self.pulse.write)(false.into()).await,
|
||||
false => (self.pulse.write)(true.into()).await,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StepperDriverMaxSpeed> for Arc<RwLock<StepperDriverMaxSpeed>> {
|
||||
fn from(actor: StepperDriverMaxSpeed) -> Self {
|
||||
Arc::new(RwLock::new(actor))
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/// Translates the encoder value to a larger space and handle underflow/overflow situations
|
||||
#[derive(Debug)]
|
||||
pub struct Encoder {
|
||||
pub position: i64,
|
||||
pub last_encoder_position: u32,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
position: 0,
|
||||
last_encoder_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, counter_value: u32, counter_overflow: bool, counter_underflow: bool) {
|
||||
// Handle overflow case
|
||||
if counter_overflow {
|
||||
// When overflow occurs, the counter wraps from MAX to 0
|
||||
// Calculate how much it moved past MAX
|
||||
let delta = counter_value as i64;
|
||||
// Add the remaining distance from last position to MAX, plus the overflow amount
|
||||
self.position += (u32::MAX - self.last_encoder_position) as i64 + 1 + delta;
|
||||
}
|
||||
// Handle underflow case
|
||||
else if counter_underflow {
|
||||
// When underflow occurs, the counter wraps from 0 to MAX
|
||||
// Calculate how much it moved below 0
|
||||
let delta = (u32::MAX - counter_value) as i64;
|
||||
// Subtract the distance from last position to 0, plus the underflow amount
|
||||
self.position -= self.last_encoder_position as i64 + 1 + delta;
|
||||
}
|
||||
// Normal case - no overflow/underflow
|
||||
else {
|
||||
// Calculate the signed difference between current and last position
|
||||
let delta = counter_value as i64 - self.last_encoder_position as i64;
|
||||
// Update the position
|
||||
self.position += delta;
|
||||
}
|
||||
|
||||
// Update the last encoder position for next time
|
||||
self.last_encoder_position = counter_value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Tests that the position tracker correctly handles normal forward movement
|
||||
/// without any overflow conditions.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Initial state is zeroed
|
||||
/// 2. Moving forward updates both position and last_encoder_position
|
||||
/// 3. Consecutive forward movements accumulate correctly
|
||||
#[test]
|
||||
fn test_normal_forward_movement() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Initial state
|
||||
assert_eq!(tracker.position, 0);
|
||||
assert_eq!(tracker.last_encoder_position, 0);
|
||||
|
||||
// Move forward by 100
|
||||
tracker.update(100, false, false);
|
||||
assert_eq!(tracker.position, 100);
|
||||
assert_eq!(tracker.last_encoder_position, 100);
|
||||
|
||||
// Move forward by another 50
|
||||
tracker.update(150, false, false);
|
||||
assert_eq!(tracker.position, 150);
|
||||
assert_eq!(tracker.last_encoder_position, 150);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker correctly handles normal backward movement
|
||||
/// without any underflow conditions.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Starting from a non-zero position
|
||||
/// 2. Moving backward updates both position and last_encoder_position
|
||||
/// 3. Consecutive backward movements accumulate correctly
|
||||
#[test]
|
||||
fn test_normal_backward_movement() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Start at position 1000
|
||||
tracker.position = 1000;
|
||||
tracker.last_encoder_position = 1000;
|
||||
|
||||
// Move backward by 100
|
||||
tracker.update(900, false, false);
|
||||
assert_eq!(tracker.position, 900);
|
||||
assert_eq!(tracker.last_encoder_position, 900);
|
||||
|
||||
// Move backward by another 50
|
||||
tracker.update(850, false, false);
|
||||
assert_eq!(tracker.position, 850);
|
||||
assert_eq!(tracker.last_encoder_position, 850);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker correctly handles overflow conditions
|
||||
/// where the encoder counter wraps from near its maximum value to a small value.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. When overflow flag is set, the tracker correctly calculates the true movement
|
||||
/// 2. Position is updated to account for the full distance traveled
|
||||
/// 3. last_encoder_position is updated to the new counter value
|
||||
#[test]
|
||||
fn test_overflow() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Set position near the maximum u32 value
|
||||
tracker.position = 100;
|
||||
tracker.last_encoder_position = u32::MAX - 10;
|
||||
|
||||
// Overflow: counter goes from (MAX-10) to 5
|
||||
// This means we moved forward by 10 steps to MAX, then 1 step to overflow,
|
||||
// then 5 more steps to reach the current position
|
||||
tracker.update(5, true, false);
|
||||
|
||||
// Expected: position += (10 + 1 + 5) = position += 16
|
||||
assert_eq!(tracker.position, 116);
|
||||
assert_eq!(tracker.last_encoder_position, 5);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker correctly handles underflow conditions
|
||||
/// where the encoder counter wraps from near zero to a value near its maximum.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. When underflow flag is set, the tracker correctly calculates the true movement
|
||||
/// 2. Position is updated to account for the full distance traveled (in negative direction)
|
||||
/// 3. last_encoder_position is updated to the new counter value
|
||||
#[test]
|
||||
fn test_underflow() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Set position near zero
|
||||
tracker.position = 100;
|
||||
tracker.last_encoder_position = 10;
|
||||
|
||||
// Underflow: counter goes from 10 to (MAX-5)
|
||||
// This means we moved backward by 10 steps to 0, then 1 step to underflow,
|
||||
// then 5 more steps to reach the current position
|
||||
tracker.update(u32::MAX - 5, false, true);
|
||||
|
||||
// Expected: position -= (10 + 1 + 5) = position -= 16
|
||||
assert_eq!(tracker.position, 84);
|
||||
assert_eq!(tracker.last_encoder_position, u32::MAX - 5);
|
||||
}
|
||||
|
||||
/// Tests that the position tracker maintains correct behavior across
|
||||
/// a sequence of different operations including normal movement,
|
||||
/// overflow, and underflow.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. The tracker maintains state correctly between operations
|
||||
/// 2. Different types of movements can be handled in sequence
|
||||
/// 3. Position calculations remain accurate throughout various scenarios
|
||||
#[test]
|
||||
fn test_multiple_operations() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Normal forward
|
||||
tracker.update(100, false, false);
|
||||
assert_eq!(tracker.position, 100);
|
||||
|
||||
// Normal backward
|
||||
tracker.update(50, false, false);
|
||||
assert_eq!(tracker.position, 50);
|
||||
|
||||
// Overflow
|
||||
tracker.last_encoder_position = u32::MAX - 5;
|
||||
tracker.position = 50; // Reset for clarity
|
||||
tracker.update(10, true, false);
|
||||
assert_eq!(tracker.position, 66); // 50 + (5 + 1 + 10)
|
||||
|
||||
// Underflow
|
||||
tracker.last_encoder_position = 5;
|
||||
tracker.position = 100; // Reset for clarity
|
||||
tracker.update(u32::MAX - 10, false, true);
|
||||
assert_eq!(tracker.position, 84); // 100 - (5 + 1 + 10)
|
||||
}
|
||||
|
||||
/// Tests that the position tracker can handle multiple overflow and underflow
|
||||
/// events, maintaining an accurate position count that exceeds the u32 range.
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Multiple overflow events accumulate correctly in the i64 position
|
||||
/// 2. Multiple underflow events correctly subtract from the position
|
||||
/// 3. The i64 position field properly extends the range beyond u32 limits
|
||||
#[test]
|
||||
fn test_large_movements() {
|
||||
let mut tracker = Encoder::new();
|
||||
|
||||
// Test tracking beyond u32 limits with multiple overflows
|
||||
for _ in 0..5 {
|
||||
// Simulate overflow: move from (MAX-100) to 100
|
||||
tracker.last_encoder_position = u32::MAX - 100;
|
||||
tracker.update(100, true, false);
|
||||
|
||||
// Each iteration adds (100 + 1 + 100) = 201 to position
|
||||
// We move 100 steps to MAX, 1 step to overflow, and 100 more steps to reach 100
|
||||
}
|
||||
|
||||
// After 5 iterations, position should be 5 * 201 = 1005
|
||||
assert_eq!(tracker.position, 1005);
|
||||
|
||||
// Test tracking with multiple underflows
|
||||
for _ in 0..3 {
|
||||
// Simulate underflow: move from 100 to (MAX-100)
|
||||
tracker.last_encoder_position = 100;
|
||||
tracker.update(u32::MAX - 100, false, true);
|
||||
|
||||
// Each iteration subtracts (100 + 1 + 100) = 201 from position
|
||||
// We move 100 steps to 0, 1 step to underflow, and 100 more steps to reach MAX-100
|
||||
}
|
||||
|
||||
// Final position: 1005 - (3 * 201) = 1005 - 603 = 402
|
||||
assert_eq!(tracker.position, 402);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use super::Actor;
|
||||
use encoder::Encoder;
|
||||
use ethercat_hal::io::pulse_train_output::PulseTrainOutput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
pub mod encoder;
|
||||
|
||||
/// Set a digital output high and low with a given interval
|
||||
#[derive(Debug)]
|
||||
pub struct StepperDriverPulseTrain {
|
||||
pulse: PulseTrainOutput,
|
||||
encoder: Encoder,
|
||||
frequency: i32,
|
||||
}
|
||||
|
||||
impl<'ptodevice> StepperDriverPulseTrain {
|
||||
pub fn new(output: PulseTrainOutput) -> Self {
|
||||
Self {
|
||||
pulse: output,
|
||||
encoder: Encoder::new(),
|
||||
frequency: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ptodevice> Actor for StepperDriverPulseTrain {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.pulse.state)().await;
|
||||
let mut output = state.output.clone();
|
||||
|
||||
// sync the counter to the encoder
|
||||
self.encoder.update(
|
||||
state.input.counter_value,
|
||||
state.input.counter_overflow,
|
||||
state.input.counter_underflow,
|
||||
);
|
||||
|
||||
// write frequency
|
||||
output.frequency_value = self.frequency;
|
||||
|
||||
(self.pulse.write)(output).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ptodevice> StepperDriverPulseTrain {
|
||||
pub fn set_frequency(&mut self, frequency: i32) {
|
||||
self.frequency = frequency;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use std::{pin::Pin, time::Instant};
|
||||
|
||||
use ethercat_hal::io::temperature_input::TemperatureInput;
|
||||
|
||||
use super::Actor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TemperatureInputGetter {
|
||||
input: TemperatureInput,
|
||||
temperature: f64,
|
||||
wiring_error: bool,
|
||||
}
|
||||
|
||||
impl TemperatureInputGetter {
|
||||
pub fn new(input: TemperatureInput) -> Self {
|
||||
Self {
|
||||
input,
|
||||
temperature: 0.0,
|
||||
wiring_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_temperature(&self) -> f64 {
|
||||
self.temperature
|
||||
}
|
||||
|
||||
pub fn get_wiring_error(&self) -> bool {
|
||||
self.wiring_error
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for TemperatureInputGetter {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let input = (self.input.state)().await.input;
|
||||
self.temperature = input.temperature as f64;
|
||||
self.wiring_error = input.overvoltage | input.undervoltage;
|
||||
if self.wiring_error {
|
||||
self.temperature = 0.0;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use super::Actor;
|
||||
use ethercat_hal::io::temperature_input::TemperatureInput;
|
||||
use std::{future::Future, pin::Pin, time::Instant};
|
||||
|
||||
/// Log the state of a temperature input
|
||||
pub struct TemperatureInputLogger {
|
||||
input: TemperatureInput,
|
||||
}
|
||||
|
||||
impl TemperatureInputLogger {
|
||||
pub fn new(input: TemperatureInput) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for TemperatureInputLogger {
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let state = (self.input.state)().await;
|
||||
println!("TemperatureInputLogger: {:?}C", state.input.temperature);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClampingTimeagnosticPidController {
|
||||
// Params
|
||||
/// Proportional gain
|
||||
kp: f64,
|
||||
/// Integral gain
|
||||
ki: f64,
|
||||
/// Derivative gain
|
||||
kd: f64,
|
||||
// State
|
||||
/// Proportional error
|
||||
ep: f64,
|
||||
/// Integral error
|
||||
ei: f64,
|
||||
/// Derivative error
|
||||
ed: f64,
|
||||
|
||||
min_ep: Option<f64>,
|
||||
max_ep: Option<f64>,
|
||||
|
||||
min_ei: Option<f64>,
|
||||
max_ei: Option<f64>,
|
||||
|
||||
min_ed: Option<f64>,
|
||||
max_ed: Option<f64>,
|
||||
|
||||
min_signal: Option<f64>,
|
||||
max_signal: Option<f64>,
|
||||
|
||||
last: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ClampingTimeagnosticPidController {
|
||||
pub fn new(
|
||||
kp: f64,
|
||||
ki: f64,
|
||||
kd: f64,
|
||||
min_ep: Option<f64>,
|
||||
max_ep: Option<f64>,
|
||||
min_ei: Option<f64>,
|
||||
max_ei: Option<f64>,
|
||||
min_ed: Option<f64>,
|
||||
max_ed: Option<f64>,
|
||||
min_signal: Option<f64>,
|
||||
max_signal: Option<f64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
kd,
|
||||
ep: 0.0,
|
||||
ei: 0.0,
|
||||
ed: 0.0,
|
||||
last: None,
|
||||
min_ep,
|
||||
max_ep,
|
||||
min_ei,
|
||||
max_ei,
|
||||
min_ed,
|
||||
max_ed,
|
||||
min_signal,
|
||||
max_signal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_new(kp: f64, ki: f64, kd: f64) -> Self {
|
||||
Self {
|
||||
kp,
|
||||
ki,
|
||||
kd,
|
||||
ep: 0.0,
|
||||
ei: 0.0,
|
||||
ed: 0.0,
|
||||
min_ep: None,
|
||||
max_ep: None,
|
||||
min_ei: None,
|
||||
max_ei: None,
|
||||
min_ed: None,
|
||||
max_ed: None,
|
||||
last: None,
|
||||
min_signal: None,
|
||||
max_signal: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kp(&self) -> f64 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub fn get_kd(&self) -> f64 {
|
||||
self.kd
|
||||
}
|
||||
|
||||
pub 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 {
|
||||
match (min, max) {
|
||||
(Some(min), Some(max)) => value.clamp(min, max),
|
||||
(Some(min), None) => value.max(min),
|
||||
(None, Some(max)) => value.min(max),
|
||||
(None, None) => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
|
||||
let signal = match self.last {
|
||||
// First update
|
||||
None => {
|
||||
// Calculate error
|
||||
let ep = error;
|
||||
|
||||
// Calculate signal
|
||||
let signal = self.kp * ep;
|
||||
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
|
||||
signal,
|
||||
self.min_signal,
|
||||
self.max_signal,
|
||||
);
|
||||
|
||||
// Set values
|
||||
self.ep =
|
||||
ClampingTimeagnosticPidController::optional_clamp(ep, self.min_ep, self.max_ep);
|
||||
self.ei = 0.0;
|
||||
self.ed = 0.0;
|
||||
self.last = Some(t);
|
||||
|
||||
clamped_signal
|
||||
}
|
||||
// Subsequent updates
|
||||
Some(last) => {
|
||||
// Calculate the time delta in seconds
|
||||
let dt = t.duration_since(last).as_secs_f64();
|
||||
|
||||
// Calculate errors
|
||||
let ep = ClampingTimeagnosticPidController::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 ed = ClampingTimeagnosticPidController::optional_clamp(
|
||||
(ep - self.ep) / dt,
|
||||
self.min_ed,
|
||||
self.max_ed,
|
||||
);
|
||||
|
||||
// Make factors timeagnostic
|
||||
let kp = self.kp * dt;
|
||||
let ki = self.ki * dt;
|
||||
let kd = self.kd * dt;
|
||||
|
||||
// Calculate signal
|
||||
let signal = kp * ep + ki * ei + kd * ed;
|
||||
let clamped_signal = ClampingTimeagnosticPidController::optional_clamp(
|
||||
signal,
|
||||
self.min_signal,
|
||||
self.max_signal,
|
||||
);
|
||||
|
||||
// Set values
|
||||
self.ep = ep;
|
||||
self.ei = ei;
|
||||
self.ed = ed;
|
||||
self.last = Some(t);
|
||||
|
||||
clamped_signal
|
||||
}
|
||||
};
|
||||
|
||||
signal
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.ep = 0.0;
|
||||
self.ei = 0.0;
|
||||
self.ed = 0.0;
|
||||
self.last = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::controllers::clamping_timeagnostic_pid::ClampingTimeagnosticPidController;
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_both_bounds() {
|
||||
let val = 5.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(1.0), Some(4.0));
|
||||
assert_eq!(clamped, 4.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(6.0), Some(10.0));
|
||||
assert_eq!(clamped, 6.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), Some(6.0));
|
||||
assert_eq!(clamped, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_only_min() {
|
||||
let val = 3.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(5.0), None);
|
||||
assert_eq!(clamped, 5.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, Some(2.0), None);
|
||||
assert_eq!(clamped, 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_only_max() {
|
||||
let val = 7.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(6.0));
|
||||
assert_eq!(clamped, 6.0);
|
||||
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, Some(8.0));
|
||||
assert_eq!(clamped, 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_clamp_with_no_bounds() {
|
||||
let val = 42.0;
|
||||
let clamped = ClampingTimeagnosticPidController::optional_clamp(val, None, None);
|
||||
assert_eq!(clamped, 42.0);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@ use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AccelerationSpeedController {
|
||||
/// Maximum acceleration in units per second (positive value)
|
||||
acceleration: f64,
|
||||
/// Maximum deceleration in units per second (positive value)
|
||||
max_acceleration: f64,
|
||||
|
||||
/// Maximum deceleration in units per second (negative value)
|
||||
deceleration: f64,
|
||||
/// Maximum acceleratoin in units per second (negative value)
|
||||
min_acceleration: f64,
|
||||
|
||||
/// Minimum speed limit (None for no limit)
|
||||
min_speed: Option<f64>,
|
||||
|
||||
/// Maximum speed limit (None for no limit)
|
||||
max_speed: Option<f64>,
|
||||
|
||||
/// Calculated speed at the last update
|
||||
last_speed: f64,
|
||||
@@ -16,15 +22,36 @@ pub struct AccelerationSpeedController {
|
||||
}
|
||||
|
||||
impl AccelerationSpeedController {
|
||||
pub fn new(acceleration: f64, deceleration: f64, initial_speed: f64) -> Self {
|
||||
pub fn new(
|
||||
min_speed: Option<f64>,
|
||||
max_speed: Option<f64>,
|
||||
min_acceleration: f64,
|
||||
max_acceleration: f64,
|
||||
initial_speed: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
acceleration,
|
||||
deceleration,
|
||||
min_acceleration,
|
||||
max_acceleration,
|
||||
min_speed,
|
||||
max_speed,
|
||||
last_speed: initial_speed,
|
||||
last_t: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new acceleration speed controller with simplified parameters.
|
||||
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(max_acceleration: f64, initial_speed: f64) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, target_speed: f64, t: Instant) -> f64 {
|
||||
// Calculate time delta
|
||||
let dt = match self.last_t {
|
||||
@@ -41,10 +68,10 @@ impl AccelerationSpeedController {
|
||||
// Get acceleration
|
||||
let acceleration = if target_speed > self.last_speed {
|
||||
// We are accelerating
|
||||
self.acceleration
|
||||
self.max_acceleration
|
||||
} else if target_speed < self.last_speed {
|
||||
// We are decelerating
|
||||
self.deceleration
|
||||
self.min_acceleration
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -63,6 +90,9 @@ impl AccelerationSpeedController {
|
||||
new_speed
|
||||
};
|
||||
|
||||
// Apply speed limits
|
||||
let new_speed = self.apply_speed_limits(new_speed);
|
||||
|
||||
self.last_speed = new_speed;
|
||||
|
||||
new_speed
|
||||
@@ -73,12 +103,42 @@ impl AccelerationSpeedController {
|
||||
self.last_t = None; // Reset the last update time
|
||||
}
|
||||
|
||||
pub fn set_acceleration(&mut self, acceleration: f64) {
|
||||
self.acceleration = acceleration;
|
||||
pub fn set_max_acceleration(&mut self, acceleration: f64) {
|
||||
self.max_acceleration = acceleration;
|
||||
}
|
||||
|
||||
pub fn set_deceleration(&mut self, deceleration: f64) {
|
||||
self.deceleration = deceleration;
|
||||
fn apply_speed_limits(&self, speed: f64) -> f64 {
|
||||
let mut limited_speed = speed;
|
||||
|
||||
if let Some(min) = self.min_speed {
|
||||
limited_speed = limited_speed.max(min);
|
||||
}
|
||||
|
||||
if let Some(max) = self.max_speed {
|
||||
limited_speed = limited_speed.min(max);
|
||||
}
|
||||
|
||||
limited_speed
|
||||
}
|
||||
|
||||
pub fn get_min_speed(&self) -> Option<f64> {
|
||||
self.min_speed
|
||||
}
|
||||
|
||||
pub fn get_max_speed(&self) -> Option<f64> {
|
||||
self.max_speed
|
||||
}
|
||||
|
||||
pub 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>) {
|
||||
self.max_speed = max_speed;
|
||||
}
|
||||
|
||||
pub fn set_min_acceleration(&mut self, deceleration: f64) {
|
||||
self.min_acceleration = deceleration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,16 +157,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_initialization() {
|
||||
let controller = AccelerationSpeedController::new(10.0, -15.0, 5.0);
|
||||
assert_relative_eq!(controller.acceleration, 10.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(controller.deceleration, -15.0, epsilon = EPSILON);
|
||||
let controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
assert_relative_eq!(controller.max_acceleration, 10.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(controller.min_acceleration, -15.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(controller.last_speed, 5.0, epsilon = EPSILON);
|
||||
assert!(controller.last_t.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_update() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 5.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
let now = Instant::now();
|
||||
|
||||
let speed = controller.update(5.0, now);
|
||||
@@ -118,7 +178,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_acceleration() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 0.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
@@ -137,7 +197,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_deceleration() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 10.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 10.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(10.0, t1); // Initialize last_t
|
||||
|
||||
@@ -156,7 +216,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_constant_speed() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 7.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 7.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(7.0, t1); // Initialize last_t
|
||||
|
||||
@@ -169,7 +229,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_acceleration_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 0.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
@@ -186,7 +246,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_deceleration_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 20.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 20.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(20.0, t1); // Initialize last_t
|
||||
|
||||
@@ -203,7 +263,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_zero_time_delta() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -15.0, 5.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
let now = Instant::now();
|
||||
|
||||
controller.update(5.0, now); // Initialize last_t
|
||||
@@ -216,7 +276,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_acceleration_capped_at_target() {
|
||||
let mut controller = AccelerationSpeedController::new(100.0, -15.0, 3.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 100.0, 3.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(3.0, t1); // Initialize last_t
|
||||
|
||||
@@ -234,7 +294,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_deceleration_capped_at_target() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -100.0, 8.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -100.0, 10.0, 8.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(8.0, t1); // Initialize last_t
|
||||
|
||||
@@ -252,7 +312,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_exact_acceleration_to_target() {
|
||||
let mut controller = AccelerationSpeedController::new(20.0, -15.0, 3.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 20.0, 3.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(3.0, t1); // Initialize last_t
|
||||
|
||||
@@ -269,7 +329,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_exact_deceleration_to_target() {
|
||||
let mut controller = AccelerationSpeedController::new(10.0, -30.0, 8.0);
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -30.0, 10.0, 8.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(8.0, t1); // Initialize last_t
|
||||
|
||||
@@ -283,4 +343,92 @@ mod tests {
|
||||
|
||||
assert_relative_eq!(speed, 5.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_limits_initialization() {
|
||||
let controller =
|
||||
AccelerationSpeedController::new(Some(-50.0), Some(100.0), -10.0, 20.0, 5.0);
|
||||
assert_eq!(controller.get_min_speed(), Some(-50.0));
|
||||
assert_eq!(controller.get_max_speed(), Some(100.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_speed_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(None, Some(50.0), -10.0, 100.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 1.0; // 1 second
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
// Try to reach 80.0, but should be limited to 50.0
|
||||
let speed = controller.update(80.0, t2);
|
||||
assert_eq!(speed, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_speed_limit() {
|
||||
let mut controller = AccelerationSpeedController::new(Some(-30.0), None, -100.0, 10.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 1.0; // 1 second
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
// Try to reach -50.0, but should be limited to -30.0
|
||||
let speed = controller.update(-50.0, t2);
|
||||
assert_eq!(speed, -30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_within_limits() {
|
||||
let mut controller =
|
||||
AccelerationSpeedController::new(Some(-50.0), Some(50.0), -20.0, 20.0, 0.0);
|
||||
let t1 = Instant::now();
|
||||
controller.update(0.0, t1); // Initialize last_t
|
||||
|
||||
let dt = 0.5; // 0.5 seconds
|
||||
let t2 = future_instant(t1, dt);
|
||||
|
||||
// Target 10.0, with acceleration 20.0, should reach 10.0 in 0.5s
|
||||
let speed = controller.update(10.0, t2);
|
||||
assert_relative_eq!(speed, 10.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_limit_setters() {
|
||||
let mut controller = AccelerationSpeedController::new(None, None, -15.0, 10.0, 5.0);
|
||||
|
||||
// Initially no limits
|
||||
assert_eq!(controller.get_min_speed(), None);
|
||||
assert_eq!(controller.get_max_speed(), None);
|
||||
|
||||
// Set limits
|
||||
controller.set_min_speed(Some(-100.0));
|
||||
controller.set_max_speed(Some(100.0));
|
||||
|
||||
assert_eq!(controller.get_min_speed(), Some(-100.0));
|
||||
assert_eq!(controller.get_max_speed(), Some(100.0));
|
||||
|
||||
// Remove limits
|
||||
controller.set_min_speed(None);
|
||||
controller.set_max_speed(None);
|
||||
|
||||
assert_eq!(controller.get_min_speed(), None);
|
||||
assert_eq!(controller.get_max_speed(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_simple_constructor() {
|
||||
let controller = AccelerationSpeedController::new_simple(20.0, 5.0);
|
||||
|
||||
// Check that acceleration values are symmetric
|
||||
assert_eq!(controller.min_acceleration, -20.0);
|
||||
assert_eq!(controller.max_acceleration, 20.0);
|
||||
assert_eq!(controller.last_speed, 5.0);
|
||||
|
||||
// Check that speed limits are None
|
||||
assert_eq!(controller.get_min_speed(), None);
|
||||
assert_eq!(controller.get_max_speed(), None);
|
||||
}
|
||||
}
|
||||
@@ -17,19 +17,36 @@ pub struct AngularAccelerationSpeedController {
|
||||
|
||||
impl AngularAccelerationSpeedController {
|
||||
pub fn new(
|
||||
acceleration: AngularAcceleration,
|
||||
deceleration: AngularAcceleration,
|
||||
min_speed: Option<AngularVelocity>,
|
||||
max_speed: Option<AngularVelocity>,
|
||||
min_acceleration: AngularAcceleration,
|
||||
max_acceleration: AngularAcceleration,
|
||||
initial_speed: AngularVelocity,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationSpeedController::new(
|
||||
acceleration.get::<radian_per_second_squared>(),
|
||||
deceleration.get::<radian_per_second_squared>(),
|
||||
min_speed.map(|s| s.get::<radian_per_second>()),
|
||||
max_speed.map(|s| s.get::<radian_per_second>()),
|
||||
min_acceleration.get::<radian_per_second_squared>(),
|
||||
max_acceleration.get::<radian_per_second_squared>(),
|
||||
initial_speed.get::<radian_per_second>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new angular acceleration speed controller with simplified parameters.
|
||||
/// Sets min_acceleration to -max_acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(max_acceleration: AngularAcceleration, initial_speed: AngularVelocity) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
-max_acceleration, // min_acceleration (deceleration)
|
||||
max_acceleration, // max_acceleration
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, target_speed: AngularVelocity, t: Instant) -> AngularVelocity {
|
||||
let target_speed = target_speed.get::<radian_per_second>();
|
||||
let new_speed = self.controller.update(target_speed, t);
|
||||
@@ -41,12 +58,34 @@ impl AngularAccelerationSpeedController {
|
||||
self.controller.reset(initial_speed);
|
||||
}
|
||||
|
||||
pub fn set_acceleration(&mut self, acceleration: AngularAcceleration) {
|
||||
pub fn set_max_acceleration(&mut self, acceleration: AngularAcceleration) {
|
||||
self.controller
|
||||
.set_acceleration(acceleration.get::<radian_per_second_squared>());
|
||||
.set_max_acceleration(acceleration.get::<radian_per_second_squared>());
|
||||
}
|
||||
pub fn set_deceleration(&mut self, deceleration: AngularAcceleration) {
|
||||
pub fn set_min_acceleration(&mut self, deceleration: AngularAcceleration) {
|
||||
self.controller
|
||||
.set_deceleration(deceleration.get::<radian_per_second_squared>());
|
||||
.set_min_acceleration(deceleration.get::<radian_per_second_squared>());
|
||||
}
|
||||
|
||||
pub fn get_min_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(|s| AngularVelocity::new::<radian_per_second>(s))
|
||||
}
|
||||
|
||||
pub fn get_max_speed(&self) -> Option<AngularVelocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(|s| AngularVelocity::new::<radian_per_second>(s))
|
||||
}
|
||||
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<AngularVelocity>) {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.map(|s| s.get::<radian_per_second>()));
|
||||
}
|
||||
|
||||
pub fn set_max_speed(&mut self, max_speed: Option<AngularVelocity>) {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.map(|s| s.get::<radian_per_second>()));
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,34 @@ pub struct LinearAccelerationLimitingController {
|
||||
|
||||
impl LinearAccelerationLimitingController {
|
||||
pub fn new(
|
||||
min_speed: Option<Velocity>,
|
||||
max_speed: Option<Velocity>,
|
||||
acceleration: Acceleration,
|
||||
deceleration: Acceleration,
|
||||
initial_speed: Velocity,
|
||||
) -> Self {
|
||||
Self {
|
||||
controller: AccelerationSpeedController::new(
|
||||
min_speed.map(|s| s.get::<meter_per_second>()),
|
||||
max_speed.map(|s| s.get::<meter_per_second>()),
|
||||
acceleration.get::<meter_per_second_squared>(),
|
||||
deceleration.get::<meter_per_second_squared>(),
|
||||
initial_speed.get::<meter_per_second>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
/// Creates a new linear acceleration speed controller with simplified parameters.
|
||||
/// Sets deceleration to -acceleration for symmetric behavior.
|
||||
/// No speed limits are applied.
|
||||
pub fn new_simple(acceleration: Acceleration, initial_speed: Velocity) -> Self {
|
||||
Self::new(
|
||||
None, // min_speed
|
||||
None, // max_speed
|
||||
acceleration, // acceleration
|
||||
-acceleration, // deceleration (negative for deceleration)
|
||||
initial_speed,
|
||||
)
|
||||
}
|
||||
pub fn update(&mut self, target_speed: Velocity, t: Instant) -> Velocity {
|
||||
let target_speed = target_speed.get::<meter_per_second>();
|
||||
let new_speed = self.controller.update(target_speed, t);
|
||||
@@ -40,10 +56,28 @@ impl LinearAccelerationLimitingController {
|
||||
}
|
||||
pub fn set_acceleration(&mut self, acceleration: Acceleration) {
|
||||
self.controller
|
||||
.set_acceleration(acceleration.get::<meter_per_second_squared>());
|
||||
.set_max_acceleration(acceleration.get::<meter_per_second_squared>());
|
||||
}
|
||||
pub fn set_deceleration(&mut self, deceleration: Acceleration) {
|
||||
self.controller
|
||||
.set_deceleration(deceleration.get::<meter_per_second_squared>());
|
||||
.set_min_acceleration(deceleration.get::<meter_per_second_squared>());
|
||||
}
|
||||
pub fn get_min_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_min_speed()
|
||||
.map(|s| Velocity::new::<meter_per_second>(s))
|
||||
}
|
||||
pub fn get_max_speed(&self) -> Option<Velocity> {
|
||||
self.controller
|
||||
.get_max_speed()
|
||||
.map(|s| Velocity::new::<meter_per_second>(s))
|
||||
}
|
||||
pub fn set_min_speed(&mut self, min_speed: Option<Velocity>) {
|
||||
self.controller
|
||||
.set_min_speed(min_speed.map(|s| s.get::<meter_per_second>()));
|
||||
}
|
||||
pub fn set_max_speed(&mut self, max_speed: Option<Velocity>) {
|
||||
self.controller
|
||||
.set_max_speed(max_speed.map(|s| s.get::<meter_per_second>()));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod clamping_timeagnostic_pid;
|
||||
pub mod first_degree_motion;
|
||||
pub mod pid;
|
||||
pub mod second_degree_motion;
|
||||
@@ -9,7 +9,6 @@ pub struct PidController {
|
||||
ki: f64,
|
||||
/// Derivative gain
|
||||
kd: f64,
|
||||
|
||||
// State
|
||||
/// Proportional error
|
||||
ep: f64,
|
||||
@@ -34,6 +33,25 @@ impl PidController {
|
||||
}
|
||||
}
|
||||
|
||||
pub 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 {
|
||||
self.kp
|
||||
}
|
||||
|
||||
pub fn get_ki(&self) -> f64 {
|
||||
self.ki
|
||||
}
|
||||
|
||||
pub fn get_kd(&self) -> f64 {
|
||||
self.kd
|
||||
}
|
||||
|
||||
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
|
||||
let signal = match self.last {
|
||||
// First update
|
||||
@@ -47,6 +65,7 @@ impl PidController {
|
||||
// Set values
|
||||
self.ep = ep;
|
||||
self.ei = 0.0;
|
||||
|
||||
self.ed = 0.0;
|
||||
self.last = Some(t);
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use uom::si::angle::radian;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct AngleConverter {
|
||||
pub flip_x: bool, // true to flip X-axis (mirror horizontally)
|
||||
pub flip_y: bool, // true to flip Y-axis (mirror vertically)
|
||||
pub is_cw: bool, // true for clockwise, false for counter-clockwise
|
||||
}
|
||||
|
||||
impl AngleConverter {
|
||||
/// Create a custom coordinate system
|
||||
pub fn new(flip_x: bool, flip_y: bool, is_cw: bool) -> Self {
|
||||
Self {
|
||||
flip_x,
|
||||
flip_y,
|
||||
is_cw,
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard mathematical coordinate system (CCW positive, 0° at positive X)
|
||||
pub fn mathematical() -> Self {
|
||||
Self::new(false, false, false)
|
||||
}
|
||||
|
||||
/// Screen/graphics coordinate system (CW positive, 0° at positive X, Y-flipped)
|
||||
pub fn screen() -> Self {
|
||||
Self::new(false, true, false)
|
||||
}
|
||||
|
||||
/// System where 0° points up, clockwise positive
|
||||
pub fn y_up_cw() -> Self {
|
||||
Self::new(false, false, true)
|
||||
}
|
||||
|
||||
/// System where 0° points down, counter-clockwise positive
|
||||
pub fn y_down_ccw() -> Self {
|
||||
Self::new(false, true, true)
|
||||
}
|
||||
|
||||
/// System where 0° points left, clockwise positive
|
||||
pub 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 {
|
||||
Self::mathematical()
|
||||
}
|
||||
|
||||
/// System where 0° points up, counter-clockwise positive
|
||||
pub fn y_up_ccw() -> Self {
|
||||
Self::new(false, false, false)
|
||||
}
|
||||
|
||||
/// System where 0° points down, clockwise positive
|
||||
pub fn y_down_cw() -> Self {
|
||||
Self::new(false, true, false)
|
||||
}
|
||||
|
||||
/// System where 0° points left, counter-clockwise positive
|
||||
pub 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 {
|
||||
let normalized_input = self.normalize_angle(math_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Apply axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result = 180.0 + result, // Both flips: 180° rotation
|
||||
}
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
self.normalize_angle(result)
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f32)
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f32) -> f32 {
|
||||
let normalized_input = self.normalize_angle(system_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
// Apply inverse axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result = 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;
|
||||
let converted_degrees = self.degrees_encode(degrees);
|
||||
converted_degrees * PI / 180.0
|
||||
}
|
||||
|
||||
/// 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;
|
||||
let converted_degrees = self.degrees_decode(degrees);
|
||||
converted_degrees * PI / 180.0
|
||||
}
|
||||
|
||||
/// Convert angle from mathematical system to this system (f64)
|
||||
pub fn degrees_encode_f64(&self, math_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle_f64(math_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Apply axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result = 180.0 + result, // Both flips: 180° rotation
|
||||
}
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
self.normalize_angle_f64(result)
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f64)
|
||||
pub fn degrees_decode_f64(&self, system_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle_f64(system_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
|
||||
// Handle rotation direction
|
||||
if self.is_cw {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
// Apply inverse axis flips
|
||||
match (self.flip_x, self.flip_y) {
|
||||
(false, false) => {} // No change
|
||||
(true, false) => result = 180.0 - result, // X-flip: mirror across Y-axis
|
||||
(false, true) => result = -result, // Y-flip: mirror across X-axis
|
||||
(true, true) => result = result - 180.0, // Both flips: inverse 180° rotation
|
||||
}
|
||||
|
||||
self.normalize_angle_f64(result)
|
||||
}
|
||||
|
||||
/// Convert angle in radians from mathematical system to this system (f64)
|
||||
pub fn radians_encode_f64(&self, math_angle_radians: f64) -> f64 {
|
||||
let degrees = math_angle_radians * 180.0 / std::f64::consts::PI;
|
||||
let converted_degrees = self.degrees_encode_f64(degrees);
|
||||
converted_degrees * std::f64::consts::PI / 180.0
|
||||
}
|
||||
|
||||
/// 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 converted_degrees = self.degrees_decode_f64(degrees);
|
||||
converted_degrees * std::f64::consts::PI / 180.0
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f32)
|
||||
fn normalize_angle(&self, angle: f32) -> f32 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f64)
|
||||
fn normalize_angle_f64(&self, angle: f64) -> f64 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
}
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AngleConverterUom {
|
||||
angle_converter: AngleConverter,
|
||||
}
|
||||
|
||||
impl AngleConverterUom {
|
||||
pub 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 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>()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_constructors() {
|
||||
let math = AngleConverter::mathematical();
|
||||
assert_eq!(math.flip_x, false);
|
||||
assert_eq!(math.flip_y, false);
|
||||
assert_eq!(math.is_cw, false);
|
||||
|
||||
let screen = AngleConverter::screen();
|
||||
assert_eq!(screen.flip_x, false);
|
||||
assert_eq!(screen.flip_y, true);
|
||||
assert_eq!(screen.is_cw, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_screen_converter_bidirectional() {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Math to screen (Y-flip only)
|
||||
assert_eq!(converter.degrees_encode(0.0), 0.0); // Right stays right
|
||||
assert_eq!(converter.degrees_encode(90.0), 270.0); // Up becomes down
|
||||
assert_eq!(converter.degrees_encode(270.0), 90.0); // Down becomes up
|
||||
assert_eq!(converter.degrees_encode(180.0), 180.0); // Left stays left
|
||||
|
||||
// Screen to math (should be inverse)
|
||||
assert_eq!(converter.degrees_decode(0.0), 0.0); // Right stays right
|
||||
assert_eq!(converter.degrees_decode(270.0), 90.0); // Down becomes up
|
||||
assert_eq!(converter.degrees_decode(90.0), 270.0); // Up becomes down
|
||||
assert_eq!(converter.degrees_decode(180.0), 180.0); // Left stays left
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_x_flip_converter() {
|
||||
let converter = AngleConverter::x_left_cw();
|
||||
|
||||
// Math to x-flipped system
|
||||
assert_eq!(converter.degrees_encode(0.0), 180.0); // Right becomes left
|
||||
assert_eq!(converter.degrees_encode(90.0), 90.0); // Up stays up
|
||||
assert_eq!(converter.degrees_encode(180.0), 0.0); // Left becomes right
|
||||
assert_eq!(converter.degrees_encode(270.0), 270.0); // Down stays down
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_both_flips_converter() {
|
||||
let converter = AngleConverter::new(true, true, false);
|
||||
|
||||
// Math to both-flipped system (180° rotation)
|
||||
assert_eq!(converter.degrees_encode(0.0), 180.0); // Right becomes left
|
||||
assert_eq!(converter.degrees_encode(90.0), 270.0); // Up becomes down
|
||||
assert_eq!(converter.degrees_encode(180.0), 0.0); // Left becomes right
|
||||
assert_eq!(converter.degrees_encode(270.0), 90.0); // Down becomes up
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radians_conversion() {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Test radians_encode
|
||||
let result = converter.radians_encode(PI / 2.0); // 90° in radians
|
||||
let expected = 270.0 * PI / 180.0; // 270° in radians
|
||||
assert!((result - expected).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_conversion() {
|
||||
let converter = AngleConverter::new(true, false, true); // X-flip + CW
|
||||
|
||||
let original = 75.0_f32;
|
||||
let converted = converter.degrees_encode(original);
|
||||
let roundtrip = converter.degrees_decode(converted);
|
||||
|
||||
assert!((original - roundtrip).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod angle_converter;
|
||||
pub mod angular_step_converter;
|
||||
pub mod linear_step_converter;
|
||||
pub mod motor_converter;
|
||||
|
||||
@@ -14,19 +14,16 @@ use ethercat_hal::devices::el3001::EL3001_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3021::EL3021_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3024::EL3024_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3204::EL3204_IDENTITY_A;
|
||||
use ethercat_hal::devices::el6021::EL6021_IDENTITY_A;
|
||||
use ethercat_hal::devices::el6021::EL6021_IDENTITY_B;
|
||||
|
||||
use ethercat_hal::devices::el3204::EL3204_IDENTITY_B;
|
||||
|
||||
use ethercat_hal::devices::el6021::EL6021_IDENTITY_C;
|
||||
use ethercat_hal::devices::el6021::{
|
||||
EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D,
|
||||
};
|
||||
use ethercat_hal::devices::el7031::{EL7031_IDENTITY_A, EL7031_IDENTITY_B};
|
||||
use ethercat_hal::devices::el7031_0030::EL7031_0030_IDENTITY_A;
|
||||
use ethercat_hal::devices::el7041_0052::EL7041_0052_IDENTITY_A;
|
||||
use ethercat_hal::devices::subdevice_identity_to_tuple;
|
||||
use ethercat_hal::helpers::ethercrab_types::{
|
||||
EthercrabSubDeviceOperational,
|
||||
EthercrabSubDevicePreoperational,
|
||||
EthercrabSubDeviceOperational, EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::MainDevice;
|
||||
use ethercrab::SubDeviceIdentity;
|
||||
@@ -58,8 +55,8 @@ impl Default for MachineIdentificationAddresses {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vendor_word: 0x0028,
|
||||
serial_word: 0x0029,
|
||||
machine_word: 0x002a,
|
||||
machine_word: 0x0029,
|
||||
serial_word: 0x002a,
|
||||
role_word: 0x002b,
|
||||
}
|
||||
}
|
||||
@@ -68,9 +65,7 @@ impl Default for MachineIdentificationAddresses {
|
||||
/// Reads the EEPROM of all subdevices to get their machine device identifications
|
||||
///
|
||||
/// Returns a vector of MachineDeviceIdentification for all subdevices
|
||||
pub async fn read_device_identifications<
|
||||
'maindevice,
|
||||
>(
|
||||
pub async fn read_device_identifications<'maindevice>(
|
||||
subdevices: &Vec<EthercrabSubDevicePreoperational<'maindevice>>,
|
||||
maindevice: &MainDevice<'maindevice>,
|
||||
) -> Vec<Result<DeviceMachineIdentification, Error>> {
|
||||
@@ -123,7 +118,6 @@ pub async fn machine_device_identification<'maindevice>(
|
||||
module_path!(),
|
||||
subdevice.name()
|
||||
)))?,
|
||||
|
||||
},
|
||||
role: subdevice
|
||||
.eeprom_read::<u16>(maindevice, addresses.role_word)
|
||||
@@ -135,17 +129,20 @@ pub async fn machine_device_identification<'maindevice>(
|
||||
)))?,
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"[{}::machine_device_identification] Read MDI from EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
|
||||
module_path!(),
|
||||
tracing::debug!(
|
||||
"Read MDI from EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
|
||||
subdevice.name(),
|
||||
mdi.machine_identification_unique.machine_identification.vendor,
|
||||
mdi.machine_identification_unique
|
||||
.machine_identification
|
||||
.vendor,
|
||||
addresses.vendor_word,
|
||||
addresses.vendor_word + 1,
|
||||
mdi.machine_identification_unique.serial,
|
||||
addresses.serial_word,
|
||||
addresses.serial_word + 1,
|
||||
mdi.machine_identification_unique.machine_identification.machine,
|
||||
mdi.machine_identification_unique
|
||||
.machine_identification
|
||||
.machine,
|
||||
addresses.machine_word,
|
||||
addresses.machine_word + 1,
|
||||
mdi.role,
|
||||
@@ -163,17 +160,22 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
|
||||
device_identification: &DeviceMachineIdentification,
|
||||
) -> Result<(), Error> {
|
||||
let addresses = get_identification_addresses(&subdevice.identity(), subdevice.name())?;
|
||||
log::debug!(
|
||||
"[{}::write_machine_device_identification] Writing MDI to EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
|
||||
module_path!(),
|
||||
tracing::debug!(
|
||||
"Writing MDI to EEPROM for device {}\nVendor: 0x{:08x} at 0x{:04x}-0x{:04x}\nSerial: 0x{:08x} at 0x{:04x}-0x{:04x}\nMachine: 0x{:08x} at 0x{:04x}-0x{:04x}\nRole: 0x{:08x} at 0x{:04x}-0x{:04x}",
|
||||
subdevice.name(),
|
||||
device_identification.machine_identification_unique.machine_identification.vendor,
|
||||
device_identification
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
.vendor,
|
||||
addresses.vendor_word,
|
||||
addresses.vendor_word + 1,
|
||||
device_identification.machine_identification_unique.serial,
|
||||
addresses.serial_word,
|
||||
addresses.serial_word + 1,
|
||||
device_identification.machine_identification_unique.machine_identification.machine,
|
||||
device_identification
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
.machine,
|
||||
addresses.machine_word,
|
||||
addresses.machine_word + 1,
|
||||
device_identification.role,
|
||||
@@ -185,7 +187,10 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
|
||||
.eeprom_write_dangerously(
|
||||
maindevice,
|
||||
addresses.vendor_word,
|
||||
device_identification.machine_identification_unique.machine_identification.vendor,
|
||||
device_identification
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
.vendor,
|
||||
)
|
||||
.await?;
|
||||
subdevice
|
||||
@@ -199,7 +204,10 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
|
||||
.eeprom_write_dangerously(
|
||||
maindevice,
|
||||
addresses.machine_word,
|
||||
device_identification.machine_identification_unique.machine_identification.machine,
|
||||
device_identification
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
.machine,
|
||||
)
|
||||
.await?;
|
||||
subdevice
|
||||
@@ -234,7 +242,9 @@ pub fn get_identification_addresses<'maindevice>(
|
||||
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL7041_0052_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C => MachineIdentificationAddresses::default(),
|
||||
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C | EL6021_IDENTITY_D => {
|
||||
MachineIdentificationAddresses::default()
|
||||
}
|
||||
_ => {
|
||||
// block_on(u16dump(&subdevice, maindevice, 0x00, 0xff))?;
|
||||
Err(anyhow!(
|
||||
|
||||
@@ -21,21 +21,21 @@ const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
log::info!("Discovering EtherCAT interface...");
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Optional: log the panic in a controlled way
|
||||
if let Some(location) = panic_info.location() {
|
||||
log::debug!(
|
||||
tracing::debug!(
|
||||
"Suppressed panic in interface test: {} (at {}:{})",
|
||||
panic_info,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
log::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
// Don't call the default hook, which would print the backtrace
|
||||
}));
|
||||
@@ -49,6 +49,10 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& !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<_>>();
|
||||
|
||||
@@ -60,34 +64,40 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
.iter()
|
||||
.map(|iface| {
|
||||
let name = iface.name.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::panic::catch_unwind(|| {
|
||||
smol::block_on(async {
|
||||
match test_interface(&name) {
|
||||
Ok(_) => {
|
||||
log::debug!("Found working EtherCAT interface: {}", name);
|
||||
Some(name.clone())
|
||||
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
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Interface {} failed: {}", name, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.expect("Failed to spawn thread")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Return first successful interface
|
||||
let result =
|
||||
tasks
|
||||
.into_iter()
|
||||
.find_map(|task| match task.join().expect("Should join thread") {
|
||||
Some(name) => Some(name),
|
||||
None => None,
|
||||
});
|
||||
// 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();
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
@@ -99,7 +109,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
log::debug!("Testing interface: {}", interface);
|
||||
tracing::trace!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
@@ -133,7 +143,12 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
},
|
||||
MainDeviceConfig::default(),
|
||||
MainDeviceConfig {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::None,
|
||||
},
|
||||
));
|
||||
|
||||
let result = maindevice
|
||||
|
||||
@@ -1,40 +1,108 @@
|
||||
pub fn xor_u128_to_u64(value: u128) -> u64 {
|
||||
(value as u64) ^ ((value >> 64) as u64)
|
||||
/// Folds bytes using XOR to produce a u128 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u128.
|
||||
/// If there are fewer than 16 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u128(bytes: &[u8]) -> u128 {
|
||||
let mut result = 0u128;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 16) * 8;
|
||||
result ^= (byte as u128) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn xor_u128_to_u32(value: u128) -> u32 {
|
||||
(value as u32) ^ ((value >> 32) as u32) ^ ((value >> 64) as u32) ^ ((value >> 96) as u32)
|
||||
/// Folds bytes using XOR to produce a u64 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u64.
|
||||
/// If there are fewer than 8 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u64(bytes: &[u8]) -> u64 {
|
||||
let mut result = 0u64;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 8) * 8;
|
||||
result ^= (byte as u64) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn xor_u128_to_u16(value: u128) -> u16 {
|
||||
value
|
||||
.to_le_bytes()
|
||||
.iter()
|
||||
.fold(0, |buf, &byte| buf ^ byte as u16)
|
||||
/// Folds bytes using XOR to produce a u32 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u32.
|
||||
/// If there are fewer than 4 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u32(bytes: &[u8]) -> u32 {
|
||||
let mut result = 0u32;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 4) * 8;
|
||||
result ^= (byte as u32) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn xor_u128_to_u8(value: u128) -> u8 {
|
||||
value.to_le_bytes().iter().fold(0, |buf, &byte| buf ^ byte)
|
||||
/// Folds bytes using XOR to produce a u16 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them together in chunks to produce a u16.
|
||||
/// If there are fewer than 2 bytes, the remaining bytes are padded with zeros.
|
||||
pub fn byte_folding_u16(bytes: &[u8]) -> u16 {
|
||||
let mut result = 0u16;
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let shift = (i % 2) * 8;
|
||||
result ^= (byte as u16) << shift;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// hash algorithm djb2
|
||||
pub fn hashing(input_to_hash: &str) -> u128 {
|
||||
let mut hash: u128 = 5381;
|
||||
let chars = input_to_hash.chars();
|
||||
/// Folds bytes using XOR to produce a u8 value.
|
||||
///
|
||||
/// Takes a slice of bytes and XORs them all together to produce a u8.
|
||||
pub fn byte_folding_u8(bytes: &[u8]) -> u8 {
|
||||
bytes.iter().fold(0u8, |acc, &byte| acc ^ byte)
|
||||
}
|
||||
|
||||
for ch in chars {
|
||||
hash = ((hash << 5) + hash) + (ch as u128);
|
||||
/// Computes a djb2 hash of the input bytes.
|
||||
///
|
||||
/// The djb2 algorithm is a simple non-cryptographic hash function created by
|
||||
/// Daniel J. Bernstein. It uses the formula: `hash = hash * 33 + byte` for each byte.
|
||||
///
|
||||
/// This implementation uses the traditional 32-bit hash value and uses wrapping
|
||||
/// arithmetic to handle potential overflows gracefully.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `bytes` - A slice of bytes to hash
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A 32-bit hash value as a `u32`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use control_core::helpers::hashing::hash_djb2;
|
||||
///
|
||||
/// let hash = hash_djb2(b"hello world");
|
||||
/// assert_ne!(hash, 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Algorithm Details
|
||||
///
|
||||
/// - Initial hash value: 5381 (djb2 magic number)
|
||||
/// - For each byte: `hash = hash * 33 + byte`
|
||||
/// - Uses wrapping arithmetic to prevent overflow panics
|
||||
pub fn hash_djb2(bytes: &[u8]) -> u32 {
|
||||
let mut hash: u32 = 5381;
|
||||
|
||||
for byte in bytes {
|
||||
hash = hash.wrapping_mul(33).wrapping_add(*byte as u32);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash() {
|
||||
let test_case = "/dev/ttyUSB1";
|
||||
let hash = hashing(test_case);
|
||||
let hash_2bytes = xor_u128_to_u16(hash);
|
||||
log::info!("{}", test_case);
|
||||
let hash_u32 = hash_djb2(test_case.as_bytes());
|
||||
let hash_u16 = byte_folding_u16(&hash_u32.to_le_bytes());
|
||||
tracing::info!("{}", test_case);
|
||||
assert_eq!(test_case, "/dev/ttyUSB1");
|
||||
assert!(hash == 8977446972540388683742);
|
||||
assert!(hash_2bytes == 165);
|
||||
println!("{:?}", hash);
|
||||
assert_eq!(hash_u32, 266121182);
|
||||
assert_eq!(hash_u16, 40962);
|
||||
}
|
||||
@@ -1,4 +1,26 @@
|
||||
/// Normalizes a value from a given range [min, max] to the range [0, 1].
|
||||
///
|
||||
/// This function maps a value from an arbitrary range to the standard
|
||||
/// normalized range [0, 1]. Values o/// Plot for Steepness = -1:ge are clamped
|
||||
/// to the boundaries (0.0 or 1.0).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - The input value to normalize
|
||||
/// * `min` - The minimum value of the input range
|
||||
/// * `max` - The maximum value of the input range
|
||||
///
|
||||
/// # Returns
|
||||
/// * Normalized value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Panics
|
||||
/// * Panics if `min >= max`
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::normalize;
|
||||
/// let normalized = normalize(5.0, 0.0, 10.0); // Returns 0.5
|
||||
/// let clamped = normalize(-2.0, 0.0, 10.0); // Returns 0.0 (clamped)
|
||||
/// ```
|
||||
pub fn normalize<T>(value: T, min: T, max: T) -> f64
|
||||
where
|
||||
T: Into<f64>,
|
||||
@@ -19,6 +41,24 @@ where
|
||||
}
|
||||
|
||||
/// Scales a normalized value from the range [0, 1] to a new range [new_min, new_max].
|
||||
///
|
||||
/// This function takes a normalized value (typically from 0 to 1) and maps it
|
||||
/// to a new target range. It's the inverse operation of normalization.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `normalized_value` - Input value in range [0.0, 1.0]
|
||||
/// * `new_min` - The minimum value of the target range
|
||||
/// * `new_max` - The maximum value of the target range
|
||||
///
|
||||
/// # Returns
|
||||
/// * Scaled value in the range [new_min, new_max]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::scale;
|
||||
/// let scaled = scale(0.5, 0.0, 100.0); // Returns 50.0
|
||||
/// let negative_range = scale(0.25, -10.0, 10.0); // Returns -5.0
|
||||
/// ```
|
||||
pub fn scale<T>(normalized_value: f64, new_min: T, new_max: T) -> T
|
||||
where
|
||||
T: Into<f64> + From<f64>,
|
||||
@@ -31,24 +71,395 @@ where
|
||||
T::from(result)
|
||||
}
|
||||
|
||||
/// Transforma normalized value from the range [0, 1] to a [0, 1] range using an normalized exponential function.
|
||||
pub fn interpolate_exponential(normalized_value: f64, a: f64) -> f64 {
|
||||
if a.abs() < 1e-10 {
|
||||
/// Performs hinge interpolation creating a piecewise linear function.
|
||||
///
|
||||
/// This function creates a piecewise linear curve that:
|
||||
/// - For values < x: linearly interpolates from (0, 0) to (x, y)
|
||||
/// - For values > x: linearly interpolates from (x, y) to (1, 1)
|
||||
/// - For value == x: returns y
|
||||
///
|
||||
/// The function assumes the input value is normalized to the range [0, 1],
|
||||
/// and creates a "hinge" or "elbow" shape at the point (x, y).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - The input value (typically in range [0, 1])
|
||||
/// * `x` - The x-coordinate of the hinge point (typically in range [0, 1])
|
||||
/// * `y` - The y-coordinate of the hinge point (typically in range [0, 1])
|
||||
///
|
||||
/// # Returns
|
||||
/// * Interpolated value based on the hinge function
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::interpolate_hinge;
|
||||
/// // Create a hinge at (0.3, 0.7)
|
||||
/// let result1 = interpolate_hinge(0.15, 0.3, 0.7); // Returns 0.35 (halfway to hinge)
|
||||
/// let result2 = interpolate_hinge(0.3, 0.3, 0.7); // Returns 0.7 (at hinge)
|
||||
/// let result3 = interpolate_hinge(0.65, 0.3, 0.7); // Returns 0.85 (halfway from hinge to 1)
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// Hinge at (0.8, 0.2)
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠤⠤⠒⠒⠊⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠒⠒⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⣀⣀⠤⠤⠔⠒⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠉⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
pub fn interpolate_hinge<T>(value: T, x: T, y: T) -> f64
|
||||
where
|
||||
T: Into<f64>,
|
||||
{
|
||||
let value_f64: f64 = value.into();
|
||||
let x_f64: f64 = x.into();
|
||||
let y_f64: f64 = y.into();
|
||||
|
||||
// If we are under X we scale linearly from 0 to y
|
||||
// If we are over X we scale linearly from y to 1
|
||||
if value_f64 < x_f64 {
|
||||
return 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;
|
||||
} else {
|
||||
return y_f64;
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms a normalized value from the range [0, 1] to a [0, 1] range using a normalized exponential function.
|
||||
///
|
||||
/// This function creates smooth curves that are either convex (fast start, slow end)
|
||||
/// or concave (slow start, fast end) depending on the steepness parameter sign.
|
||||
/// The curve always passes through (0,0) and (1,1).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `normalized_value` - Input value in range [0.0, 1.0]
|
||||
/// * `steepness` - Controls the curve shape:
|
||||
/// - `steepness = 0.0`: Linear interpolation (y = x)
|
||||
/// - `steepness > 0.0`: Convex curve (slow start → fast end)
|
||||
/// - `steepness < 0.0`: Concave curve (fast start → slow end)
|
||||
/// - Typical range: -5.0 to 5.0 for most applications
|
||||
///
|
||||
/// # Returns
|
||||
/// * Transformed value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::interpolate_exponential;
|
||||
/// let convex = interpolate_exponential(0.5, 2.0); // Slow start, fast end
|
||||
/// let concave = interpolate_exponential(0.5, -2.0); // Fast start, slow end
|
||||
/// let linear = interpolate_exponential(0.5, 0.0); // Linear (returns 0.5)
|
||||
/// ```
|
||||
///
|
||||
/// # Plots
|
||||
/// Steepness = -3
|
||||
/// Steepness = -3
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠔⠒⠒⠉⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = -1
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⢀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡲⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ -0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 0
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢂⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 1
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⢀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢂⣀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 3
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⣀⣀⠤⠤⠒⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠉⠉⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
pub fn interpolate_exponential(normalized_value: f64, steepness: f64) -> f64 {
|
||||
if steepness.abs() < 1e-10 {
|
||||
// When a is very close to zero, the function becomes linear
|
||||
return normalized_value;
|
||||
}
|
||||
let numerator = a.exp().powf(normalized_value) - 1.0;
|
||||
let denominator = a.exp() - 1.0;
|
||||
let numerator = steepness.exp().powf(normalized_value) - 1.0;
|
||||
let denominator = steepness.exp() - 1.0;
|
||||
|
||||
clip(numerator / denominator)
|
||||
}
|
||||
|
||||
/// A piecewise exponential interpolation function that creates smooth S-curves
|
||||
/// through the points (0,0), (0.5,0.5), and (1,1).
|
||||
///
|
||||
/// The function combines two normalized exponential segments:
|
||||
/// - First half (0 to 0.5): inverted exponential (concave, fast start → slow end)
|
||||
/// - Second half (0.5 to 1): standard exponential (convex, slow start → fast end)
|
||||
///
|
||||
/// This creates an S-like curve that's concave then convex, similar to
|
||||
/// a smoothed step function or sigmoid-like transition.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - Input value, should be in range [0.0, 1.0]
|
||||
/// * `steepness` - Controls how steep the S-curve is
|
||||
/// - `steepness = 0.0`: Linear interpolation (y = x)
|
||||
/// - `steepness > 0.0`: More pronounced S-curve
|
||||
/// - Higher values create sharper transitions at the boundaries
|
||||
/// - Typical range: 0.5 to 5.0 for most applications
|
||||
///
|
||||
/// # Returns
|
||||
/// * Interpolated y value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::interpolate_inflected_exponential;
|
||||
/// let y = interpolate_inflected_exponential(0.25, 2.0); // Gentle S-curve
|
||||
/// let y = interpolate_inflected_exponential(0.75, 5.0); // Sharp S-curve
|
||||
/// ```
|
||||
///
|
||||
/// # Plots
|
||||
/// Steepness = 0.5
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠜⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠜⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⢀⠤⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡢⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 1
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠒⠁⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⡰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⡰⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡲⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 2
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠃⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠉⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⠤⠒⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⣀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⡠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
///
|
||||
/// Steepness = 5
|
||||
/// ```ignore
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁ 1.0
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠔⠉⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡠⠤⠤⠤⠤⠤⠤⠤⠒⠒⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⠀⠀⠀⠀⢀⠔⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡁⠀⠀⢠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠄⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠂⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⣁⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⢼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁⠈⠀⠁ 0.0
|
||||
/// 0.0 1.0
|
||||
/// ```
|
||||
pub fn interpolate_inflected_exponential(x: f64, steepness: f64) -> f64 {
|
||||
debug_assert!((0.0..=1.0).contains(&x), "x must be in range [0.0, 1.0]");
|
||||
debug_assert!(steepness >= 0.0, "steepness must be non-negative");
|
||||
|
||||
if steepness == 0.0 {
|
||||
// Linear case
|
||||
return x;
|
||||
}
|
||||
|
||||
if x <= 0.5 {
|
||||
// First segment: inverted exponential (concave)
|
||||
// Maps [0, 0.5] → [0, 0.5] with concave shape
|
||||
let x_norm = 2.0 * x; // Scale to [0, 1]
|
||||
|
||||
// Use inverted exponential interpolation
|
||||
let result = 1.0 - interpolate_exponential(1.0 - x_norm, steepness);
|
||||
0.5 * result
|
||||
} else {
|
||||
// Second segment: standard exponential (convex)
|
||||
// Maps [0.5, 1] → [0.5, 1] with convex shape
|
||||
let x_norm = 2.0 * (x - 0.5); // Scale to [0, 1]
|
||||
|
||||
// Use standard exponential interpolation
|
||||
let result = interpolate_exponential(x_norm, steepness);
|
||||
0.5 + 0.5 * result
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverts a value from the range [0, 1] to [1, 0].
|
||||
///
|
||||
/// This function flips a normalized value, where 0 becomes 1 and 1 becomes 0.
|
||||
/// Values outside [0, 1] are first clamped to this range before inversion.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - Input value (will be clamped to [0.0, 1.0])
|
||||
///
|
||||
/// # Returns
|
||||
/// * Inverted value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::invert;
|
||||
/// let inverted = invert(0.3); // Returns 0.7
|
||||
/// let boundary = invert(0.0); // Returns 1.0
|
||||
/// let clamped = invert(1.5); // Returns 0.0 (input clamped to 1.0 first)
|
||||
/// ```
|
||||
pub fn invert(value: f64) -> f64 {
|
||||
let value = clip(value);
|
||||
1.0 - value
|
||||
}
|
||||
|
||||
/// Clips a value to the range [0, 1].
|
||||
///
|
||||
/// This function ensures that any input value is constrained to the
|
||||
/// normalized range [0, 1] by clamping values below 0 to 0 and
|
||||
/// values above 1 to 1.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - Input value to clip
|
||||
///
|
||||
/// # Returns
|
||||
/// * Clipped value in range [0.0, 1.0]
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use control_core::helpers::interpolation::clip;
|
||||
/// let normal = clip(0.5); // Returns 0.5
|
||||
/// let low = clip(-0.3); // Returns 0.0
|
||||
/// let high = clip(1.7); // Returns 1.0
|
||||
/// ```
|
||||
pub fn clip(value: f64) -> f64 {
|
||||
if value < 0.0 {
|
||||
return 0.0;
|
||||
@@ -178,6 +589,289 @@ mod tests {
|
||||
assert_relative_eq!(clip(1.5), 1.0, epsilon = EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_inflected_exponential() {
|
||||
// Test boundary points (0,0), (0.5,0.5), and (1,1) for various steepness values
|
||||
for steepness in [0.0, 0.5, 1.0, 2.0, 5.0].iter() {
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(0.0, *steepness),
|
||||
0.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(0.5, *steepness),
|
||||
0.5,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(1.0, *steepness),
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Test linear case (steepness = 0)
|
||||
for x in [0.0, 0.25, 0.5, 0.75, 1.0].iter() {
|
||||
assert_relative_eq!(
|
||||
interpolate_inflected_exponential(*x, 0.0),
|
||||
*x,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
// Test monotonicity - function should be strictly increasing
|
||||
for steepness in [0.5, 1.0, 2.0, 5.0].iter() {
|
||||
let mut prev = interpolate_inflected_exponential(0.0, *steepness);
|
||||
for i in 1..=20 {
|
||||
let x = i as f64 / 20.0;
|
||||
let current = interpolate_inflected_exponential(x, *steepness);
|
||||
assert!(
|
||||
current >= prev,
|
||||
"Failed monotonicity test at x={}, steepness={}, prev={}, current={}",
|
||||
x,
|
||||
steepness,
|
||||
prev,
|
||||
current
|
||||
);
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
// Test S-curve property: first half should be concave, second half convex
|
||||
let steepness = 2.0;
|
||||
|
||||
// First half: check concave property (derivative decreasing)
|
||||
let x1 = 0.1;
|
||||
let x2 = 0.2;
|
||||
let x3 = 0.3;
|
||||
let y1 = interpolate_inflected_exponential(x1, steepness);
|
||||
let y2 = interpolate_inflected_exponential(x2, steepness);
|
||||
let y3 = interpolate_inflected_exponential(x3, steepness);
|
||||
|
||||
let slope1 = (y2 - y1) / (x2 - x1);
|
||||
let slope2 = (y3 - y2) / (x3 - x2);
|
||||
assert!(
|
||||
slope1 > slope2,
|
||||
"First half should be concave (decreasing slope)"
|
||||
);
|
||||
|
||||
// Second half: check convex property (derivative increasing)
|
||||
let x1 = 0.7;
|
||||
let x2 = 0.8;
|
||||
let x3 = 0.9;
|
||||
let y1 = interpolate_inflected_exponential(x1, steepness);
|
||||
let y2 = interpolate_inflected_exponential(x2, steepness);
|
||||
let y3 = interpolate_inflected_exponential(x3, steepness);
|
||||
|
||||
let slope1 = (y2 - y1) / (x2 - x1);
|
||||
let slope2 = (y3 - y2) / (x3 - x2);
|
||||
assert!(
|
||||
slope1 < slope2,
|
||||
"Second half should be convex (increasing slope)"
|
||||
);
|
||||
|
||||
// Test that output is always in [0, 1] range
|
||||
for steepness in [0.0, 1.0, 5.0, 10.0].iter() {
|
||||
for i in 0..=100 {
|
||||
let x = i as f64 / 100.0;
|
||||
let y = interpolate_inflected_exponential(x, *steepness);
|
||||
assert!(
|
||||
y >= 0.0 && y <= 1.0,
|
||||
"Output {} out of range [0,1] for x={}, steepness={}",
|
||||
y,
|
||||
x,
|
||||
steepness
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_hinge() {
|
||||
// Test basic functionality with hinge at (0.3, 0.7)
|
||||
let x_hinge = 0.3;
|
||||
let y_hinge = 0.7;
|
||||
|
||||
// Test exact hinge point
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(x_hinge, x_hinge, y_hinge),
|
||||
y_hinge,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test left segment (value < x): should interpolate from (0,0) to (x,y)
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.0, x_hinge, y_hinge),
|
||||
0.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.15, x_hinge, y_hinge),
|
||||
0.35, // 0.15/0.3 * 0.7 = 0.5 * 0.7 = 0.35
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test right segment (value > x): should interpolate from (x,y) to (1,1)
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(1.0, x_hinge, y_hinge),
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.65, x_hinge, y_hinge),
|
||||
0.85, // y + ((0.65-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + 0.5 * 0.3 = 0.85
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test with different hinge points
|
||||
// Hinge at (0.5, 0.5) - should be linear
|
||||
assert_relative_eq!(interpolate_hinge(0.25, 0.5, 0.5), 0.25, epsilon = EPSILON);
|
||||
assert_relative_eq!(interpolate_hinge(0.75, 0.5, 0.5), 0.75, epsilon = EPSILON);
|
||||
|
||||
// Hinge at (0.2, 0.9) - steep left, shallow right
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.1, 0.2, 0.9),
|
||||
0.45, // 0.1/0.2 * 0.9 = 0.5 * 0.9 = 0.45
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.6, 0.2, 0.9),
|
||||
0.95, // 0.9 + ((0.6-0.2)/(1.0-0.2)) * (1.0-0.9) = 0.9 + 0.5 * 0.1 = 0.95
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test edge cases
|
||||
// Hinge at (0, 0)
|
||||
assert_relative_eq!(interpolate_hinge(0.0, 0.0, 0.0), 0.0, epsilon = EPSILON);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.5, 0.0, 0.0),
|
||||
0.5, // Should interpolate from (0,0) to (1,1)
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Hinge at (1, 1)
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.5, 1.0, 1.0),
|
||||
0.5, // Should interpolate from (0,0) to (1,1)
|
||||
epsilon = EPSILON
|
||||
);
|
||||
assert_relative_eq!(interpolate_hinge(1.0, 1.0, 1.0), 1.0, epsilon = EPSILON);
|
||||
|
||||
// Test with different numeric types
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(0.5f32, 0.3f32, 0.7f32),
|
||||
0.7857142857142857, // 0.7 + ((0.5-0.3)/(1.0-0.3)) * (1.0-0.7) = 0.7 + (2/7) * 0.3
|
||||
epsilon = 1e-7 // Relaxed epsilon for f32 precision
|
||||
);
|
||||
assert_relative_eq!(
|
||||
interpolate_hinge(1u8, 0u8, 1u8), // 1.0, 0.0, 1.0 - should return 1.0
|
||||
1.0,
|
||||
epsilon = EPSILON
|
||||
);
|
||||
|
||||
// Test monotonicity - function should be non-decreasing
|
||||
let test_x = 0.4;
|
||||
let test_y = 0.6;
|
||||
let mut prev = interpolate_hinge(0.0, test_x, test_y);
|
||||
|
||||
for i in 1..=20 {
|
||||
let value = i as f64 / 20.0;
|
||||
let current = interpolate_hinge(value, test_x, test_y);
|
||||
assert!(
|
||||
current >= prev,
|
||||
"Failed monotonicity test at value={}, x={}, y={}, prev={}, current={}",
|
||||
value,
|
||||
test_x,
|
||||
test_y,
|
||||
prev,
|
||||
current
|
||||
);
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_exponential_graph() {
|
||||
use textplots::{Chart, Plot, Shape};
|
||||
|
||||
println!("\n=== Exponential Interpolation Examples ===");
|
||||
|
||||
// Test different steepness values (both positive and negative)
|
||||
let steepness_values = [-3.0, -1.0, 0.0, 1.0, 3.0];
|
||||
|
||||
for steepness in steepness_values.iter() {
|
||||
println!("\nSteepness = {}", steepness);
|
||||
|
||||
// Generate data points
|
||||
let points: Vec<(f32, f32)> = (0..=50)
|
||||
.map(|i| {
|
||||
let x = i as f64 / 50.0;
|
||||
let y = interpolate_exponential(x, *steepness);
|
||||
(x as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create and display chart
|
||||
Chart::new(60, 60, 0.0, 1.0)
|
||||
.lineplot(&Shape::Lines(&points))
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_hinge_graph() {
|
||||
use textplots::{Chart, Plot, Shape};
|
||||
|
||||
println!("\n=== hinge Interpolation Examples ===");
|
||||
|
||||
// Test different hinge points
|
||||
let hinges = [(0.3, 0.7), (0.5, 0.5), (0.2, 0.9), (0.8, 0.2)];
|
||||
|
||||
for &(x_hinge, y_hinge) in hinges.iter() {
|
||||
println!("\nHinge at ({}, {})", x_hinge, y_hinge);
|
||||
|
||||
// Generate data points
|
||||
let points: Vec<(f32, f32)> = (0..=50)
|
||||
.map(|i| {
|
||||
let x = i as f64 / 50.0;
|
||||
let y = interpolate_hinge(x, x_hinge, y_hinge);
|
||||
(x as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create and display chart
|
||||
Chart::new(60, 60, 0.0, 1.0)
|
||||
.lineplot(&Shape::Lines(&points))
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolate_inflected_exponential_graph() {
|
||||
use textplots::{Chart, Plot, Shape};
|
||||
|
||||
// Test different steepness values
|
||||
let steepness_values = [0.5, 1.0, 2.0, 5.0];
|
||||
|
||||
for steepness in steepness_values.iter() {
|
||||
println!("\nSteepness = {}", steepness);
|
||||
|
||||
// Generate data points
|
||||
let points: Vec<(f32, f32)> = (0..=50)
|
||||
.map(|i| {
|
||||
let x = i as f64 / 50.0;
|
||||
let y = interpolate_inflected_exponential(x, *steepness);
|
||||
(x as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create and display chart
|
||||
Chart::new(60, 60, 0.0, 1.0)
|
||||
.lineplot(&Shape::Lines(&points))
|
||||
.display();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_normalize_invalid_range() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use smol::Timer;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::instrument;
|
||||
|
||||
pub struct LoopThrottle {
|
||||
target_cycle_time: Duration,
|
||||
@@ -32,7 +33,7 @@ impl LoopThrottle {
|
||||
on_update,
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn sleep(&mut self) {
|
||||
self.total_cycles += 1;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod loop_trottle;
|
||||
pub mod compare_lists;
|
||||
pub mod hashing;
|
||||
pub mod interpolation;
|
||||
pub mod loop_trottle;
|
||||
pub mod moving_time_window;
|
||||
pub mod retry;
|
||||
@@ -0,0 +1,261 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
iter::Sum,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// A helper trait to handle division for averaging
|
||||
pub trait DivideByCount {
|
||||
fn divide_by_count(self, count: usize) -> Self;
|
||||
}
|
||||
|
||||
impl DivideByCount for f64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for f32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as f32)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for i32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for i64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for u32 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivideByCount for u64 {
|
||||
fn divide_by_count(self, count: usize) -> Self {
|
||||
self / (count as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>;
|
||||
pub type I32Window = MovingTimeWindow<i32>;
|
||||
pub type I64Window = MovingTimeWindow<i64>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MovingTimeWindow<T>
|
||||
where
|
||||
T: Copy + PartialOrd + Sum + DivideByCount + std::fmt::Debug + Default,
|
||||
{
|
||||
max_samples: usize,
|
||||
samples: VecDeque<(T, Instant)>,
|
||||
last_sample: Option<(T, Instant)>,
|
||||
sample_time: Duration,
|
||||
cached_average: Option<T>,
|
||||
cached_max: Option<T>,
|
||||
cached_min: Option<T>,
|
||||
}
|
||||
|
||||
/// A time-based moving window that maintains samples within a specified duration and count limit.
|
||||
///
|
||||
/// This structure efficiently tracks values over time, automatically removing samples that fall
|
||||
/// outside the configured time window or exceed the maximum sample count. It provides cached
|
||||
/// average calculation to avoid redundant computations.
|
||||
impl<T> MovingTimeWindow<T>
|
||||
where
|
||||
T: Copy + PartialOrd + Sum + DivideByCount + std::fmt::Debug + Default,
|
||||
{
|
||||
/// Creates a new moving time window with the specified duration and maximum sample count.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The time window duration; samples older than this will be automatically removed
|
||||
/// * `samples` - Maximum number of samples to retain, regardless of their age
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `MovingTimeWindow` instance with empty sample collection and initialized cache
|
||||
pub fn new(duration: Duration, max_samples: usize) -> Self {
|
||||
Self {
|
||||
max_samples,
|
||||
samples: VecDeque::with_capacity(max_samples),
|
||||
last_sample: None,
|
||||
sample_time: duration / (max_samples as u32),
|
||||
cached_average: None,
|
||||
cached_max: None,
|
||||
cached_min: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the window with a new sample value at the given timestamp.
|
||||
///
|
||||
/// This method adds the new sample to the window and automatically removes samples that:
|
||||
/// - Are older than the configured duration from the current timestamp
|
||||
/// - Exceed the maximum sample count limit
|
||||
///
|
||||
/// The cached average is invalidated after this operation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `value` - The numeric value to add to the window
|
||||
/// * `timestamp` - The time at which this sample was taken
|
||||
pub fn update(&mut self, value: T, timestamp: Instant) {
|
||||
// Chekc if we should add a new sample based on the sample time and the last sample
|
||||
if let Some((_, last_timestamp)) = self.last_sample {
|
||||
if timestamp.duration_since(last_timestamp) < self.sample_time {
|
||||
// If the new sample is too close to the last one, we skip adding it
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have reached the maximum sample count, remove the oldest sample
|
||||
if self.samples.len() >= self.max_samples {
|
||||
self.samples.pop_back();
|
||||
}
|
||||
|
||||
// Add the new sample
|
||||
self.samples.push_front((value, timestamp));
|
||||
self.last_sample = Some((value, timestamp));
|
||||
|
||||
// Invalidate all cached values since we added a new sample
|
||||
self.cached_average = None;
|
||||
self.cached_max = None;
|
||||
self.cached_min = None;
|
||||
}
|
||||
|
||||
/// Calculates and returns the average of all samples currently in the window.
|
||||
///
|
||||
/// This method uses caching to avoid redundant calculations. The average is only
|
||||
/// recalculated when the window contents have changed since the last call.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The arithmetic mean of all current samples, or T::default() if the window is empty
|
||||
pub fn average(&mut self) -> T {
|
||||
// Return cached value if available
|
||||
if let Some(cached) = self.cached_average {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let sum: T = self.samples.iter().map(|(v, _)| *v).sum();
|
||||
let count = self.samples.len();
|
||||
|
||||
let avg = if count > 0 {
|
||||
sum.divide_by_count(count)
|
||||
} else {
|
||||
T::default()
|
||||
};
|
||||
|
||||
// Cache the calculated value
|
||||
self.cached_average = Some(avg);
|
||||
avg
|
||||
}
|
||||
|
||||
/// Calculates and returns the maximum value of all samples currently in the window.
|
||||
///
|
||||
/// This method uses caching to avoid redundant calculations. The maximum is only
|
||||
/// recalculated when the window contents have changed since the last call.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The maximum value of all current samples, or T::default() if the window is empty
|
||||
pub fn max(&mut self) -> T {
|
||||
// Return cached value if available
|
||||
if let Some(cached) = self.cached_max {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let result = self
|
||||
.samples
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.fold(None, |acc, val| match acc {
|
||||
None => Some(val),
|
||||
Some(current_max) => {
|
||||
if val > current_max {
|
||||
Some(val)
|
||||
} else {
|
||||
Some(current_max)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Cache the calculated value
|
||||
self.cached_max = Some(result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Calculates and returns the minimum value of all samples currently in the window.
|
||||
///
|
||||
/// This method uses caching to avoid redundant calculations. The minimum is only
|
||||
/// recalculated when the window contents have changed since the last call.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The minimum value of all current samples, or T::default() if the window is empty
|
||||
pub fn min(&mut self) -> T {
|
||||
// Return cached value if available
|
||||
if let Some(cached) = self.cached_min {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let result = self
|
||||
.samples
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.fold(None, |acc, val| match acc {
|
||||
None => Some(val),
|
||||
Some(current_min) => {
|
||||
if val < current_min {
|
||||
Some(val)
|
||||
} else {
|
||||
Some(current_min)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Cache the calculated value
|
||||
self.cached_min = Some(result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generic() {
|
||||
// Test that the struct compiles with basic numeric types
|
||||
let _: MovingTimeWindow<f64> = MovingTimeWindow::new(Duration::from_secs(1), 1);
|
||||
let _: MovingTimeWindow<f32> = MovingTimeWindow::new(Duration::from_secs(1), 1);
|
||||
let _: MovingTimeWindow<i32> = MovingTimeWindow::new(Duration::from_secs(1), 1);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,38 @@
|
||||
pub fn retry<T, E, F>(n: usize, mut f: F) -> Result<T, E>
|
||||
use std::future::Future;
|
||||
|
||||
/// Retries a fallible operation up to `n` times synchronously.
|
||||
///
|
||||
/// This function executes the provided closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - All retry attempts are exhausted
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
|
||||
/// * `f` - A mutable closure that returns a `Result<T, E>`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt if all retries are exhausted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_n_times;
|
||||
///
|
||||
/// let mut attempt = 0;
|
||||
/// let result = retry_n_times(3, || {
|
||||
/// attempt += 1;
|
||||
/// if attempt < 3 {
|
||||
/// Err("failed")
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// });
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// ```
|
||||
pub fn retry_n_times<T, E, F>(n: usize, mut f: F) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Result<T, E>,
|
||||
{
|
||||
@@ -11,3 +45,383 @@ where
|
||||
}
|
||||
Err(last_err.unwrap())
|
||||
}
|
||||
|
||||
/// Retries a fallible async operation up to `n` times.
|
||||
///
|
||||
/// This function executes the provided async closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - All retry attempts are exhausted
|
||||
///
|
||||
/// Each attempt is awaited before proceeding to the next retry or returning.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `n` - The maximum number of retry attempts (0 means only one attempt)
|
||||
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt if all retries are exhausted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_n_times_async;
|
||||
/// use std::sync::Arc;
|
||||
/// use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
///
|
||||
/// async fn example() {
|
||||
/// let attempt = Arc::new(AtomicUsize::new(0));
|
||||
/// let attempt_clone = attempt.clone();
|
||||
/// let result = retry_n_times_async(3, move || {
|
||||
/// let attempt = attempt_clone.clone();
|
||||
/// async move {
|
||||
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
/// if current < 3 {
|
||||
/// Err("failed")
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// }
|
||||
/// }).await;
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry_n_times_async<T, E, F, Fut>(n: usize, mut f: F) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<T, E>>,
|
||||
{
|
||||
let mut last_err: Option<E> = None;
|
||||
for _ in 0..=n {
|
||||
match f().await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap())
|
||||
}
|
||||
|
||||
/// Retries a fallible operation until a closure decides to stop retrying.
|
||||
///
|
||||
/// This function executes the provided closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - The retry condition returns false
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `f` - A mutable closure that returns a `Result<T, E>`
|
||||
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt when retries stop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_conditionally;
|
||||
///
|
||||
/// let mut attempt = 0;
|
||||
/// let result = retry_conditionally(
|
||||
/// || {
|
||||
/// attempt += 1;
|
||||
/// if attempt < 3 {
|
||||
/// Err(format!("failed attempt {}", attempt))
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// },
|
||||
/// |err| {
|
||||
/// // Retry up to 3 times
|
||||
/// !err.contains("attempt 3")
|
||||
/// }
|
||||
/// );
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// ```
|
||||
pub fn retry_conditionally<T, E, F, R>(mut f: F, mut should_retry: R) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Result<T, E>,
|
||||
R: FnMut(&E) -> bool,
|
||||
{
|
||||
loop {
|
||||
match f() {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if !should_retry(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retries a fallible async operation until a closure decides to stop retrying.
|
||||
///
|
||||
/// This function executes the provided async closure `f` repeatedly until either:
|
||||
/// - The operation succeeds (returns `Ok(T)`)
|
||||
/// - The retry condition returns false
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `f` - A mutable closure that returns a `Future<Output = Result<T, E>>`
|
||||
/// * `should_retry` - A closure that takes `&E` and returns `bool` to decide if retry should continue
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(T)` if any attempt succeeds, or `Err(E)` containing the error
|
||||
/// from the last failed attempt when retries stop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use control_core::helpers::retry::retry_conditionally_async;
|
||||
/// use std::sync::Arc;
|
||||
/// use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
///
|
||||
/// async fn example() {
|
||||
/// let attempt = Arc::new(AtomicUsize::new(0));
|
||||
/// let attempt_clone = attempt.clone();
|
||||
/// let result = retry_conditionally_async(
|
||||
/// move || {
|
||||
/// let attempt = attempt_clone.clone();
|
||||
/// async move {
|
||||
/// let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
/// if current < 3 {
|
||||
/// Err(format!("failed attempt {}", current))
|
||||
/// } else {
|
||||
/// Ok("success")
|
||||
/// }
|
||||
/// }
|
||||
/// },
|
||||
/// |err| {
|
||||
/// // Retry up to 3 times
|
||||
/// !err.contains("attempt 3")
|
||||
/// }
|
||||
/// ).await;
|
||||
/// assert_eq!(result, Ok("success"));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry_conditionally_async<T, E, F, Fut, R>(
|
||||
mut f: F,
|
||||
mut should_retry: R,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<T, E>>,
|
||||
R: FnMut(&E) -> bool,
|
||||
{
|
||||
loop {
|
||||
match f().await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if !should_retry(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Simple blocking executor for async functions in tests
|
||||
fn block_on<F: std::future::Future>(future: F) -> F::Output {
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::thread;
|
||||
|
||||
struct DummyWaker;
|
||||
impl std::task::Wake for DummyWaker {
|
||||
fn wake(self: Arc<Self>) {}
|
||||
}
|
||||
|
||||
let waker = Waker::from(Arc::new(DummyWaker));
|
||||
let mut context = Context::from_waker(&waker);
|
||||
let mut future = Box::pin(future);
|
||||
|
||||
loop {
|
||||
match future.as_mut().poll(&mut context) {
|
||||
Poll::Ready(result) => return result,
|
||||
Poll::Pending => {
|
||||
thread::yield_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_success_first_attempt() {
|
||||
let result = retry_n_times(3, || Ok::<i32, &str>(42));
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_success_after_failures() {
|
||||
let mut attempt = 0;
|
||||
let result = retry_n_times(3, || {
|
||||
attempt += 1;
|
||||
if attempt < 3 { Err("failed") } else { Ok(42) }
|
||||
});
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(attempt, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_all_attempts_fail() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, &str> = retry_n_times(2, || {
|
||||
attempt += 1;
|
||||
Err("always fails")
|
||||
});
|
||||
assert_eq!(result, Err("always fails"));
|
||||
assert_eq!(attempt, 3); // 0..=2 means 3 attempts
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_zero_retries() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, &str> = retry_n_times(0, || {
|
||||
attempt += 1;
|
||||
Err("failed")
|
||||
});
|
||||
assert_eq!(result, Err("failed"));
|
||||
assert_eq!(attempt, 1); // Only one attempt
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_success_first_attempt() {
|
||||
let result = block_on(retry_n_times_async(3, || async { Ok::<i32, &str>(42) }));
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_success_after_failures() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result = block_on(retry_n_times_async(3, move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
if *a < 3 { Err("failed") } else { Ok(42) }
|
||||
}
|
||||
}));
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(*attempt.borrow(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_all_attempts_fail() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result: Result<i32, &str> = block_on(retry_n_times_async(2, move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
Err("always fails")
|
||||
}
|
||||
}));
|
||||
assert_eq!(result, Err("always fails"));
|
||||
assert_eq!(*attempt.borrow(), 3); // 0..=2 means 3 attempts
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_and_success_with_condition() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, String> = retry_conditionally(
|
||||
|| {
|
||||
attempt += 1;
|
||||
if attempt < 3 {
|
||||
Err(format!("failed attempt {}", attempt))
|
||||
} else {
|
||||
Ok(42)
|
||||
}
|
||||
},
|
||||
|err| {
|
||||
// Retry as long as it's not the 3rd attempt
|
||||
!err.contains("attempt 3")
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(attempt, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_and_stop_early() {
|
||||
let mut attempt = 0;
|
||||
let result: Result<i32, String> = retry_conditionally(
|
||||
|| {
|
||||
attempt += 1;
|
||||
Err(format!("failed attempt {}", attempt))
|
||||
},
|
||||
|_err| {
|
||||
// Stop after first failure
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(attempt, 1);
|
||||
assert_eq!(result.unwrap_err(), "failed attempt 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_success_with_condition() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result: Result<i32, String> = block_on(retry_conditionally_async(
|
||||
move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
if *a < 3 {
|
||||
Err(format!("failed attempt {}", *a))
|
||||
} else {
|
||||
Ok(42)
|
||||
}
|
||||
}
|
||||
},
|
||||
|err| {
|
||||
// Retry as long as it's not the 3rd attempt
|
||||
!err.contains("attempt 3")
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(*attempt.borrow(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_async_stop_early() {
|
||||
let attempt = Rc::new(RefCell::new(0));
|
||||
let attempt_clone = attempt.clone();
|
||||
let result: Result<i32, String> = block_on(retry_conditionally_async(
|
||||
move || {
|
||||
let attempt = attempt_clone.clone();
|
||||
async move {
|
||||
let mut a = attempt.borrow_mut();
|
||||
*a += 1;
|
||||
Err(format!("failed attempt {}", *a))
|
||||
}
|
||||
},
|
||||
|_err| {
|
||||
// Stop after first failure
|
||||
false
|
||||
},
|
||||
));
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(*attempt.borrow(), 1);
|
||||
assert_eq!(result.unwrap_err(), "failed attempt 1");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
pub mod actors;
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod ethercat;
|
||||
pub mod helpers;
|
||||
pub mod machines;
|
||||
pub mod modbus;
|
||||
pub mod realtime;
|
||||
pub mod rest;
|
||||
pub mod serial;
|
||||
pub mod socketio;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::socketio::namespace::NamespaceInterface;
|
||||
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 dyn NamespaceInterface;
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -15,6 +17,16 @@ impl MachineIdentificationUnique {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use smol::lock::{Mutex, RwLock};
|
||||
use anyhow::anyhow;
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
use crate::{
|
||||
machines::{
|
||||
@@ -6,8 +11,9 @@ use crate::{
|
||||
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
|
||||
},
|
||||
serial::SerialDevice,
|
||||
socketio::event::GenericEvent,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{collections::HashMap, sync::{Arc, Weak}};
|
||||
|
||||
use super::{
|
||||
Machine,
|
||||
@@ -18,9 +24,9 @@ use super::{
|
||||
#[derive(Debug)]
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub serial_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl MachineManager {
|
||||
@@ -36,6 +42,8 @@ impl MachineManager {
|
||||
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();
|
||||
@@ -43,9 +51,8 @@ impl MachineManager {
|
||||
// group devices by machine device identification
|
||||
let device_grouping_result = group_devices_by_identification(device_identifications);
|
||||
|
||||
log::info!(
|
||||
"[{}::set_ethercat_devices] Device Groups {:?}",
|
||||
module_path!(),
|
||||
tracing::info!(
|
||||
"Device Groups {}",
|
||||
device_grouping_result.device_groups.len()
|
||||
);
|
||||
|
||||
@@ -63,6 +70,8 @@ impl MachineManager {
|
||||
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
|
||||
@@ -76,7 +85,7 @@ impl MachineManager {
|
||||
pub fn get(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<&Result<Box<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
@@ -87,6 +96,8 @@ impl MachineManager {
|
||||
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 };
|
||||
|
||||
@@ -99,13 +110,11 @@ impl MachineManager {
|
||||
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(),
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"[{}::add_serial_device] Adding serial machine {:?}",
|
||||
module_path!(),
|
||||
new_machine
|
||||
);
|
||||
tracing::info!("Adding serial machine {:?}", new_machine);
|
||||
|
||||
self.serial_machines.insert(
|
||||
device_identification_identified
|
||||
@@ -122,12 +131,57 @@ impl MachineManager {
|
||||
.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use smol::lock::Mutex;
|
||||
|
||||
@@ -9,14 +9,14 @@ pub enum MachineManagerIter<'a> {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
@@ -25,13 +25,13 @@ pub enum MachineManagerIter<'a> {
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines:
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Box<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Result<Box<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use crate::actors::Actor;
|
||||
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;
|
||||
@@ -10,4 +16,65 @@ pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineNewTrait + MachineApi + Actor + Any + Debug {}
|
||||
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,4 +1,4 @@
|
||||
use crate::serial::SerialDevice;
|
||||
use crate::{machines::manager::MachineManager, serial::SerialDevice};
|
||||
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
@@ -6,8 +6,11 @@ use ethercat_hal::{
|
||||
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::lock::RwLock;
|
||||
use std::sync::Arc;
|
||||
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>(
|
||||
@@ -17,6 +20,10 @@ pub trait MachineNewTrait {
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait MachineAct {
|
||||
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
|
||||
}
|
||||
|
||||
pub struct MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
@@ -39,6 +46,8 @@ pub struct MachineNewParams<
|
||||
'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<
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
|
||||
use anyhow::Error;
|
||||
use smol::lock::Mutex;
|
||||
use std::{any::TypeId, collections::HashMap};
|
||||
use std::{any::TypeId, collections::HashMap, sync::Arc};
|
||||
|
||||
pub type MachineNewClosure =
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Box<Mutex<dyn Machine>>, Error> + Send + Sync>;
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
|
||||
|
||||
pub struct MachineRegistry {
|
||||
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
|
||||
@@ -27,7 +27,7 @@ impl MachineRegistry {
|
||||
machine_identficiation,
|
||||
// create a machine construction closure
|
||||
Box::new(|machine_new_params| {
|
||||
Ok(Box::new(Mutex::new(T::new(machine_new_params)?)))
|
||||
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -36,7 +36,7 @@ impl MachineRegistry {
|
||||
pub fn new_machine(
|
||||
&self,
|
||||
machine_new_params: &MachineNewParams,
|
||||
) -> Result<Box<Mutex<dyn Machine>>, anyhow::Error> {
|
||||
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
|
||||
// get machiine identification
|
||||
let device_identification =
|
||||
&machine_new_params
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
pub mod modbus_serial_interface;
|
||||
|
||||
use anyhow::Error;
|
||||
use crc::{CRC_16_MODBUS, Crc};
|
||||
use serial;
|
||||
use std::time::Duration;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
pub enum ParityType {
|
||||
Even,
|
||||
Odd,
|
||||
@@ -22,6 +25,58 @@ pub enum ModbusFunctionCode {
|
||||
DiagnoseFunction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum ModbusExceptionCode {
|
||||
None = 0,
|
||||
IllegalFunction = 1,
|
||||
IllegalDataAddress = 2,
|
||||
IllegalDataValue = 3,
|
||||
SlaveDeviceFailure = 4,
|
||||
Acknowledge = 5,
|
||||
SlaveDeviceBusy = 6,
|
||||
MemoryParityError = 8,
|
||||
GatewayPathUnavailable = 10,
|
||||
GatewayTargetDeviceFailedToRespond = 11,
|
||||
Unknown(u8), // For any unknown exception codes
|
||||
}
|
||||
|
||||
impl From<ModbusExceptionCode> for u8 {
|
||||
fn from(code: ModbusExceptionCode) -> Self {
|
||||
match code {
|
||||
ModbusExceptionCode::None => 0,
|
||||
ModbusExceptionCode::IllegalFunction => 1,
|
||||
ModbusExceptionCode::IllegalDataAddress => 2,
|
||||
ModbusExceptionCode::IllegalDataValue => 3,
|
||||
ModbusExceptionCode::SlaveDeviceFailure => 4,
|
||||
ModbusExceptionCode::Acknowledge => 5,
|
||||
ModbusExceptionCode::SlaveDeviceBusy => 6,
|
||||
ModbusExceptionCode::MemoryParityError => 8,
|
||||
ModbusExceptionCode::GatewayPathUnavailable => 10,
|
||||
ModbusExceptionCode::GatewayTargetDeviceFailedToRespond => 11,
|
||||
ModbusExceptionCode::Unknown(code) => code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for ModbusExceptionCode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ModbusRequest {
|
||||
pub slave_id: u8,
|
||||
@@ -139,7 +194,7 @@ fn validate_modbus_response(raw_data: Vec<u8>) -> Result<Vec<u8>, Error> {
|
||||
|
||||
// expects to be given the entire raw message
|
||||
fn extract_crc(raw_data: &Vec<u8>) -> Result<u16, Error> {
|
||||
if raw_data.len() < 2 {
|
||||
if raw_data.len() < 5 {
|
||||
return Err(anyhow::anyhow!("Not enough data to extract CRC"));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
use crate::modbus::{ModbusRequest, ModbusResponse};
|
||||
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RequestMetaData {
|
||||
priority: u32,
|
||||
ignored_times: u32,
|
||||
/// This is used when a machine for example takes 4ms to process the request
|
||||
/// and is added ontop of the standard waiting time for a serial transfer
|
||||
extra_delay: Option<u32>,
|
||||
no_response_expected: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
/// WaitingForResponse is set after sending a request through the serial_interface
|
||||
WaitingForResponse,
|
||||
/// After Sending a Request we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has transmit accepted toggled
|
||||
/// Then we can set state = ReadyToSend
|
||||
WaitingForRequestAccept,
|
||||
/// After Receiving a Response we need to wait atleast one ethercat cycle
|
||||
/// After one Cycle we check if el6021 status has received accepted toggled
|
||||
WaitingForReceiveAccept,
|
||||
/// ReadyToSend is set after receiving the response from the serial_interface
|
||||
ReadyToSend,
|
||||
/// Initial State
|
||||
Uninitialized,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModbusSerialInterface {
|
||||
serial_interface: SerialInterface,
|
||||
baudrate: Option<u32>,
|
||||
encoding: Option<SerialEncoding>,
|
||||
response: Option<ModbusResponse>,
|
||||
state: State,
|
||||
last_message_size: usize,
|
||||
last_message_delay: u32,
|
||||
pub last_message_id: u32,
|
||||
no_response_expected: bool,
|
||||
request_map: HashMap<u32, ModbusRequest>,
|
||||
request_metadata_map: HashMap<u32, RequestMetaData>,
|
||||
last_ts: Instant,
|
||||
}
|
||||
|
||||
impl ModbusSerialInterface {
|
||||
pub fn new(serial_interface: SerialInterface) -> Self {
|
||||
Self {
|
||||
serial_interface,
|
||||
baudrate: None,
|
||||
encoding: None,
|
||||
request_map: HashMap::new(),
|
||||
request_metadata_map: HashMap::new(),
|
||||
response: None,
|
||||
last_message_size: 0,
|
||||
last_message_delay: 0,
|
||||
state: State::Uninitialized,
|
||||
last_ts: Instant::now(),
|
||||
last_message_id: 0,
|
||||
no_response_expected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
!matches!(self.state, State::Uninitialized)
|
||||
}
|
||||
|
||||
pub fn add_request(
|
||||
&mut self,
|
||||
request_id: u32,
|
||||
priority: u32,
|
||||
request: ModbusRequest,
|
||||
no_response_expected: bool,
|
||||
delay: Option<u32>,
|
||||
) {
|
||||
self.request_map.insert(request_id, request);
|
||||
self.request_metadata_map
|
||||
.entry(request_id)
|
||||
.or_insert_with(|| RequestMetaData {
|
||||
priority,
|
||||
ignored_times: 0,
|
||||
extra_delay: delay,
|
||||
no_response_expected,
|
||||
});
|
||||
}
|
||||
|
||||
/// This is used internally to read the receive buffer of the el6021
|
||||
async fn read_modbus_response(&mut self) -> Result<ModbusResponse, anyhow::Error> {
|
||||
let raw_response = (self.serial_interface.read_message)()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let response = ModbusResponse::try_from(raw_response)?;
|
||||
self.last_message_size = response.data.len() + 4;
|
||||
self.state = State::WaitingForReceiveAccept;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// This is used internally to fill the write buffer of the el6021 with the modbus request
|
||||
/// Decides what requests to send first by finding the one with the highest priority
|
||||
/// For example Highest Priority requests: ResetInverter StopMotor
|
||||
async fn send_modbus_request(&mut self) {
|
||||
if self.request_map.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (highest_id, delay, no_response_expected) = self
|
||||
.request_metadata_map
|
||||
.iter_mut()
|
||||
.map(|(key, value)| {
|
||||
let effective_priority = value.priority + value.ignored_times;
|
||||
(
|
||||
*key,
|
||||
effective_priority,
|
||||
value.extra_delay.unwrap_or(0),
|
||||
value.no_response_expected,
|
||||
)
|
||||
})
|
||||
.max_by_key(|(_, priority, _, _)| *priority)
|
||||
.map(|(key, _, delay, no_response)| (key, delay, no_response))
|
||||
.unwrap(); // Safe because we checked is_empty() above
|
||||
|
||||
let request = &self.request_map[&highest_id];
|
||||
let modbus_request: Vec<u8> = request.clone().into();
|
||||
|
||||
if let Err(_) = (self.serial_interface.write_message)(modbus_request.clone()).await {
|
||||
tracing::error!("ERROR: serial_interface.write_message has failed");
|
||||
} else {
|
||||
self.last_message_delay = delay;
|
||||
self.last_message_id = highest_id;
|
||||
self.no_response_expected = no_response_expected;
|
||||
}
|
||||
|
||||
self.state = State::WaitingForRequestAccept;
|
||||
self.last_message_size = modbus_request.len();
|
||||
}
|
||||
|
||||
pub async fn initialize_communication_settings(&mut self) -> bool {
|
||||
let baudrate = (self.serial_interface.get_baudrate)().await;
|
||||
let encoding = (self.serial_interface.get_serial_encoding)().await;
|
||||
|
||||
match (baudrate, encoding) {
|
||||
(Some(b), Some(e)) => {
|
||||
self.baudrate = Some(b);
|
||||
self.encoding = Some(e);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn initialize(&mut self) -> bool {
|
||||
if matches!(self.state, State::Uninitialized) {
|
||||
let success = (self.serial_interface.initialize)().await;
|
||||
if success {
|
||||
self.state = State::ReadyToSend;
|
||||
self.initialize_communication_settings().await;
|
||||
}
|
||||
success
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_ignored_times(&mut self) {
|
||||
for metadata in self.request_metadata_map.values_mut() {
|
||||
metadata.ignored_times += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Modbus RTU has silent time between frames that needs to be adhered to, if you send before silent_time is over between frames, then there will be lost frames
|
||||
/// This silent time is needed to identify the start and end of messages
|
||||
/// This function also takes into account the time that the slave we are talking to needs to process our request
|
||||
/// bits: amount of bits sent per byte -> for a 8n1 coding: 8 data bits, 0 parity, 1 stop bit (1 start,1 stop) -> 10 bits
|
||||
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
|
||||
/// baudrate: bits per second
|
||||
/// message_size: size of original message in bytes
|
||||
pub fn calculate_modbus_rtu_timeout(
|
||||
&self,
|
||||
machine_operation_delay: Duration,
|
||||
message_size: usize,
|
||||
) -> Option<Duration> {
|
||||
let baudrate = self.baudrate?;
|
||||
let encoding = self.encoding?;
|
||||
|
||||
let nanoseconds_per_bit = 1_000_000_000 / baudrate as u64;
|
||||
let nanoseconds_per_byte = encoding.total_bits() as u64 * nanoseconds_per_bit;
|
||||
|
||||
let transmission_timeout = nanoseconds_per_byte * message_size as u64;
|
||||
let silent_time = (nanoseconds_per_byte * 35) / 10; // silent_time is 3.5x of character length
|
||||
|
||||
let total_timeout =
|
||||
transmission_timeout + machine_operation_delay.as_nanos() as u64 + silent_time;
|
||||
Some(Duration::from_nanos(total_timeout))
|
||||
}
|
||||
|
||||
pub fn get_response(&self) -> Option<&ModbusResponse> {
|
||||
self.response.as_ref()
|
||||
}
|
||||
|
||||
pub async fn act(&mut self, now_ts: Instant) {
|
||||
let elapsed = now_ts.duration_since(self.last_ts);
|
||||
|
||||
let timeout = self.calculate_modbus_rtu_timeout(
|
||||
Duration::from_nanos(self.last_message_delay as u64),
|
||||
self.last_message_size,
|
||||
);
|
||||
|
||||
let Some(timeout) = timeout else { return };
|
||||
|
||||
// Check if we need to wait for timeout (except for specific states)
|
||||
if !matches!(
|
||||
self.state,
|
||||
State::Uninitialized | State::WaitingForReceiveAccept | State::WaitingForRequestAccept
|
||||
) && elapsed < timeout
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_ts = now_ts;
|
||||
self.response = None;
|
||||
|
||||
match self.state {
|
||||
State::WaitingForResponse => match self.read_modbus_response().await {
|
||||
Ok(response) => {
|
||||
self.response = Some(response);
|
||||
}
|
||||
Err(_) => {
|
||||
self.response = None;
|
||||
if self.no_response_expected {
|
||||
self.state = State::ReadyToSend;
|
||||
}
|
||||
}
|
||||
},
|
||||
State::ReadyToSend => {
|
||||
self.send_modbus_request().await;
|
||||
|
||||
// Remove the sent request
|
||||
self.request_map.remove(&self.last_message_id);
|
||||
self.request_metadata_map.remove(&self.last_message_id);
|
||||
self.increment_ignored_times();
|
||||
}
|
||||
State::WaitingForReceiveAccept => {
|
||||
// Waste at least one Ethercat Cycle here to ensure that request/response stay in sync
|
||||
self.state = State::ReadyToSend;
|
||||
}
|
||||
State::WaitingForRequestAccept => {
|
||||
// An empty vec is used to check if we are finished with writing the message
|
||||
// This is to keep the Serialinterface more simple
|
||||
match (self.serial_interface.write_message)(vec![]).await {
|
||||
Ok(finished) if finished => {
|
||||
self.state = State::WaitingForResponse;
|
||||
}
|
||||
Err(_) => {
|
||||
self.state = State::ReadyToSend;
|
||||
}
|
||||
_ => {} // Still waiting
|
||||
}
|
||||
}
|
||||
State::Uninitialized => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Makes the current thread a real-time thread using the PREEMPT_RT capabilities of Linux.
|
||||
///
|
||||
/// This function configures the current thread with real-time scheduling properties by:
|
||||
///
|
||||
/// 1. Setting the scheduling policy to SCHED_FIFO (First-In-First-Out), which is a real-time
|
||||
/// scheduling policy that allows the thread to run until it voluntarily yields, blocks on I/O,
|
||||
/// or is preempted by a higher-priority real-time thread.
|
||||
///
|
||||
/// 2. Setting the real-time priority to 80 (on a scale of 1-99, where higher values indicate
|
||||
/// higher priority). This priority level is chosen to be higher than most IRQ handlers
|
||||
/// (typically priority 50) but lower than critical kernel tasks (priority 99).
|
||||
///
|
||||
/// When running on a PREEMPT_RT patched Linux kernel, this combination provides deterministic
|
||||
/// scheduling with minimal latency, which is essential for real-time applications.
|
||||
///
|
||||
/// # Requirements
|
||||
///
|
||||
/// - The program must run with appropriate permissions to set real-time priorities.
|
||||
/// This typically means either:
|
||||
/// - Running as root
|
||||
/// - Configuring `/etc/security/limits.conf` to allow the user to set real-time priorities
|
||||
///
|
||||
/// - The system should be running a Linux kernel with PREEMPT_RT patch applied for
|
||||
/// optimal real-time performance.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() {
|
||||
/// // Lock memory to prevent page faults
|
||||
/// lock_memory();
|
||||
///
|
||||
/// // Create a thread and make it real-time
|
||||
/// std::thread::spawn(|| {
|
||||
/// set_realtime_priority();
|
||||
///
|
||||
/// // Real-time code here
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(())` if the real-time priority was successfully set
|
||||
/// - `Err` with the error message if setting the priority failed
|
||||
#[cfg(unix)]
|
||||
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
use anyhow::anyhow;
|
||||
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
|
||||
use std::mem;
|
||||
|
||||
unsafe {
|
||||
// Get the pthread_t of the current thread using pthread_self()
|
||||
let pthread_self = libc::pthread_self();
|
||||
|
||||
// Set up the scheduling parameters
|
||||
let mut param: sched_param = mem::zeroed();
|
||||
param.sched_priority = 95;
|
||||
|
||||
// Set the thread to use SCHED_FIFO scheduling policy with our priority
|
||||
let result = pthread_setschedparam(pthread_self, SCHED_FIFO, ¶m);
|
||||
|
||||
if result != 0 {
|
||||
error!(
|
||||
"Failed to set real-time scheduling policy for thread \"{:?}\": {}",
|
||||
std::thread::current().name().unwrap_or("unknown"),
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return Err(anyhow!(
|
||||
"Failed to set real-time priority: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
} else {
|
||||
debug!(
|
||||
"Successfully set real-time priority for the thread \"{:?}\"",
|
||||
std::thread::current().name().unwrap_or("unknown")
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
|
||||
log::error!("This platform does not support realtime threads");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locks all current and future memory pages of the process into RAM to prevent page faults.
|
||||
///
|
||||
/// # When to call this function
|
||||
///
|
||||
/// This function should be called from the main thread, as early as possible in your program's
|
||||
/// execution, ideally before creating any threads and before allocating large amounts of memory.
|
||||
///
|
||||
/// Important: You only need to call this function ONCE per process, not in each thread.
|
||||
/// The memory locking applies to the entire process, including:
|
||||
/// - The main thread
|
||||
/// - All threads created after the call
|
||||
///
|
||||
/// This happens because all threads in a process share the same virtual memory space, and
|
||||
/// `mlockall()` operates on the entire process's memory space, not just the calling thread.
|
||||
///
|
||||
/// # Example: Correct usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// // Call early in main, before creating threads
|
||||
/// lock_memory()?;
|
||||
///
|
||||
/// // Now create real-time threads...
|
||||
/// std::thread::spawn(|| {
|
||||
/// set_realtime_priority();
|
||||
/// // Real-time code here
|
||||
/// });
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Example: Incorrect usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() {
|
||||
/// // Don't do this - calling in each thread is unnecessary
|
||||
/// std::thread::spawn(|| {
|
||||
/// lock_memory().unwrap(); // Redundant if already called in main
|
||||
/// set_realtime_priority();
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # How it works
|
||||
///
|
||||
/// [Rest of documentation as before...]
|
||||
#[cfg(unix)]
|
||||
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use libc::{MCL_FUTURE, mlockall};
|
||||
|
||||
unsafe {
|
||||
if mlockall(MCL_FUTURE) != 0 {
|
||||
error!("Failed to lock memory: {}", std::io::Error::last_os_error());
|
||||
return Err(
|
||||
format!("Failed to lock memory: {}", std::io::Error::last_os_error()).into(),
|
||||
);
|
||||
} else {
|
||||
debug!("Successfully locked memory for the process");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
log::error!("This platform does not support memory locking");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_core_affinity_first_core(core_id: usize) -> Result<(), anyhow::Error> {
|
||||
let cores = core_affinity::get_core_ids().unwrap_or_default();
|
||||
let core = cores.get(core_id);
|
||||
if let Some(core) = core {
|
||||
core_affinity::set_for_current(*core);
|
||||
debug!(
|
||||
"Set core affinity of thread \"{:?}\" to core {:?}",
|
||||
std::thread::current().name().unwrap_or("unknown"),
|
||||
core
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
error!("No cores available to set affinity");
|
||||
Err(anyhow::anyhow!("No cores available to set affinity"))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::fmt::Debug;
|
||||
|
||||
use crate::machines::identification::DeviceIdentification;
|
||||
|
||||
pub mod panic;
|
||||
pub mod registry;
|
||||
pub mod serial_detection;
|
||||
|
||||
@@ -24,7 +25,7 @@ pub trait SerialDeviceThread {
|
||||
|
||||
pub struct SerialDeviceNewParams {
|
||||
pub path: String,
|
||||
pub device_thread_panix_tx: smol::channel::Sender<(String, anyhow::Error)>,
|
||||
pub device_thread_panic_tx: smol::channel::Sender<(String, anyhow::Error)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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
|
||||
}));
|
||||
}
|
||||
@@ -115,11 +115,7 @@ impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
|
||||
result.removed.push(device_identification);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"[{}::SerialDetection::check_ports] Removed port: {}",
|
||||
module_path!(),
|
||||
removed.0
|
||||
);
|
||||
tracing::trace!("Removed port: {}", removed.0);
|
||||
}
|
||||
|
||||
// add new ports
|
||||
@@ -133,7 +129,7 @@ impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
|
||||
let device_result = self.serial_device_registry.new_serial_device(
|
||||
&SerialDeviceNewParams {
|
||||
path: added.0.clone(),
|
||||
device_thread_panix_tx: self.device_removal_signal_tx.clone(),
|
||||
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
|
||||
},
|
||||
&serial_device_identification,
|
||||
);
|
||||
@@ -153,11 +149,7 @@ impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
|
||||
// add to result list
|
||||
result.added.push((device_identification, device.clone()));
|
||||
|
||||
log::debug!(
|
||||
"[{}::SerialDetection::check_ports] Added port: {}",
|
||||
module_path!(),
|
||||
added.0
|
||||
);
|
||||
tracing::trace!("Added port: {}", added.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,12 +166,7 @@ impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
|
||||
};
|
||||
self.ports.remove(&path);
|
||||
|
||||
log::debug!(
|
||||
"[{}::SerialDetection::check_remove_signals] Removed device: {:?}: {}",
|
||||
module_path!(),
|
||||
path,
|
||||
error
|
||||
);
|
||||
tracing::trace!("Removed device: {:?}: {}", path, error);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use erased_serde::Serialize as ErasedSerialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Serialize)]
|
||||
pub struct GenericEvent {
|
||||
pub name: String,
|
||||
pub data: Value,
|
||||
pub data: Box<dyn ErasedSerialize + Send + Sync>,
|
||||
/// Timestamp in milliseconds
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GenericEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GenericEvent")
|
||||
.field("name", &self.name)
|
||||
.field("data", &"[erased]")
|
||||
.field("ts", &self.ts)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Event<T>
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Send + Sync + 'static,
|
||||
{
|
||||
pub name: String,
|
||||
pub data: T,
|
||||
@@ -22,39 +32,35 @@ where
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
impl<T> TryFrom<Event<T>> for GenericEvent
|
||||
impl<T> From<Event<T>> for GenericEvent
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Send + Sync + 'static,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(event: Event<T>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
fn from(event: Event<T>) -> Self {
|
||||
Self {
|
||||
name: event.name,
|
||||
data: serde_json::to_value(event.data)?,
|
||||
data: Box::new(event.data),
|
||||
ts: event.ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryFrom<&Event<T>> for GenericEvent
|
||||
impl<T> From<&Event<T>> for GenericEvent
|
||||
where
|
||||
T: Serialize,
|
||||
T: Serialize + Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(event: &Event<T>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
fn from(event: &Event<T>) -> Self {
|
||||
Self {
|
||||
name: event.name.clone(),
|
||||
data: serde_json::to_value(&event.data)?,
|
||||
data: Box::new(event.data.clone()),
|
||||
ts: event.ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Event<T>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
T: Serialize + Clone + Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(event: &str, data: T) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod event;
|
||||
pub mod namespace;
|
||||
pub mod namespace_id;
|
||||
pub mod socket_queue;
|
||||
@@ -1,23 +1,64 @@
|
||||
use crate::socketio::event::GenericEvent;
|
||||
use socketioxide::{extract::SocketRef, socket::Sid};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::socket_queue::SocketQueue;
|
||||
#[derive(Debug)]
|
||||
pub struct Namespace {
|
||||
pub sockets: Vec<SocketRef>,
|
||||
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
}
|
||||
|
||||
pub trait NamespaceInterface {
|
||||
impl Namespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
sockets: vec![],
|
||||
events: HashMap::new(),
|
||||
socket_queue_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
/// Adds a socket to the namespace.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - A reference to the socket to be added
|
||||
fn subscribe(&mut self, socket: SocketRef);
|
||||
#[instrument(skip_all)]
|
||||
pub fn subscribe(&mut self, socket: SocketRef) {
|
||||
// add the socket to the list
|
||||
self.sockets.push(socket.clone());
|
||||
}
|
||||
|
||||
/// Removes a socket from the namespace.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - A reference to the socket to be removed
|
||||
fn unsubscribe(&mut self, socket: SocketRef);
|
||||
#[instrument(skip_all)]
|
||||
pub fn unsubscribe(&mut self, socket: SocketRef) {
|
||||
// remove the socket from the list
|
||||
self.sockets.retain(|s| s.id != socket.id);
|
||||
}
|
||||
|
||||
/// Disconnects all clients in the namespace.
|
||||
///
|
||||
/// This will forcefully disconnect all sockets and clear the socket list.
|
||||
#[instrument(skip_all)]
|
||||
pub fn disconnect_all(&mut self) {
|
||||
tracing::info!("Disconnecting {} sockets in namespace", self.sockets.len());
|
||||
|
||||
// Disconnect each socket
|
||||
for socket in &self.sockets {
|
||||
let _ = socket.clone().disconnect(); // Ignore errors if socket is already disconnected
|
||||
}
|
||||
|
||||
// Clear the socket list
|
||||
self.sockets.clear();
|
||||
}
|
||||
|
||||
/// Re-emits cached events to a specific socket.
|
||||
///
|
||||
@@ -27,14 +68,23 @@ pub trait NamespaceInterface {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - A reference to the socket that will receive the cached events
|
||||
fn reemit(&mut self, socket: SocketRef);
|
||||
#[instrument(skip_all)]
|
||||
pub fn reemit(&mut self, socket: SocketRef) {
|
||||
// Collect events grouped by name/kind with their counts for sorting
|
||||
let mut event_groups: Vec<(&String, &Vec<Arc<GenericEvent>>)> =
|
||||
self.events.iter().collect();
|
||||
|
||||
/// Emits an event to all sockets in the namespace.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event to be emitted
|
||||
fn emit(&mut self, event: &GenericEvent);
|
||||
// Sort by event count (ascending - lowest count first)
|
||||
event_groups.sort_by(|a, b| a.1.len().cmp(&b.1.len()));
|
||||
|
||||
// Emit events in order of lowest count first
|
||||
for (_event_name, events) in event_groups {
|
||||
for event in events {
|
||||
// Send to global queue instead of per-socket queue
|
||||
self.send_to_queue(&socket, &event, "reemit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches an event with a specific key for later retrieval.
|
||||
///
|
||||
@@ -43,112 +93,85 @@ pub trait NamespaceInterface {
|
||||
/// * `cache_key` - A string key to identify the cache
|
||||
/// * `event` - The event to be cached
|
||||
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
|
||||
#[instrument(skip_all)]
|
||||
fn cache(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
);
|
||||
|
||||
/// Emits an event to all sockets in the namespace and caches it.
|
||||
///
|
||||
/// This is a convenience method that combines the functionality of
|
||||
/// [`Namespace::emit`] and [`Namespace::cache`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event to be emitted and cached
|
||||
/// * `cache_key` - A string key to identify the cache
|
||||
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
|
||||
fn emit_cached(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Namespace {
|
||||
sockets: Vec<SocketRef>,
|
||||
events: HashMap<String, Vec<GenericEvent>>,
|
||||
socket_queues: HashMap<Sid, SocketQueue>,
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sockets: vec![],
|
||||
events: HashMap::new(),
|
||||
socket_queues: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NamespaceInterface for Namespace {
|
||||
fn subscribe(&mut self, socket: SocketRef) {
|
||||
// add the socket to the list
|
||||
self.sockets.push(socket.clone());
|
||||
// create a new queue for this socket
|
||||
self.socket_queues.insert(socket.id, SocketQueue::new());
|
||||
}
|
||||
|
||||
fn unsubscribe(&mut self, socket: SocketRef) {
|
||||
// remove the socket from the list
|
||||
self.sockets.retain(|s| s.id != socket.id);
|
||||
// remove the socket's queue
|
||||
self.socket_queues.remove(&socket.id);
|
||||
}
|
||||
|
||||
fn reemit(&mut self, socket: SocketRef) {
|
||||
if let Some(queue) = self.socket_queues.get(&socket.id) {
|
||||
// Collect events grouped by name/kind with their counts for sorting
|
||||
let mut event_groups: Vec<(&String, &Vec<GenericEvent>)> = self.events.iter().collect();
|
||||
|
||||
// Sort by event count (ascending - lowest count first)
|
||||
event_groups.sort_by(|a, b| a.1.len().cmp(&b.1.len()));
|
||||
|
||||
// Emit events in order of lowest count first
|
||||
for (_event_name, events) in event_groups {
|
||||
for event in events {
|
||||
queue.push(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
queue.flush(socket.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&mut self, event: &GenericEvent) {
|
||||
// Use the new emit function which combines push and flush
|
||||
for socket in self.sockets.clone() {
|
||||
if let Some(queue) = self.socket_queues.get(&socket.id) {
|
||||
queue.push(event.clone());
|
||||
queue.flush(socket.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cache(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
event: Arc<GenericEvent>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>,
|
||||
) {
|
||||
let mut cached_events_for_key = self
|
||||
.events
|
||||
.entry(event.name.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
buffer_fn(&mut cached_events_for_key, event);
|
||||
buffer_fn(&mut cached_events_for_key, &event);
|
||||
}
|
||||
|
||||
fn emit_cached(
|
||||
/// Emits an event to all sockets in the namespace and caches it.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event to be emitted and cached
|
||||
/// * `buffer_fn` - A function that defines how the event should be added to the cache buffer
|
||||
#[instrument(skip_all)]
|
||||
pub fn emit(
|
||||
&mut self,
|
||||
event: &GenericEvent,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>,
|
||||
event: Arc<GenericEvent>,
|
||||
buffer_fn: &Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>,
|
||||
) {
|
||||
// cache the event
|
||||
self.cache(event, buffer_fn);
|
||||
self.cache(event.clone(), &buffer_fn);
|
||||
|
||||
// emit the event
|
||||
self.emit(event);
|
||||
// emit the event - inlined from emit function
|
||||
// Send to global queue for each socket in the namespace
|
||||
for socket in self.sockets.clone() {
|
||||
self.send_to_queue(&socket, &event, "emit");
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an event to the global queue for a specific socket.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - The socket to send the event to
|
||||
/// * `event` - The event to be sent
|
||||
/// * `operation` - A description of the operation for logging purposes (e.g., "reemit", "emit")
|
||||
#[instrument(skip_all)]
|
||||
fn send_to_queue(&self, socket: &SocketRef, event: &Arc<GenericEvent>, operation: &str) {
|
||||
tracing::trace!(
|
||||
socket_id = ?socket.id,
|
||||
event = %event.name,
|
||||
operation = %operation,
|
||||
"Sending event to global queue"
|
||||
);
|
||||
match self
|
||||
.socket_queue_tx
|
||||
.try_send((socket.clone(), event.clone()))
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::trace!(
|
||||
socket_id = ?socket.id,
|
||||
event = %event.name,
|
||||
operation = %operation,
|
||||
"Successfully sent event to global queue"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
socket_id = ?socket.id,
|
||||
event = %event.name,
|
||||
operation = %operation,
|
||||
error = %e,
|
||||
"Failed to send event to global queue"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Namespace {
|
||||
fn drop(&mut self) {
|
||||
self.disconnect_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,17 +183,17 @@ pub trait NamespaceCacheingLogic<V>
|
||||
where
|
||||
V: CacheableEvents<V>,
|
||||
{
|
||||
fn emit_cached(&mut self, event: V);
|
||||
fn emit(&mut self, event: V);
|
||||
}
|
||||
|
||||
pub trait CacheableEvents<Events> {
|
||||
fn event_value(&self) -> Result<GenericEvent, serde_json::Error>;
|
||||
fn event_value(&self) -> GenericEvent;
|
||||
fn event_cache_fn(&self) -> CacheFn;
|
||||
}
|
||||
|
||||
pub type CacheFn = Box<dyn Fn(&mut Vec<GenericEvent>, &GenericEvent) -> ()>;
|
||||
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>;
|
||||
|
||||
/// [BufferFn] that stores the last n events
|
||||
/// [`BufferFn`] that stores the last n events
|
||||
pub fn cache_n_events(n: usize) -> CacheFn {
|
||||
Box::new(move |events, event| {
|
||||
if events.len() >= n {
|
||||
@@ -180,21 +203,42 @@ pub fn cache_n_events(n: usize) -> CacheFn {
|
||||
})
|
||||
}
|
||||
|
||||
/// [BufferFn] that stores only one event
|
||||
/// [`BufferFn`] that stores only one event
|
||||
pub fn cache_one_event() -> CacheFn {
|
||||
cache_n_events(1)
|
||||
}
|
||||
|
||||
/// [BufferFn] that stores events for a certain duration
|
||||
/// [`BufferFn`] that stores first and last event
|
||||
///
|
||||
/// The primary use case of this function is to cache both the default state of a machine, which should be emitted first,
|
||||
/// and the last event, which is the most recent state of the machine.
|
||||
pub fn cache_first_and_last_event() -> CacheFn {
|
||||
Box::new(move |events, event| {
|
||||
// if the events length 0 or 1, we just push the event
|
||||
if events.is_empty() || events.len() == 1 {
|
||||
events.push(event.clone());
|
||||
return;
|
||||
}
|
||||
// if the event length is 2 we remove the last event and append a new one
|
||||
if events.len() == 2 {
|
||||
events.remove(1);
|
||||
events.push(event.clone());
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// [`BufferFn`] that stores events for a certain duration
|
||||
pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
|
||||
Box::new(move |events, event| {
|
||||
// Use event.ts instead of system time
|
||||
let current_time = event.ts as u128;
|
||||
|
||||
// calculate current bucket & last bucket
|
||||
let bucket = current_time / bucket_size.as_millis();
|
||||
let bucket_size_ms = bucket_size.as_millis().max(1);
|
||||
let bucket = current_time / bucket_size_ms; // Avoid division by zero
|
||||
let last_bucket = match events.last() {
|
||||
Some(last_event) => last_event.ts as u128 / bucket_size.as_millis(),
|
||||
Some(last_event) => last_event.ts as u128 / bucket_size_ms,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
@@ -224,24 +268,32 @@ pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
|
||||
mod tests {
|
||||
use std::cmp::min;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct TestEventData {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_n_events() {
|
||||
// Create a cache function that stores the last 2 events
|
||||
let cache_fn = cache_n_events(2);
|
||||
|
||||
// Create namespace
|
||||
let mut namespace = Namespace::new();
|
||||
// Create namespace with dummy queue_tx
|
||||
let (queue_tx, _queue_rx) = smol::channel::unbounded();
|
||||
let mut namespace = Namespace::new(queue_tx);
|
||||
assert!(namespace.events.is_empty());
|
||||
|
||||
// Add event
|
||||
let event1 = GenericEvent {
|
||||
let event1 = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": 1}),
|
||||
data: Box::new(TestEventData { value: 1 }),
|
||||
ts: 0,
|
||||
};
|
||||
namespace.cache(&event1, &cache_fn);
|
||||
});
|
||||
namespace.cache(event1, &cache_fn);
|
||||
|
||||
// Check that we have one event name in the map
|
||||
assert_eq!(namespace.events.len(), 1);
|
||||
@@ -250,12 +302,12 @@ mod tests {
|
||||
assert_eq!(namespace.events.get("test_event").unwrap()[0].ts, 0);
|
||||
|
||||
// Add another event
|
||||
let event2 = GenericEvent {
|
||||
let event2 = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": 2}),
|
||||
data: Box::new(TestEventData { value: 2 }),
|
||||
ts: 1,
|
||||
};
|
||||
namespace.cache(&event2, &cache_fn);
|
||||
});
|
||||
namespace.cache(event2, &cache_fn);
|
||||
|
||||
// Still one event name
|
||||
assert_eq!(namespace.events.len(), 1);
|
||||
@@ -265,12 +317,12 @@ mod tests {
|
||||
assert_eq!(namespace.events.get("test_event").unwrap()[1].ts, 1);
|
||||
|
||||
// Add a third event, which should remove the first one
|
||||
let event3 = GenericEvent {
|
||||
let event3 = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": 3}),
|
||||
data: Box::new(TestEventData { value: 3 }),
|
||||
ts: 2,
|
||||
};
|
||||
namespace.cache(&event3, &cache_fn);
|
||||
});
|
||||
namespace.cache(event3, &cache_fn);
|
||||
|
||||
// Still one event name
|
||||
assert_eq!(namespace.events.len(), 1);
|
||||
@@ -289,19 +341,20 @@ mod tests {
|
||||
let duration = Duration::new(10, 0);
|
||||
let bucket_size = Duration::new(1, 0);
|
||||
let cache_fn = cache_duration(duration, bucket_size);
|
||||
let (queue_tx, _queue_rx) = smol::channel::unbounded();
|
||||
|
||||
// Create namespace
|
||||
let mut namespace = Namespace::new();
|
||||
let mut namespace = Namespace::new(queue_tx);
|
||||
assert!(namespace.events.is_empty());
|
||||
|
||||
// Add events every 100ms for 20 seconds
|
||||
for i in 0..200 {
|
||||
let event = GenericEvent {
|
||||
let event = Arc::new(GenericEvent {
|
||||
name: "test_event".to_string(),
|
||||
data: serde_json::json!({"value": i}),
|
||||
data: Box::new(TestEventData { value: i }),
|
||||
ts: (i * 100) as u64,
|
||||
};
|
||||
namespace.cache(&event, &cache_fn);
|
||||
});
|
||||
namespace.cache(event, &cache_fn);
|
||||
|
||||
let should_have_events = min(i / 10, 11);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
use super::event::GenericEvent;
|
||||
|
||||
/// A queue for managing events for a specific socket
|
||||
#[derive(Debug)]
|
||||
pub struct SocketQueue {
|
||||
queue: Arc<Mutex<VecDeque<GenericEvent>>>,
|
||||
is_flushing: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl SocketQueue {
|
||||
/// Create a new socket queue for the given socket ID
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: Arc::new(Mutex::new(VecDeque::new())),
|
||||
is_flushing: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an event to the queue
|
||||
pub fn push(&self, event: GenericEvent) {
|
||||
if let Ok(mut queue) = self.queue.lock() {
|
||||
queue.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Force flush events asynchronously (assumes flushing flag is already set)
|
||||
pub fn flush(&self, socket: SocketRef) {
|
||||
// Check if we're already flushing
|
||||
if self
|
||||
.is_flushing
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
// Already flushing, return early
|
||||
return;
|
||||
}
|
||||
|
||||
let queue = Arc::clone(&self.queue);
|
||||
let is_flushing = Arc::clone(&self.is_flushing);
|
||||
|
||||
// Spawn a smol task to flush the queue
|
||||
smol::spawn(async move {
|
||||
// Process events directly from the queue without collecting first
|
||||
loop {
|
||||
let event = {
|
||||
if let Ok(mut queue_guard) = queue.lock() {
|
||||
queue_guard.pop_front()
|
||||
} else {
|
||||
break; // Exit if we can't lock the queue
|
||||
}
|
||||
};
|
||||
|
||||
let Some(event) = event else {
|
||||
break; // No more events in queue
|
||||
};
|
||||
|
||||
// retry loop for each event
|
||||
loop {
|
||||
// check if socket is still connected
|
||||
if !socket.connected() {
|
||||
break; // Exit the loop if the socket is not connected
|
||||
}
|
||||
|
||||
match socket.emit("event", &event) {
|
||||
Ok(_) => break, // Successfully emitted, exit loop
|
||||
Err(e) => match e {
|
||||
socketioxide::SendError::Serialize(_) => {
|
||||
// no reason in retrying serialization errors
|
||||
break;
|
||||
}
|
||||
socketioxide::SendError::Socket(socket_error) => match socket_error {
|
||||
socketioxide::SocketError::InternalChannelFull => {
|
||||
// wait 10ms before retrying
|
||||
log::warn!(
|
||||
"Socket {} internal channel full, retrying in 10ms",
|
||||
socket.id,
|
||||
);
|
||||
smol::Timer::after(Duration::from_millis(10)).await;
|
||||
continue; // Retry sending the event
|
||||
}
|
||||
socketioxide::SocketError::Closed => {
|
||||
// Socket is closed, no point in retrying
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the flushing flag
|
||||
is_flushing.store(false, Ordering::SeqCst);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# Environment Setup
|
||||
|
||||
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
|
||||
|
||||
## Ubuntu/Debian Installation
|
||||
|
||||
```bash
|
||||
# Just press enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
apt install npm
|
||||
apt install nodejs
|
||||
# assuming you dont have git already, Git setup not included in here
|
||||
apt install git
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control
|
||||
cd electron
|
||||
npm install
|
||||
```
|
||||
|
||||
# Running Backend and Frontend Linux
|
||||
|
||||
## Backend
|
||||
|
||||
To Compile backend code and run it on Linux:
|
||||
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
|
||||
The script sets capabilities on the compiled binary like raw socket access.
|
||||
Here it is required that you are connected to atleast one machine that is communicated with over ethercat or usb/serial.
|
||||
The Most minimal working setup would be to connect over ethernet to an ek1100 beckhoff terminal.
|
||||
|
||||
## Mock-Machine
|
||||
|
||||
The Mock-Machine can be used to test code that does not require an actual machine connection.
|
||||
Like Frontend Code for example.
|
||||
|
||||
Mock machines can be used if the compile feature mock-machine is enabled.
|
||||
|
||||
```bash
|
||||
# in root git folder
|
||||
./cargo_run_linux.sh mock-machine
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
To run the Frontend Code:
|
||||
|
||||
```bash
|
||||
# need to be in the electron folder and an additional terminal
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
Generally you contribute to the codebase by:
|
||||
|
||||
1. Opening or Choosing an existing issue (Bug,Feature,Task etc)
|
||||
2. Work on it locally
|
||||
3. Commit your changes locally
|
||||
4. Before pushing changes, if there were changes on the master branch, rebase your branch like so:
|
||||
|
||||
```bash
|
||||
git fetch
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
5. Push changes
|
||||
|
||||
```bash
|
||||
# IF you needed to rebase
|
||||
git push --force-with-lease
|
||||
# ELSE you just push
|
||||
git push
|
||||
```
|
||||
|
||||
6. Open a Pull Request on Github and link the issue by writing fix #issue_number, after the pull request is merged into master the branch is closed automatically
|
||||
7. Request a Review and hope for the best :)
|
||||
|
||||
# Recommended Editor Setup
|
||||
|
||||
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Presets
|
||||
|
||||
To allow the user to save the current configuration of a machine, they can create a preset in the presets page. Presets are presited in electron's local storage.
|
||||
|
||||
Presets are all handled by the `PresetsPage`. To use the `PresetsPage` for a single machine, one must implement the following.
|
||||
|
||||
Create a zod schema for the data, the presets contain.
|
||||
```tsx
|
||||
const mock1PresetDataSchema = z
|
||||
.object({
|
||||
frequency1: z.number(),
|
||||
frequency2: z.number(),
|
||||
frequency3: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof mock1PresetDataSchema;
|
||||
```
|
||||
Here, intensionally, `Mock1` is the schema type itself. It can be used as a type argument everything that uses presets.
|
||||
|
||||
Next, create a map containing all schema versions currently supported (the new schema, to be precise).
|
||||
```tsx
|
||||
const schemas = new Map([[1, mock1PresetDataSchema]]);
|
||||
```
|
||||
New, the schema will be used to parse version `1`. To create new versions, see migrations below.
|
||||
|
||||
One also needs to define a table, how the preset data should be shown to the user.
|
||||
```tsx
|
||||
const previewEntries: PresetPreviewEntries<Mock1> = [
|
||||
{
|
||||
name: "Frequency 1",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency1?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 2",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency2?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 3",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency3?.toFixed(3),
|
||||
},
|
||||
];
|
||||
```
|
||||
`renderValue` gets the data itself and renders the corresponding part of it. Returning undefined will automatically be rendered as the value is not set, showing "N/A".
|
||||
|
||||
Create a function for the new react component and use the appropreate machine.
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Here, two more functions need to be created. const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,The first takes in a preset and applies it on the machine. If data is missing, use the default defined by the backend.
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
|
||||
const applyPreset = (preset: Preset<Mock1>) => {
|
||||
const frequency1 = preset.data?.frequency1 ?? 100;
|
||||
const frequency2 = preset.data?.frequency2 ?? 200;
|
||||
const frequency3 = preset.data?.frequency3 ?? 500;
|
||||
|
||||
setFrequency1(frequency1);
|
||||
setFrequency2(frequency2);
|
||||
setFrequency3(frequency3);
|
||||
};
|
||||
// ...
|
||||
}
|
||||
```
|
||||
The later does the opposite, it turns a machine state, comming from the backend, into the preset data
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
|
||||
useMock1();
|
||||
// ...
|
||||
const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,
|
||||
});
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Having all this, we can simple pass it to the `PresetsPage`
|
||||
```tsx
|
||||
export function Mock1PresetsPage() {
|
||||
// ...
|
||||
return (
|
||||
<PresetsPage
|
||||
machine_identification={mock1.machine_identification}
|
||||
currentState={toPresetData(state)}
|
||||
schemas={schemas}
|
||||
schemaVersion={1}
|
||||
applyPreset={applyPreset}
|
||||
previewEntries={previewEntries}
|
||||
defaultState={toPresetData(defaultState)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
Also mind the machine identification, that has to match the machine we are using and the schema versionen. Here we pass `1`, as we also used `1` in the schemas map.
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are handled by the `PresetsPage`, however, one still has to define what to migrate.
|
||||
To do so, first create the new schema.
|
||||
```tsx
|
||||
const fancyNewMock1PresetDataSchema = z
|
||||
.object({
|
||||
frequency1_now_with_ai: z.number(),
|
||||
frequency2: z.number(),
|
||||
frequency3: z.number(),
|
||||
frequency4: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof fancyNewMock1PresetDataSchema;
|
||||
```
|
||||
and then, also create a migration that uses the new schema, but can handle the old format.
|
||||
```tsx
|
||||
const mock1PresetDataSchemaMigration: PresetSchema = z.preprocess(
|
||||
(obj: any): PresetData<Mock1> => ({
|
||||
frequency1_now_with_ai: obj?.frequency1, // Here, a field have been renamed
|
||||
frequency2: obj?.frequency2,
|
||||
frequency3: obj?.frequency3,
|
||||
frequency4: 666.0, // A new field is added. Initialize it with default the value
|
||||
}),
|
||||
fancyNewMock1PresetDataSchema,
|
||||
);
|
||||
```
|
||||
Usings zods preprocessing step,
|
||||
we can first transform the data as needed and then validate using the new schema accordingly.
|
||||
|
||||
We have to register both schemas in our schema map.
|
||||
```tsx
|
||||
const schemas = new Map<number, Mock1>([
|
||||
[1, mock1PresetDataSchemaMigration as Mock1],
|
||||
[2, fancyNewMock1PresetDataSchema],
|
||||
]);
|
||||
```
|
||||
Mind the cast, which is ok, because `mock1PresetDataSchemaMigration` parses `fancyNewMock1PresetDataSchema` by construction.
|
||||
|
||||
Lastly, simply change the schema version being passed to the `PresetsPage`.
|
||||
```diff
|
||||
@@ -62,7 +77,7 @@ export function Mock1PresetsPage() {
|
||||
machine_identification={mock1.machine_identification}
|
||||
currentState={toPresetData(state)}
|
||||
schemas={schemas}
|
||||
- schemaVersion={1}
|
||||
+ schemaVersion={2}
|
||||
applyPreset={applyPreset}
|
||||
previewEntries={previewEntries}
|
||||
defaultState={toPresetData(defaultState)}
|
||||
```
|
||||
Make sure, that all (exsiting and new migrations) return a schema version 2 conform format.
|
||||
Of corse, one can chain migrations of needed - just the end result must be complient.
|
||||
@@ -27,13 +27,13 @@ We need to know what the purpose of each device in a machine is:
|
||||
|
||||
How and where do we save the identification values? A machine does not have a central storage medium. Saving the network configuration in the application is bad when changing the EtherCAT master device. Additionally, the serial number in the EEPROM is always `0`. Not all devices have the CoE key value store.
|
||||
|
||||
The solution: Persisting the values in empty spaces inside each device's EEPROM. As seen in the [ESC Access](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_io_intro/1358008331.html&id=) documentation, there are reserved spaces. With a hex dump of the EEPROM, we can confirm that these spaces are likely not used. The EEPROM is very small, so we can fit the values as four `u32` (Big Endian) values. In case we need to customize the position of the values, we customize the addresses for each device.
|
||||
The solution: Persisting the values in empty spaces inside each device's EEPROM. As seen in the [ESC Access](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_io_intro/1358008331.html&id=) documentation, there are reserved spaces. With a hex dump of the EEPROM, we can confirm that these spaces are likely not used. The EEPROM is very small, so we can fit the values as four `u16` (Big Endian) values. In case we need to customize the position of the values, we customize the addresses for each device.
|
||||
|
||||
The default word (not byte) addresses are:
|
||||
- Vendor: `0x0028`
|
||||
- Machine: `0x0029`
|
||||
- Serial: `0x002A`
|
||||
- Machine: `0x002C`
|
||||
- Role: `0x002E`
|
||||
- Role: `0x002B`
|
||||
|
||||
## Writing values
|
||||
1. For each new device, we need to identify free space in the EEPROM and add them to `get_identification_addresses` in `control-core/src/identification/mod.rs`.
|
||||
|
||||
@@ -11,6 +11,8 @@ A functionality can be virtually wired to any device that matches the IO type. E
|
||||
|
||||
Functionalities of different machines should not be wired to the same devices because of possibly conflicting configuration and machine identification issues (which devices belong to what machine).
|
||||
|
||||
The IO types provide direct synchronous methods for interacting with hardware, eliminating the need for actor patterns while maintaining clean abstractions.
|
||||
|
||||
# Implemented IO types
|
||||
|
||||
- Digital Output (DO)
|
||||
@@ -35,13 +37,13 @@ pub enum EL2002Port {
|
||||
|
||||
Then the device must implement the respective device trait like `DigitalOutputDevice`.
|
||||
|
||||
The `digital_output_write` is used to set the desired level of the output of a given port.
|
||||
The `set_output` method is used to set the desired level of the output of a given port.
|
||||
|
||||
The `digital_output_state` is used to read the current state of the output of a given port.
|
||||
The `get_output` method is used to read the current state of the output of a given port.
|
||||
|
||||
```rust
|
||||
impl DigitalOutputDevice<EL2002Port> for EL2002 {
|
||||
fn digital_output_write(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
|
||||
fn set_output(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
|
||||
let expect_text = "All channels should be Some(_)";
|
||||
match port {
|
||||
EL2002Port::DO1 => self.rxpdo.channel1.as_mut().expect(&expect_text).value = value.into(),
|
||||
@@ -49,84 +51,95 @@ impl DigitalOutputDevice<EL2002Port> for EL2002 {
|
||||
}
|
||||
}
|
||||
|
||||
fn digital_output_state(&self, port: EL2002Port) -> DigitalOutputState {
|
||||
fn get_output(&self, port: EL2002Port) -> DigitalOutputOutput {
|
||||
let expect_text = "All channels should be Some(_)";
|
||||
DigitalOutputState {
|
||||
output: DigitalOutputOutput(match port {
|
||||
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
|
||||
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
|
||||
}),
|
||||
}
|
||||
DigitalOutputOutput(match port {
|
||||
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
|
||||
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementing for an `Actor`
|
||||
## Using IO types in machine logic
|
||||
|
||||
The `act` function of an `Actor` will be called after the inputs are read from the device and before the outputs are written for the next EtherCAT frame.
|
||||
IO types now provide direct synchronous methods for reading and writing values, making them easy to use in machine logic without async/await patterns.
|
||||
|
||||
The actor needs to be constructed with the `DigitalOutput` and hold it in its struct.
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalOutputToggler {
|
||||
output: DigitalOutput,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Implementing the `act` function which will toggle the output every cycle.
|
||||
|
||||
The `state` and `write` values are callbacks and must be wrapped with parentheses to be called.
|
||||
```rust
|
||||
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
{
|
||||
let state = (self.output.state)().await;
|
||||
match state.output.into() {
|
||||
true => (self.output.write)(false.into()).await,
|
||||
false => (self.output.write)(true.into()).await,
|
||||
}
|
||||
}
|
||||
})
|
||||
impl DigitalOutputToggler {
|
||||
pub fn new(output: DigitalOutput) -> Self {
|
||||
Self { output }
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self) {
|
||||
let current_state = self.output.get();
|
||||
self.output.set(!current_state);
|
||||
}
|
||||
|
||||
pub fn turn_on(&mut self) {
|
||||
self.output.set(true);
|
||||
}
|
||||
|
||||
pub fn turn_off(&mut self) {
|
||||
self.output.set(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Getting `DigitalOutput` from a `Device`
|
||||
With the `new` function of the `DigitalOutput` struct, you can create a new `DigitalOutput` instance from a device and its port. We can then provide this `DigitalOutput` instance to the `Actor` struct.
|
||||
With the `new` function of the `DigitalOutput` struct, you can create a new `DigitalOutput` instance from a device and its port. The IO type provides direct synchronous methods for interaction.
|
||||
|
||||
```rust
|
||||
let el2002 = EL2002::new();
|
||||
let el2002 = Arc::new(RwLock::new(EL2002::new()));
|
||||
let digital_output = DigitalOutput::new(el2002, EL2002Port::DO1);
|
||||
let actor = DigitalOutputToggler::new(digital_output);
|
||||
let mut toggler = DigitalOutputToggler::new(digital_output);
|
||||
|
||||
// Direct usage
|
||||
toggler.turn_on();
|
||||
toggler.toggle();
|
||||
```
|
||||
|
||||
# Creating a new IO type
|
||||
To create a new IO type, we must know if it's writable (output) or readable (input), or combined (input/output) type.
|
||||
|
||||
We first define the `DigitalOutput` struct with the `write` and `state` fields. Every IO needs a `state` field but only writable types need a `write` field.
|
||||
|
||||
The `state` field is an async callback which returns the `DigitalOutputState` struct.
|
||||
|
||||
The `write` field is an async callback which takes the value.
|
||||
For output types, we define the struct with sync closures for setting and getting values:
|
||||
|
||||
```rust
|
||||
pub struct DigitalOutput {
|
||||
pub write: Box<dyn Fn(DigitalOutputOutput) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
|
||||
pub state:
|
||||
Box<dyn Fn() -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> + Send + Sync>,
|
||||
set_output: Box<dyn Fn(DigitalOutputOutput) -> () + Send + Sync>,
|
||||
get_output: Box<dyn Fn() -> DigitalOutputOutput + Send + Sync>,
|
||||
}
|
||||
```
|
||||
|
||||
Next, we need to implement the `DigitalOutputState` and `DigitalOutputOutput` structs.
|
||||
For input types, we only need the getter:
|
||||
|
||||
The `DigitalOutputState` contains the timestamps of the output (and inputs if applicable) and the `DigitalOutputOutput` struct (and the input struct if applicable). This is the return type of `DigitalOutput::state`.
|
||||
|
||||
The `DigitalOutputOutput` is both contained in the `DigitalOutputState` and is the input type of the `DigitalOutput::write` function. It implements the `From` trait in both directions to `bool` to allow easy access to the inner value on this simple type (as seen in the `act` example above).
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DigitalOutputState {
|
||||
pub output: DigitalOutputOutput,
|
||||
pub struct DigitalInput {
|
||||
get_input: Box<dyn Fn() -> Result<DigitalInputInput, anyhow::Error> + Send + Sync>,
|
||||
}
|
||||
```
|
||||
|
||||
For combined input/output types, we have separate getters for input and output:
|
||||
|
||||
```rust
|
||||
pub struct StepperVelocityEL70x1 {
|
||||
set_output: Box<dyn Fn(StepperVelocityEL70x1Output) -> Result<(), Error> + Send + Sync>,
|
||||
get_output: Box<dyn Fn() -> Result<StepperVelocityEL70x1Output, Error> + Send + Sync>,
|
||||
get_input: Box<dyn Fn() -> Result<StepperVelocityEL70x1Input, Error> + Send + Sync>,
|
||||
// ... additional closures as needed
|
||||
}
|
||||
```
|
||||
|
||||
Next, we need to implement the input and output structs separately.
|
||||
|
||||
For a digital output, we only need the output struct:
|
||||
|
||||
```rust
|
||||
/// Output value
|
||||
/// true: high
|
||||
/// false: low
|
||||
@@ -146,8 +159,35 @@ impl From<DigitalOutputOutput> for bool {
|
||||
}
|
||||
```
|
||||
|
||||
Now we need to implement the constructor for a `DigitalOutput` instance. The `new` function takes a device and a port and returns a `DigitalOutput` instance.
|
||||
We need to construct the async closures which is a bit of type madness since we also need to move the device `Arc` into them.
|
||||
For input types, we define input structs:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DigitalInputInput {
|
||||
pub value: bool,
|
||||
}
|
||||
```
|
||||
|
||||
For combined input/output types, we define both structs separately, avoiding the need for state containers.
|
||||
|
||||
```rust
|
||||
pub struct DigitalOutputOutput(pub bool);
|
||||
|
||||
impl From<bool> for DigitalOutputOutput {
|
||||
fn from(value: bool) -> Self {
|
||||
DigitalOutputOutput(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DigitalOutputOutput> for bool {
|
||||
fn from(value: DigitalOutputOutput) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now we implement the constructor for the IO type. The `new` function takes a device and port and returns the IO instance with sync closures:
|
||||
|
||||
```rust
|
||||
impl DigitalOutput {
|
||||
pub fn new<PORT>(
|
||||
@@ -157,46 +197,75 @@ impl DigitalOutput {
|
||||
where
|
||||
PORT: Clone + Send + Sync + 'static,
|
||||
{
|
||||
// build async write closure
|
||||
// build sync write closure
|
||||
let port1 = port.clone();
|
||||
let device1 = device.clone();
|
||||
let write = Box::new(
|
||||
move |value: DigitalOutputOutput| -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let device_clone = device1.clone();
|
||||
let port_clone = port1.clone();
|
||||
Box::pin(async move {
|
||||
let mut device = device_clone.write().await;
|
||||
device.digital_output_write(port_clone, value);
|
||||
})
|
||||
},
|
||||
);
|
||||
let set_output = Box::new(move |value: DigitalOutputOutput| {
|
||||
let mut device = block_on(device1.write());
|
||||
device.set_output(port1.clone(), value);
|
||||
});
|
||||
|
||||
// build async get closure
|
||||
// build sync get closure
|
||||
let port2 = port.clone();
|
||||
let device2 = device.clone();
|
||||
let state = Box::new(
|
||||
move || -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> {
|
||||
let device2 = device2.clone();
|
||||
let port_clone = port2.clone();
|
||||
Box::pin(async move {
|
||||
let device = device2.read().await;
|
||||
device.digital_output_state(port_clone)
|
||||
})
|
||||
},
|
||||
);
|
||||
DigitalOutput { write, state }
|
||||
let get_output = Box::new(move || -> DigitalOutputOutput {
|
||||
let device = block_on(device2.read());
|
||||
device.get_output(port2.clone())
|
||||
});
|
||||
|
||||
DigitalOutput {
|
||||
set_output,
|
||||
get_output,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the digital output value
|
||||
pub fn set(&self, enabled: bool) {
|
||||
(self.set_output)(enabled.into());
|
||||
}
|
||||
|
||||
/// Get the current output value
|
||||
pub fn get(&self) -> bool {
|
||||
let output = (self.get_output)();
|
||||
output.into()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Lastly, we can create the `DigitalOutputDevice` trait which needs to be implemented by the device.
|
||||
Lastly, we create the device trait that needs to be implemented by devices. The trait methods are simplified and separated by input/output:
|
||||
|
||||
```rust
|
||||
pub trait DigitalOutputDevice<PORT>: Send + Sync
|
||||
where
|
||||
PORT: Clone,
|
||||
{
|
||||
fn digital_output_write(&mut self, port: PORT, value: DigitalOutputOutput);
|
||||
fn digital_output_state(&self, port: PORT) -> DigitalOutputState;
|
||||
fn set_output(&mut self, port: PORT, value: DigitalOutputOutput);
|
||||
fn get_output(&self, port: PORT) -> DigitalOutputOutput;
|
||||
}
|
||||
```
|
||||
|
||||
For input devices:
|
||||
|
||||
```rust
|
||||
pub trait DigitalInputDevice<PORT>: Send + Sync
|
||||
where
|
||||
PORT: Clone,
|
||||
{
|
||||
fn get_input(&self, port: PORT) -> Result<DigitalInputInput, anyhow::Error>;
|
||||
}
|
||||
```
|
||||
|
||||
For combined input/output devices:
|
||||
|
||||
```rust
|
||||
pub trait StepperVelocityEL70x1Device<PORT>: Send + Sync
|
||||
where
|
||||
PORT: Clone,
|
||||
{
|
||||
fn set_output(&mut self, port: PORT, value: StepperVelocityEL70x1Output) -> Result<(), Error>;
|
||||
fn get_output(&self, port: PORT) -> Result<StepperVelocityEL70x1Output, Error>;
|
||||
fn get_input(&self, port: PORT) -> Result<StepperVelocityEL70x1Input, Error>;
|
||||
// Additional methods as needed for specific device functionality
|
||||
fn get_speed_range(&self, port: PORT) -> SpeedRange;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,479 @@
|
||||
# Logging and Tracing Configuration
|
||||
|
||||
This document describes the logging and tracing system for the QiTech control server, including structured logging, distributed tracing, and different output formats.
|
||||
|
||||
## Overview
|
||||
|
||||
The QiTech control server uses a modular tracing system built on top of the `tracing` crate, supporting multiple output formats and observability backends. The system is designed to be flexible and configurable through Cargo features.
|
||||
|
||||
## Features
|
||||
|
||||
The logging system supports three main features that can be enabled independently:
|
||||
|
||||
- **`tracing-fmt`**: Human-readable console output (default)
|
||||
- **`tracing-journald`**: systemd journal integration for production Linux systems
|
||||
- **`tracing-otel`**: OpenTelemetry integration for distributed tracing
|
||||
|
||||
### Default Configuration
|
||||
|
||||
By default, the server uses `tracing-fmt` for development-friendly console output:
|
||||
|
||||
```bash
|
||||
cargo run # Uses tracing-fmt by default
|
||||
```
|
||||
|
||||
## Feature-Based Configuration
|
||||
|
||||
### Format Logging (`tracing-fmt`)
|
||||
|
||||
The fmt logger provides structured, human-readable output to the console. This is the default feature and is ideal for development and debugging.
|
||||
|
||||
**Features:**
|
||||
- Colored output (when supported)
|
||||
- Thread names and IDs
|
||||
- Line numbers and file locations
|
||||
- Configurable time formats (debug vs release)
|
||||
- Target filtering
|
||||
|
||||
**Example output:**
|
||||
```
|
||||
2025-06-01T10:30:45.123Z INFO server::main: Starting QiTech Control Server
|
||||
2025-06-01T10:30:45.125Z DEBUG ethercat::init: Initializing EtherCAT master
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Default behavior
|
||||
cargo run
|
||||
|
||||
# Explicit feature selection
|
||||
cargo run --features tracing-fmt --no-default-features
|
||||
```
|
||||
|
||||
### Journal Logging (`tracing-journald`)
|
||||
|
||||
The journald logger integrates with systemd's journal for production Linux deployments. **This is the standard logging backend used on NixOS systems** and other systemd-based distributions.
|
||||
|
||||
**Features:**
|
||||
- Native systemd journal integration
|
||||
- Structured metadata preservation
|
||||
- System-level log aggregation
|
||||
- Log rotation and retention policies
|
||||
- Remote log collection support
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Enable journald logging
|
||||
cargo run --features tracing-journald --no-default-features
|
||||
|
||||
# Combined with other features
|
||||
cargo run --features "tracing-journald,tracing-otel"
|
||||
```
|
||||
|
||||
**Viewing logs on NixOS:**
|
||||
```bash
|
||||
# View all logs from the service
|
||||
journalctl -u qitech-control-server -f
|
||||
|
||||
# Filter by log level
|
||||
journalctl -u qitech-control-server -p info
|
||||
|
||||
# JSON output for structured data
|
||||
journalctl -u qitech-control-server -o json-pretty
|
||||
|
||||
# Follow logs with timestamp
|
||||
journalctl -u qitech-control-server -f --since "1 hour ago"
|
||||
```
|
||||
|
||||
### OpenTelemetry Tracing (`tracing-otel`)
|
||||
|
||||
OpenTelemetry provides distributed tracing capabilities by exporting traces to observability platforms like Jaeger, Zipkin, or commercial APM solutions.
|
||||
|
||||
**Features:**
|
||||
- Custom channel-based OTLP gRPC exporter for async runtime isolation
|
||||
- Hardcoded service metadata (no environment variables)
|
||||
- Dedicated Tokio thread for span export (isolated from main smol runtime)
|
||||
- Batch span processing for efficient export
|
||||
- Distributed trace correlation across services
|
||||
- Integration with HTTP middleware and database operations
|
||||
|
||||
**Technical Implementation:**
|
||||
The implementation uses a custom `ChannelSpanExporter` that sends spans via `std::sync::mpsc::channel` to a dedicated Tokio thread. This design completely isolates the OpenTelemetry Tokio dependency from the main application's smol-based async runtime, preventing runtime conflicts.
|
||||
|
||||
**Configuration:**
|
||||
- Service Name: `qitech-control-server`
|
||||
- Service Version: Automatically set from `Cargo.toml`
|
||||
- Service Namespace: `qitech`
|
||||
- OTLP Endpoint: `http://localhost:4317` (Jaeger gRPC endpoint)
|
||||
- Export Method: Channel-based with dedicated Tokio thread
|
||||
|
||||
**Setup Jaeger for Development:**
|
||||
|
||||
```bash
|
||||
# Run Jaeger with OTLP support
|
||||
docker run -d --name jaeger \
|
||||
-e COLLECTOR_OTLP_ENABLED=true \
|
||||
-p 16686:16686 \
|
||||
-p 14268:14268 \
|
||||
-p 4317:4317 \
|
||||
-p 4318:4318 \
|
||||
jaegertracing/all-in-one:latest
|
||||
|
||||
# Access Jaeger UI
|
||||
open http://localhost:16686
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Enable OpenTelemetry with OTLP export to Jaeger
|
||||
cargo run --features tracing-otel --no-default-features
|
||||
|
||||
# Combined with console output
|
||||
cargo run --features "tracing-fmt,tracing-otel"
|
||||
```
|
||||
|
||||
The server will automatically export traces to Jaeger at `http://localhost:4317`. All HTTP requests, database operations, and custom spans will be visible in the Jaeger UI. The implementation uses a custom channel-based exporter that spawns a dedicated Tokio thread for OTLP gRPC communication, ensuring compatibility with the main application's smol async runtime.
|
||||
|
||||
## Multiple Feature Combinations
|
||||
|
||||
You can enable multiple logging features simultaneously:
|
||||
|
||||
```bash
|
||||
# All logging features enabled
|
||||
cargo run --features "tracing-fmt,tracing-journald,tracing-otel"
|
||||
|
||||
# Production setup: journald + OpenTelemetry
|
||||
cargo run --features "tracing-journald,tracing-otel" --no-default-features
|
||||
|
||||
# Development setup: console + OpenTelemetry
|
||||
cargo run --features "tracing-fmt,tracing-otel" --no-default-features
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Log Level Configuration
|
||||
|
||||
The log level is controlled through the `RUST_LOG` environment variable:
|
||||
|
||||
```bash
|
||||
# Basic log levels
|
||||
RUST_LOG=debug cargo run
|
||||
RUST_LOG=info cargo run # Default
|
||||
RUST_LOG=warn cargo run
|
||||
RUST_LOG=error cargo run
|
||||
|
||||
# Module-specific filtering
|
||||
RUST_LOG=server=debug,ethercat=info cargo run
|
||||
|
||||
# Complex filtering
|
||||
RUST_LOG="info,tower_http=debug,axum=debug" cargo run
|
||||
```
|
||||
|
||||
### OpenTelemetry Configuration
|
||||
|
||||
When using the `tracing-otel` feature, the service configuration is hardcoded and uses a custom channel-based exporter for async runtime compatibility:
|
||||
|
||||
#### Service Configuration
|
||||
- **Service Name**: "qitech-control-server"
|
||||
- **Service Version**: Automatically set from Cargo.toml package version
|
||||
- **Service Namespace**: "qitech"
|
||||
- **OTLP Endpoint**: "http://localhost:4317" (Jaeger gRPC endpoint)
|
||||
|
||||
#### Implementation Details
|
||||
The OpenTelemetry implementation uses a custom `ChannelSpanExporter` that:
|
||||
1. Receives spans from the tracing layer via an MPSC channel
|
||||
2. Spawns a dedicated Tokio thread for OTLP gRPC export
|
||||
3. Isolates OpenTelemetry's async runtime from the main smol-based application
|
||||
4. Provides reliable span export to Jaeger without runtime conflicts
|
||||
|
||||
This design ensures that the OpenTelemetry tracing system works seamlessly with applications using different async runtimes (smol, tokio, etc.).
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Development Setup
|
||||
|
||||
For local development with console output:
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug cargo run --features tracing-fmt
|
||||
```
|
||||
|
||||
### Production Setup (NixOS)
|
||||
|
||||
For production deployment on NixOS systems, we use journald logging as the primary backend:
|
||||
|
||||
```bash
|
||||
RUST_LOG=info cargo run --features "tracing-journald,tracing-otel" --no-default-features
|
||||
```
|
||||
|
||||
This configuration is automatically used in our NixOS deployments through the system service configuration.
|
||||
|
||||
### Distributed Tracing with OpenTelemetry
|
||||
|
||||
The OpenTelemetry implementation provides full distributed tracing capabilities with automatic export to Jaeger:
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug cargo run --features "tracing-fmt,tracing-otel"
|
||||
```
|
||||
|
||||
This starts the application with both console logging and OpenTelemetry tracing enabled. Spans are automatically exported to Jaeger via the custom channel-based exporter, allowing you to view distributed traces in the Jaeger UI at `http://localhost:16686`.
|
||||
|
||||
### Setting Up Jaeger to Receive Traces
|
||||
|
||||
To collect and visualize traces from your application, you can run Jaeger as a trace receiver:
|
||||
|
||||
#### 1. Start Jaeger with OTLP Support
|
||||
|
||||
Run Jaeger using Docker with OTLP endpoints enabled:
|
||||
|
||||
```bash
|
||||
docker run -d --name jaeger \
|
||||
-e COLLECTOR_OTLP_ENABLED=true \
|
||||
-p 16686:16686 \
|
||||
-p 14268:14268 \
|
||||
-p 14250:14250 \
|
||||
-p 4317:4317 \
|
||||
-p 4318:4318 \
|
||||
jaegertracing/all-in-one:latest
|
||||
```
|
||||
|
||||
#### 2. Access Jaeger UI
|
||||
|
||||
Open your browser and go to:
|
||||
- **URL**: http://localhost:16686
|
||||
- **Service**: Select "qitech-control-server" from the dropdown
|
||||
- **Operation**: Choose specific operations to trace
|
||||
- **Find Traces**: Click to view collected traces
|
||||
|
||||
#### 3. Run Your Application with OpenTelemetry
|
||||
|
||||
The OpenTelemetry implementation automatically exports traces to Jaeger:
|
||||
|
||||
```bash
|
||||
# Run with OpenTelemetry tracing enabled
|
||||
cargo run --features "tracing-fmt,tracing-otel"
|
||||
```
|
||||
|
||||
The application uses these hardcoded service identifiers:
|
||||
- **Service Name**: "qitech-control-server"
|
||||
- **Service Version**: From Cargo.toml package version
|
||||
- **Service Namespace**: "qitech"
|
||||
- **OTLP Endpoint**: "http://localhost:4317" (gRPC)
|
||||
|
||||
Traces will automatically appear in the Jaeger UI under the service name "qitech-control-server". The custom channel-based exporter ensures reliable trace delivery even when using different async runtimes.
|
||||
|
||||
### Alternative: Using Docker Compose
|
||||
|
||||
Create a `docker-compose.yml` for easier management:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:latest
|
||||
ports:
|
||||
- "16686:16686" # Jaeger UI
|
||||
- "14268:14268" # HTTP collector
|
||||
- "14250:14250" # gRPC collector
|
||||
- "4317:4317" # OTLP gRPC
|
||||
- "4318:4318" # OTLP HTTP
|
||||
environment:
|
||||
- COLLECTOR_OTLP_ENABLED=true
|
||||
```
|
||||
|
||||
Run with:
|
||||
```bash
|
||||
docker-compose up -d jaeger
|
||||
```
|
||||
|
||||
### Other Observability Platforms
|
||||
|
||||
The current channel-based OpenTelemetry exporter is configured for Jaeger (OTLP gRPC endpoint). For other platforms like Grafana, DataDog, or custom OTLP collectors, you can modify the endpoint configuration in `src/logging/opentelemetry.rs` by updating the `OTLP_ENDPOINT` constant.
|
||||
|
||||
## Adding Tracing to Code
|
||||
|
||||
### Basic Logging
|
||||
|
||||
```rust
|
||||
use tracing::{info, debug, warn, error, trace};
|
||||
|
||||
pub fn my_function() {
|
||||
trace!("Very detailed debug information");
|
||||
debug!("Debug information for developers");
|
||||
info!("General information about program execution");
|
||||
warn!("Something unexpected happened");
|
||||
error!("An error occurred: {}", error_message);
|
||||
}
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
**Important**: When logging to journald (systemd's logging service), key-value pairs in event logs are not properly captured. Instead, include structured data using string formatting within the message itself.
|
||||
|
||||
```rust
|
||||
use tracing::{info, instrument};
|
||||
|
||||
// ❌ Don't use key-value pairs in event logs (won't work with journald)
|
||||
info!(
|
||||
user_id = %user.id,
|
||||
email = %user.email,
|
||||
"Updating user profile"
|
||||
);
|
||||
|
||||
// ✅ Use string formatting instead (works with journald)
|
||||
info!(
|
||||
"Updating user profile user_id={} email={}",
|
||||
user.id,
|
||||
user.email
|
||||
);
|
||||
|
||||
// ✅ Spans can still use structured fields
|
||||
#[instrument(fields(user_id = %user.id, operation = "update"))]
|
||||
pub fn update_user(user: &User) {
|
||||
info!(
|
||||
"Updating user profile user_id={} email={}",
|
||||
user.id,
|
||||
user.email
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Spans
|
||||
|
||||
```rust
|
||||
use tracing::{Span, instrument};
|
||||
|
||||
pub fn complex_operation() {
|
||||
let span = tracing::info_span!(
|
||||
"complex_operation",
|
||||
operation_id = 123,
|
||||
stage = "initialization"
|
||||
);
|
||||
let _enter = span.enter();
|
||||
|
||||
info!("Starting complex operation");
|
||||
|
||||
// Create child span
|
||||
let child_span = tracing::debug_span!("database_query");
|
||||
let _child_enter = child_span.enter();
|
||||
|
||||
debug!("Executing database query");
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```rust
|
||||
use tracing::{error, warn};
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn operation_with_error_handling() -> Result<()> {
|
||||
match risky_operation() {
|
||||
Ok(result) => {
|
||||
info!("Operation completed successfully result={:?}", result);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Operation failed error={}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Docker Compose Example
|
||||
|
||||
Complete observability stack with Jaeger:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:latest
|
||||
ports:
|
||||
- "16686:16686"
|
||||
- "14268:14268"
|
||||
environment:
|
||||
- COLLECTOR_OTLP_ENABLED=true
|
||||
|
||||
control-server:
|
||||
build: .
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- OTEL_EXPORTER=jaeger
|
||||
- OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:14268/api/traces
|
||||
- OTEL_SERVICE_NAME=qitech-control-server
|
||||
depends_on:
|
||||
- jaeger
|
||||
# Use journald for system logs
|
||||
logging:
|
||||
driver: journald
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Fmt logging**: Minimal overhead, suitable for development
|
||||
- **Journald logging**: Low overhead, optimized for production systems
|
||||
- **OpenTelemetry**: Moderate overhead due to span collection and channel-based export; the custom implementation minimizes impact by using a dedicated thread for OTLP communication
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No log output
|
||||
- Check `RUST_LOG` environment variable
|
||||
- Verify the correct features are enabled
|
||||
- Ensure the logging initialization is called in `main()`
|
||||
|
||||
### OpenTelemetry traces not appearing in Jaeger
|
||||
- Verify Jaeger is running with OTLP support on port 4317
|
||||
- Check that the `tracing-otel` feature is enabled
|
||||
- Ensure Jaeger OTLP collector is enabled: `COLLECTOR_OTLP_ENABLED=true`
|
||||
- Look for error messages in the application logs related to OTLP export
|
||||
- Verify the service name "qitech-control-server" appears in Jaeger's service dropdown
|
||||
|
||||
### Service name shows as "unknown_service:server" in Jaeger
|
||||
- This is a known issue where the resource configuration may not be applied correctly
|
||||
- The traces will still work correctly, but the service name display may be incorrect
|
||||
- Check the span metadata for the correct service information
|
||||
|
||||
### Journald logs not appearing
|
||||
- Ensure systemd is running
|
||||
- Check systemd service configuration
|
||||
- Verify the `tracing-journald` feature is enabled
|
||||
|
||||
## NixOS Configuration
|
||||
|
||||
The QiTech control server is automatically configured for optimal logging on NixOS systems:
|
||||
|
||||
### Package Configuration
|
||||
|
||||
The server package is built with the `tracing-journald` feature enabled:
|
||||
|
||||
```nix
|
||||
# In nixos/packages/server.nix
|
||||
buildFeatures = [ "tracing-journald" ];
|
||||
buildNoDefaultFeatures = true;
|
||||
```
|
||||
|
||||
### Service Configuration
|
||||
|
||||
The systemd service is configured to use journald logging:
|
||||
|
||||
```nix
|
||||
# In nixos/modules/qitech.nix
|
||||
systemd.services.qitech-control-server = {
|
||||
serviceConfig = {
|
||||
StandardOutput = "journal";
|
||||
StandardError = "journal";
|
||||
SyslogIdentifier = "qitech-control-server";
|
||||
};
|
||||
|
||||
environment = {
|
||||
RUST_LOG = "info,tower_http=debug,axum=debug";
|
||||
QITECH_OS = "1"; # Legacy compatibility
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
This configuration ensures that all logs are properly structured and integrated with the systemd journal.
|
||||
|
||||
For more specific configuration options, see the individual module documentation in the source code.
|
||||
@@ -134,46 +134,38 @@ A slave recognizes message data as one message when a **3.5 character long no-da
|
||||
|
||||
---
|
||||
|
||||
# Mitsubishi Inverter settings for Modbus RTU
|
||||
# Inverter settings
|
||||
|
||||
Protocol Selection: set Pr 549 to 1 (Modbus RTU)
|
||||
set 551 to a non zero value (9999)
|
||||
Operation mode selection: set Pr 79 to 0
|
||||
PU communication station number: set Pr 117 to a value of 1-32 (this is also the slave address)
|
||||
PU communication speed: set Pr 118 the same baudrate as the Beckhoff terminal. For example Terminal has 19200 then you need to set 192
|
||||
PU communication stop bit: set Pr 119 according to Beckhoff terminal setting
|
||||
PU communication parity check: set Pr 120 according to Beckhoff terminal setting
|
||||
## Motor Constants
|
||||
|
||||
# el6021 settings needed for Modbus RTU
|
||||
|
||||
Continuous is needed for Modbus RTU
|
||||
8000:04 Enable send fifo data continuous TRUE
|
||||
8000:06 Enable half duplex
|
||||
8000:11 Enable Baud rate 19200 Baud
|
||||
8000:15 Data Frame 8N2
|
||||
|
||||
# Inverter settings relevant for Motor
|
||||
|
||||
## Motor Constant
|
||||
|
||||
The following is a table that shows which Registers need to be set for the safe operation of the Motor.
|
||||
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
|
||||
|
||||
| Parameter number (Pr) | Name | Value |
|
||||
| --------------------- | ---------------------------- | ------------- |
|
||||
| 71 | Applied Motor | 0 |
|
||||
| 80 | Motor Capacity | 15 (1.5 kw) |
|
||||
| 96 | Auto tuning setting/status | 1 |
|
||||
| 1 | Max Frequency | 5000 |
|
||||
| 2 | Min Frequency | 0 |
|
||||
| 9 | Electronic Thermal O/L Relay | 3.30 (Ampere) |
|
||||
| 3 | Base frequency | 5000 (50 Hz) |
|
||||
| 19 | Base frequency voltage | 400 (Volt) |
|
||||
| `71` | Applied Motor | 0 |
|
||||
| `80` | Motor Capacity | 15 (1.5 kw) |
|
||||
| `96` | Auto tuning setting/status | 1 |
|
||||
| `1` | Max Frequency | 5000 |
|
||||
| `2` | Min Frequency | 0 |
|
||||
| `9` | Electronic Thermal O/L Relay | 3.30 (Ampere) |
|
||||
| `3` | Base frequency | 5000 (50 Hz) |
|
||||
| `19` | Base frequency voltage | 400 (Volt) |
|
||||
|
||||
## Settings relevant for Communication/Control
|
||||
## All Settings needed for the Inverter Communication/Operation with Extruder
|
||||
|
||||
| Parameter number (Pr) | Name | Value |
|
||||
| --------------------- | ------------------------------------ | ----- |
|
||||
| 79 | Operation Mode | 0 |
|
||||
| 340 | Communication startup mode selection | 1 |
|
||||
|
||||
The Combination of 79 = 0 and 340 = 1 starts the inverter in the NET operation Mode, which enables us to start/stop the motor.
|
||||
The Following settings are required to get the inverter Communication and Operation working properly
|
||||
| Parameter Number (Pr) | Value | Description |
|
||||
| --------------------- | ----- | -------------------------------------------------------------------------------------------- |
|
||||
| `Pr. 78` | 0 | Allows Reverse Rotation |
|
||||
| `Pr. 79` | 0 | Operation Mode Selection |
|
||||
| `Pr. 117` | 1 | Station Number (Slave Number) |
|
||||
| `Pr. 118` | 192 | Baudrate (192 → 19200 bps) |
|
||||
| `Pr. 119` | 0 | Stop Bit Length |
|
||||
| `Pr. 120` | 2 | Parity Check |
|
||||
| `Pr. 121` | 9999 | PU communication retry count |
|
||||
| `Pr. 122` | 1.0 | Communication Check Time (Time until no communication causes E.PUE fault), 0.1 seconds steps |
|
||||
| `Pr. 338` | 0 | Communication operation command source, 0 -> given through PU |
|
||||
| `Pr. 339` | 0 | Communication speed command source, 0 -> given through PU |
|
||||
| `Pr. 340` | 1 | Startup in Network Mode |
|
||||
| `Pr. 549` | 1 | Protocol Selection (1 = Modbus) |
|
||||
| `Pr. 551` | 9999 | Automatically Detect PU Connector |
|
||||
@@ -6,31 +6,32 @@ This guide will help you set up a NixOS system with QiTech Control and manage up
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### 1. Add Home Manager Channel
|
||||
### 1. Install Git
|
||||
|
||||
```bash
|
||||
sudo nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
|
||||
sudo nix-channel --update
|
||||
nixos-env -i git
|
||||
```
|
||||
|
||||
### 2. Configure System Files
|
||||
|
||||
Create the three main configuration files:
|
||||
|
||||
- `flake.nix` - Defines inputs, overlays, and system configuration
|
||||
- `configuration.nix` - Main NixOS system configuration
|
||||
- `home.nix` - Home Manager configuration for the qitech user
|
||||
|
||||
### 3. Build and Apply Configuration
|
||||
### 2. Clone the Control Repo
|
||||
|
||||
```bash
|
||||
# Build and switch to the new configuration
|
||||
sudo nixos-rebuild switch --flake .#nixos
|
||||
git clone https://github.com/qitechgmbh/control
|
||||
cd control
|
||||
```
|
||||
|
||||
### 3. Run Install Script
|
||||
|
||||
```bash
|
||||
# run as admin either with sudo or as root!
|
||||
sudo ./nixos-install.sh
|
||||
```
|
||||
|
||||
After the Installation script is finished the Computer should have rebooted and automatically started the control software.
|
||||
|
||||
### 4. Manual configuration
|
||||
|
||||
Disable any power management features or display sleep settings in the GNOME settings. Check that the on-screen keyboard is enabled under accessibility settings.
|
||||
Disable any power management features or display sleep settings in the GNOME settings.
|
||||
Check that the on-screen keyboard is enabled under accessibility settings.
|
||||
|
||||
## System Configuration Overview
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"endOfLine": "auto",
|
||||
"endOfLine": "lf",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/styles/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/styles/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "qitech-control-electron",
|
||||
"productName": "QiTech Control",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "QiTech Control Electron Application",
|
||||
"main": ".vite/build/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -38,25 +38,23 @@
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"electron": "34.0.2",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron": "^36.4.0",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
|
||||
"globals": "^15.15.0",
|
||||
"globals": "^16.2.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
@@ -67,6 +65,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@jub3i/tree-kill": "^0.0.15",
|
||||
"@radix-ui/react-collapsible": "^1.1.8",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
@@ -84,23 +83,25 @@
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3": "^7.9.0",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"i18next": "^25.0.2",
|
||||
"immer": "^10.1.1",
|
||||
"lucide-react": "^0.488.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-msgpack-parser": "^3.0.2",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -8,10 +8,14 @@ import { router } from "./routes/router";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { enableMapSet } from "immer";
|
||||
import { useGlobalLogStreaming } from "./hooks/useGlobalLogStreaming";
|
||||
|
||||
export default function App() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
// Start global log streaming
|
||||
useGlobalLogStreaming();
|
||||
|
||||
useEffect(() => {
|
||||
syncThemeWithLocal();
|
||||
updateAppLanguage(i18n);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Extruder Manual
|
||||
@@ -0,0 +1 @@
|
||||
# Laser Manual
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,84 @@
|
||||
# Mock Manual
|
||||
|
||||
## Overview
|
||||
|
||||
This is the operating manual for the Mock Machine control system. The Mock Machine is a simulated industrial device used for testing and development purposes.
|
||||
|
||||

|
||||
_Figure 1: Mock Machine Mascot_
|
||||
|
||||
## Safety Information
|
||||
|
||||
⚠️ **Important Safety Notes:**
|
||||
|
||||
- Always ensure proper safety protocols are followed
|
||||
- Verify all connections before operation
|
||||
- Keep emergency stop accessible at all times
|
||||
|
||||
## Operating Instructions
|
||||
|
||||
### Starting the Machine
|
||||
|
||||
1. **Power On Sequence**
|
||||
- Check all safety switches
|
||||
- Verify emergency stops are released
|
||||
- Power on the main controller
|
||||
|
||||
2. **Initial Setup**
|
||||
- Navigate to the Control tab
|
||||
- Check system status indicators
|
||||
- Verify all sensors are operational
|
||||
|
||||
### Control Interface
|
||||
|
||||
The mock machine interface provides:
|
||||
|
||||
- **Control Tab**: Main operation controls and status
|
||||
- **Graph Tab**: Real-time data visualization
|
||||
- **Manual Tab**: This handbook and documentation
|
||||
|
||||
### Monitoring
|
||||
|
||||
Use the Graph tab to monitor:
|
||||
|
||||
- System performance metrics
|
||||
- Sensor readings
|
||||
- Historical data trends
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Possible Cause | Solution |
|
||||
| -------------------- | -------------------- | ------------------------- |
|
||||
| No communication | Network disconnected | Check network connections |
|
||||
| Sensor errors | Sensor malfunction | Verify sensor wiring |
|
||||
| Control unresponsive | Software issue | Restart control system |
|
||||
|
||||
### Error Codes
|
||||
|
||||
- **E001**: Communication timeout
|
||||
- **E002**: Sensor out of range
|
||||
- **E003**: Emergency stop activated
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Daily Checks
|
||||
|
||||
- [ ] Verify all indicators are green
|
||||
- [ ] Check for error messages
|
||||
- [ ] Review system logs
|
||||
|
||||
### Weekly Maintenance
|
||||
|
||||
- [ ] Clean control panel
|
||||
- [ ] Check cable connections
|
||||
- [ ] Update system logs
|
||||
|
||||
## Contact Information
|
||||
|
||||
For technical support, contact the engineering team or refer to the system documentation.
|
||||
|
||||
---
|
||||
|
||||
_Last updated: June 2025_
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,689 @@
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
# EC Declaration of Conformity
|
||||
|
||||
**according to 2006/42/EC, Annex II, No. 1 A**
|
||||
|
||||
The content of the EC Declaration of Conformity corresponds to Annex II, No. 1 A, of Directive 2006/42/EC.
|
||||
|
||||
<br><br><br>
|
||||
|
||||
**Manufacturer of the machine:**
|
||||
|
||||
QiTech Industries GmbH
|
||||
|
||||
Landwehrsrtaße 55
|
||||
|
||||
64293, Darmstadt
|
||||
<br><br><br>
|
||||
|
||||
**Representative of the manufacturer:**
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
Landwehrsrtaße 55
|
||||
|
||||
64293, Darmstadt
|
||||
<br><br><br>
|
||||
|
||||
**under consideration:**
|
||||
| General designation | Filament winding machine |
|
||||
| ------------------- | ---------------------------------- |
|
||||
| function | Winding and rewinding of filaments |
|
||||
| Model | QiTech Winder |
|
||||
| type | Filament winding machine |
|
||||
| Trade name | QiTech Winder |
|
||||
<br><br>
|
||||
|
||||
We hereby declare that the above-mentioned machine is in conformity with all relevant provisions of the EC Machinery Directive 2006/42/EC. The machine is also in compliance with all relevant provisions of the following EC Directives:
|
||||
|
||||
- 2014/30/EU Electromagnetic Compatibility / European EMC Directive
|
||||
- 2014/35/EU Low Voltage Directive (protection objectives only)
|
||||
- 2011/65/EU ROHS Directives (ROHS 2)
|
||||
|
||||
The following harmonized standards (or parts of these standards) have been applied:
|
||||
|
||||
- DIN EN ISO 12100
|
||||
|
||||
Note: References of the EC directives, harmonized standards and standards and specifications can be found in the standards search
|
||||
|
||||
<br><br><br>
|
||||
|
||||
Darmstadt, 10/06/2025
|
||||
|
||||
64392 Darmstadt,
|
||||
|
||||
<img src="src/assets/markdown/winder/signature.jpeg" width="300" height="60"/>
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# Operating instructions based on DIN EN ISO 20607
|
||||
|
||||
| Designation | QiTech Winder |
|
||||
| ------------- | --------------------------------- |
|
||||
| Manufacturer | QiTech Industries GmbH |
|
||||
| Administrator | Milan von dem Bussche |
|
||||
| Address | Landwehrstraße 55 64293 Darmstadt |
|
||||
|
||||
<br><br>
|
||||
|
||||
**Read before commissioning!**
|
||||
|
||||
These operating instructions are intended to enable all persons working on the machine to carry out their work safely and correctly.
|
||||
|
||||
Every person who operates, maintains or repairs the machine must have read the operating instructions, in particular the safety regulations, before commissioning.
|
||||
|
||||
These operating instructions must be available on the machine at all times. The operator must ensure that all information necessary for the safe operation of the machine is made available to the operating personnel.
|
||||
|
||||
If the operating instructions are available in electronic form, the operator must ensure that the operating personnel is able to view the operating instructions.
|
||||
|
||||
The original version of these operating instructions is written in English.
|
||||
|
||||
We therefore ask you to read through the relevant instructions carefully and to observe them thoroughly.
|
||||
|
||||
After unpacking the machine, check for any damage that may have occurred during transport.
|
||||
|
||||
<br>
|
||||
|
||||
ATTENTION: Before using the machine for the first time, please inform yourself about the national regulations (occupational safety, environment).
|
||||
|
||||
<br>
|
||||
|
||||
Reproduction and reprinting in any form, even in extracts, requires the written authorization of QiTech Industries.
|
||||
|
||||
Operating instructions
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 1 Machine description
|
||||
|
||||

|
||||
|
||||
| Item number | Item number Description |
|
||||
| ----------- | ----------------------------------------- |
|
||||
| 1 | Spool axis |
|
||||
| 2 | Coil quick-release fastener |
|
||||
| 3 | Grooved plate |
|
||||
| 4 | Traverse |
|
||||
| 5 | Installer |
|
||||
| 6 | Laser diode |
|
||||
| 7 | Load arm |
|
||||
| 8 | Laser measuring system (optional) |
|
||||
| 9 | Puller rollers |
|
||||
| 10 | Filament guide |
|
||||
| 11 | Emergency stop switch |
|
||||
| 12 | LAN-cable |
|
||||
| 13 | Connecter cable (if purchased with laser) |
|
||||
| 14 | Power supply cable |
|
||||
| 15 | Underframe |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 1.1 Noise emissions
|
||||
|
||||
**Sound power level (LWAC)**
|
||||
|
||||
| Specification | Value | Unit |
|
||||
| ------------- | ----- | ---- |
|
||||
| Idle mode | <70 | dB |
|
||||
|
||||
<br><br>
|
||||
|
||||
**Workplace-related emission value (LPAC)**
|
||||
|
||||
| Specification | Value | Unit |
|
||||
| ------------- | ----- | ---- |
|
||||
| Idle mode | <70 | dB |
|
||||
|
||||
Noise emission specification according to DIN 45649 Part 1
|
||||
|
||||
<br><br>
|
||||
|
||||
Noise measurement method and operating condition acc: DIN 45635 Part 16 and Part 1605
|
||||
|
||||
Accuracy class: 2
|
||||
|
||||
<br>
|
||||
|
||||
# 1.2 General, important Safety instructions
|
||||
|
||||
1. Only professionally trained persons may operate the machine. The guarantee and warranty will be invalidated if damage is caused by improper operation.
|
||||
2. We would like to point out that no liability is accepted for damage caused by failure to observe these operating instructions.
|
||||
3. The operator of the machine must ensure that at least one copy of the operating instructions is kept in the immediate vicinity of the machine and is accessible to persons working with the machine.
|
||||
4. The operator must ensure that the safety and danger notices on the machine are observed and that the signs are clearly legible.
|
||||
5. Never work without the prescribed personal protective equipment (e.g. safety footwear).
|
||||
6. Wear close-fitting clothing and a hairnet if you have long hair. Do not wear loose or baggy clothing (ties, shirt sleeves, jewelry, etc.).
|
||||
7. Activities with a risk of entrapment may only be carried out without gloves. If gloves are required for individual activities, the machine must first be brought into a safe condition.
|
||||
8. If the emission sound pressure level at the workplace exceeds 80 dB (A), hearing protection must be worn.
|
||||
9. Never leave the machine in operation without supervision.
|
||||
10. Secure your machine so that it cannot be accessed by children. Persons who have not been instructed must not operate the machine.
|
||||
11. Before using the machine, make sure that it is in perfect condition (carry out a visual inspection before starting work). Pay particular attention to any damage to the earthing contact plug or the electrical connection. Never use the machine with defective, crushed or bare cables.
|
||||
12. Insert the earthing contact plug into an earthing contact socket suitable for the machine. The supply cable for the machine may only be connected to a socket outlet with earthing contact or a connection box. Have the socket outlet or connection box checked by a qualified electrician beforehand.
|
||||
13. During maintenance and cleaning work, the machine must be switched off and the earthing contact plug disconnected. Work on the electrical equipment of the machine may only be carried out by qualified electricians. The safety regulations for electrical engineering must be observed:
|
||||
- Switch off the machine
|
||||
- Disconnect the LAN-connection
|
||||
- Secure the machine against being switched on again
|
||||
- Have the machine de-energized by a qualified electrician
|
||||
- Earth and short-circuit the machine
|
||||
- Cover or isolate neighboring live parts
|
||||
|
||||
14. Do not reach into moving parts of the machine.
|
||||
15. Keep a safe distance from rotating parts of the machine. Only change the bobbin when the bobbin axis is stationary.
|
||||
16. Keep the work area clean. Do not place any objects on the machine.
|
||||
17. Always switch off the machine when you are not using it.
|
||||
18. Stay with the machine until it has come to a standstill.
|
||||
19. Repairs may only be carried out by qualified specialist personnel! Repair work may only be carried out by persons who are qualified for the repair work in question and who are familiar with the relevant occupational safety regulations.
|
||||
20. Protect the machine from moisture.
|
||||
21. Check the machine regularly for damage. Only replace damaged parts with original parts and have them replaced by qualified specialists. The guarantee and warranty will be invalidated if accessories and spare parts are used that are not authorized for the machine.
|
||||
22. To avoid inadequate lighting, we recommend installing light sources in accordance with national health and safety regulations (e.g. workplace directive).
|
||||
23. All persons working on the machine must be trained based on these operating instructions before starting work. In particular, persons working on the machine must be familiarized with the safety markings described in chapter 1.3.
|
||||
|
||||
<br>
|
||||
|
||||
| Picture | Danger! |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
## 1.3 Description of the pictograms, symbols and warning notices used
|
||||
|
||||
| Pictogram | Description of pictogram | Pictogram | Pictogram Description |
|
||||
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# Prohibition signs
|
||||
|
||||
| Prohibition signs Pictogram | Pictogram description | International pictograms / other Pictogram | Pictogram Description |
|
||||
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| <img src="src/assets/markdown/winder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="src/assets/markdown/winder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="src/assets/markdown/winder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="src/assets/markdown/winder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="src/assets/markdown/winder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="src/assets/markdown/winder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="src/assets/markdown/winder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="src/assets/markdown/winder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="src/assets/markdown/winder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="src/assets/markdown/winder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="src/assets/markdown/winder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="src/assets/markdown/winder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="src/assets/markdown/winder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="src/assets/markdown/winder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="src/assets/markdown/winder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="src/assets/markdown/winder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 1.4 Intended use
|
||||
|
||||
The QiTech Winder is designed for winding and rewinding filaments between 1 mm and 3 mm.
|
||||
The winder must be placed on a stable, level surface and secured if necessary. The test and work sequence is described in chapter 3.4 Work sequence.
|
||||
When using the winder, it is essential that the work steps described in the chapter "Work sequence" or in the operating instructions are followed.
|
||||
In the event of significant changes or deviations, it is necessary to carry out a new risk assessment in accordance with the Machinery Directive and to document the changes in the corresponding documents.
|
||||
|
||||
<br>
|
||||
|
||||
The machine is designed exclusively for use inside buildings. The machine must not be operated in potentially explosive atmospheres. Use in the area of food production is prohibited.
|
||||
Use of the machine for purposes other than those specified is prohibited.
|
||||
Other intended uses include cleaning and maintenance work as well as troubleshooting, fault rectification and repair.
|
||||
|
||||
<br><br>
|
||||
|
||||
# 1.5 Reasonably foreseeable misuse
|
||||
|
||||
Reasonably foreseeable misuse exists in particular when winding unsuitable filaments:
|
||||
|
||||
- Filaments that are difficult to bend
|
||||
- Easily tearable filaments
|
||||
- Filaments outside the permitted diameter range of 1mm - 3mm
|
||||
- Filaments with a surface temperature of > 50°C
|
||||
- Particularly flat, sharp-edged or pointed filaments
|
||||
|
||||
In addition, the following procedures must be regarded as misuse:
|
||||
|
||||
- Operating the winder on an uneven surface or in a position other than the intended horizontal position or on an unbraked trolley.
|
||||
- The use of unauthorized coil sizes.
|
||||
- Use of spare parts and components not authorized by the manufacturer.
|
||||
- Operating the winder without supervision or in ghost shifts.
|
||||
|
||||
<br><br><br>
|
||||
|
||||

|
||||
|
||||
<br><br><br>
|
||||
|
||||
Furthermore, there is always the possibility that existing/provided safety guards are dismantled or the existing sensors are manipulated or triggered by means other than the intended components.
|
||||
|
||||
The misuse mentioned must be explicitly prohibited with reference to these operating instructions and by written operating instructions, and the use of the winder must also be explicitly restricted in the operating instructions to the intended use mentioned above.
|
||||
|
||||
Maintenance and repair of the machine may only be carried out by the manufacturer's employees, trained skilled workers or semi-skilled and trained personnel.
|
||||
|
||||
<br>
|
||||
|
||||
# 1.6 Significant modification / other use
|
||||
|
||||
Any use of the machine other than the intended use is prohibited and is considered improper use. Claims of any kind against the manufacturer or its authorized representatives due to damage resulting from improper use of the machine are excluded. The operator alone is liable for all damage caused by improper use.
|
||||
|
||||
<br>
|
||||
|
||||
# 1.7 Description of the safety devices
|
||||
|
||||
**Emergency stop devices**
|
||||
|
||||
The emergency stop switch is used to quickly stop the machine in the event of danger. It can be reached from all positions in front of the machine. When an emergency stop device is actuated, the entire machine is stopped by disconnecting the mains supply.
|
||||
|
||||
Hazardous movements are brought to a controlled standstill as quickly as possible. The machine can only be restarted by physically resetting the emergency stop switch and then starting one of the operating modes in the software.
|
||||
|
||||
<br>
|
||||
|
||||
<img src="src/assets/markdown/winder/emergency_stop.jpeg" width="300" height="300"/>
|
||||
|
||||
<br>
|
||||
|
||||
**Overheating protection**
|
||||
|
||||
If one of the motor drivers becomes too hot, it switches off automatically and sends an error message to the control system. This then stops all other components of the machine.
|
||||
|
||||
<br>
|
||||
|
||||
**Spool stop in the event of filament breakage**
|
||||
|
||||
If the filament tears during winding, the spool stops automatically after a short time.
|
||||
|
||||
<br>
|
||||
|
||||
**Separating protective devices:**
|
||||
|
||||
The housing clad with aluminum-dibond plates prevents contact with hot motors, interference with the drive spindle of the traverse and manipulation of the electronics. It also prevents electrostatic charges from jumping from the filament to the electronics.
|
||||
|
||||
If guards are opened or removed for maintenance work, they must be closed or refitted before the machine is switched on again. It is not permitted to operate the machine without all properly installed and operational protective devices.
|
||||
|
||||
<br><br>
|
||||
|
||||
# 1.8 Instruction and Training
|
||||
|
||||
As the contractor/operator, you are obliged to inform and instruct the operating personnel about existing legal and accident prevention regulations as well as about existing safety equipment on the winder. This obligation also extends to safety equipment installed around the winder. The various technical qualifications of the employees must be taken into account.
|
||||
|
||||
The operating personnel must have understood the instruction and it must be ensured that the instruction is observed. This is the only way to ensure that your personnel work in a safety and hazard-conscious manner. Compliance with the contents of the instruction should be checked regularly.
|
||||
|
||||
As the owner/operator, you must therefore obtain written confirmation from each employee that they have attended an instruction session.
|
||||
|
||||
<br><br>
|
||||
|
||||
# 1.9 Noise emissions from the machine during activities
|
||||
|
||||
The noise emissions of the machine were measured on the same machine types. Under normal operating conditions, the maximum noise emission of the extruder is below the maximum permitted values.
|
||||
|
||||
Depending on the activity or the duration of the activity, the operator must determine whether suitable hearing protection is required as part of a risk assessment.
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 2 Life phases of the machine
|
||||
|
||||
<br>
|
||||
|
||||
# 2.1 Transport and Assembly
|
||||
|
||||
**Requirements for the installation site**
|
||||
|
||||
- Installation only in industrial and commercial indoor areas.
|
||||
- The surface must be level and solid.
|
||||
- The location must be as fire-free as possible and equipped with a fire extinguisher.
|
||||
|
||||
<br><br>
|
||||
| Picture | Danger |
|
||||
|---------|--------|
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves. |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to incorrect installation and commissioning!** <br> Errors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|
||||
<br><br>
|
||||
|
||||
# 2.2 Assembly, installation and Commissioning
|
||||
|
||||
**Basic installation**
|
||||
|
||||
The Winder is supplied with a mobile container, the parking brakes must be applied.
|
||||
|
||||
The cables must be routed in such a way that they cannot be tripped over.
|
||||
|
||||
<br>
|
||||
|
||||
**Updating the QiTech Control Software**
|
||||
|
||||
The machines are delivered with the newest version of the software installed. Updates, as well as previous updates and the software version can be viewed under Setup-> Updates. If a new version of the software is available, it will be displayed here. To perform the update an internet connection is required. In the next step, click the red "Select" button next to the chosen update. The display will now shut down, and when it is turned back on, the latest version of the software will be installed.
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**Safety Measures**
|
||||
|
||||
Some important safety measures must be observed to ensure safe operation of the QiTech Winder:
|
||||
|
||||
- It is important to only use original fuses with the prescribed current ratings to avoid overloading.
|
||||
- If inspection, maintenance and repair work is carried out on electrical components, these must be de-energized beforehand. Equipment that has been disconnected must be secured against unintentional or automatic reconnection. First check that there is no voltage and insulate neighboring live components.
|
||||
- Care must be taken to ensure that design features are not modified in such a way as to reduce safety.
|
||||
- If it is necessary to work on live components, an additional person should be called in to operate the emergency stop button or the main switch in an emergency.
|
||||
- Only voltage-insulated tools and appropriate clothing may be used.
|
||||
- Proper earthing of the electrical system must be ensured by protective conductor systems.
|
||||
- Cables must be checked regularly for damage and replaced if necessary.
|
||||
|
||||
<br>
|
||||
|
||||
# 2.3 Operation and control (work sequence)
|
||||
|
||||
A visual inspection must be carried out every working day before the machine is started.
|
||||
|
||||
Then start the machine as follows:
|
||||
|
||||
**1. Switch on the machine by plugging in the 220V power supply**
|
||||
|
||||
- Make sure that the emergency Stop button is inactive
|
||||
- Turn on the display by pressing the button on the right side of the display above the USB connectors
|
||||
- Winder then appears in the left column
|
||||
|
||||
**2. Mount the spool on the spool axle**
|
||||
|
||||
- Remove the quick-release fastener
|
||||
- Move the driver in the groove to the height of the hole in the spool flank
|
||||
- Slide the spool over the axle and the driver
|
||||
- Tighten the quick-release fastener again
|
||||
|
||||
**3. Calibrating the spool sides**
|
||||
|
||||
- Select "Control" in the function bar of the software
|
||||
- And go to the “Hold” mode
|
||||
- Move the "Outer Limit" slider to the approximate position of the outside of the bobbin
|
||||
- Click on "save"
|
||||
- Using the laser dot, repeat the previous steps and fine-tune until the dot is exactly on the edge of the inner bobbin flank
|
||||
- Repeat the same process for the other outer spool flank
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**4. Start pulling**
|
||||
|
||||
- Select “Control” in the function bar
|
||||
- Press the "Pull" button
|
||||
- Thread the filament through the puller rollers (be careful, counter-rotating rollers!)
|
||||
- For extrusion: Use the "Puller, Target Speed" slider to manually adjust the speed until the filament has the desired diameter
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**5. Start winding**
|
||||
|
||||
- Rotate the load arm upwards and place it on the puller
|
||||
- Press the "Wind" button
|
||||
- Thread the filament coming out of the puller through the feeder into the spool and fix it there
|
||||
- Place the load arm on the filament strand from above
|
||||
|
||||
<br>
|
||||
|
||||
**6. Change the spool**
|
||||
|
||||
- Press the "Pull" button
|
||||
- Remove the spool as described above and insert a new spool
|
||||
- Continue winding by clicking on "Wind" and repeat the winding process as mentioned above
|
||||
|
||||
<br>
|
||||
|
||||
**7. Stopping the winding process**
|
||||
|
||||
- Press the "Hold" button - motors energized but not in motion
|
||||
- Select "Standby" mode - no current on the motors
|
||||
- If the filament gets caught, the spool does not spin or there is a danger to persons, press the emergency stop button
|
||||
|
||||
<br>
|
||||
|
||||
**8. Switch off the machine**
|
||||
|
||||
- switch off the machine by disconnecting the power supply cable
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
**Unwinding filament**
|
||||
|
||||
- Mount the filament roll to be unwound onto the spool axle
|
||||
- Select “Config” in the function bar
|
||||
- Press the “Reverse” button to change the mode
|
||||
- Go back to “Control” in the function bar and adjust the “Puller Target Speed” as normal
|
||||
- Feed the filament through the puller rollers in the opposite direction to winding (caution, counter-rotating rollers!)
|
||||
|
||||
<br>
|
||||
|
||||
**Changing process variables during operation**
|
||||
|
||||
All settings such as the position of the spool sides, the winding speed or the filament diameter can also be adjusted during the winding process without interrupting it. To do this, change the value in the relevant field and click the "Wind" button again to apply it.
|
||||
|
||||
<br>
|
||||
|
||||
**Rewinding filament**
|
||||
|
||||
- Mount the full spool in front of the Puller wheels, so that it can spin freely
|
||||
- Mount the empty bobbin on the bobbin axle
|
||||
- Calibrate the Traverse the same way as mentioned above
|
||||
- Guide the filament from the full spool over the feeder and fix it to the spool
|
||||
- Click on “Control" in the function bar
|
||||
- Click on “Wind”
|
||||
- Adjust the pulling speed using the slider.
|
||||
|
||||
<br>
|
||||
|
||||
| Picture | Danger! |
|
||||
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to operation of the machine in faulty condition** <br> If safety-relevant changes or defects occur on the machine, shut down immediately, secure against restarting and report the process to the responsible office/person! |
|
||||
|
||||
<br><br>
|
||||
|
||||
## 2.4 Maintenance, inspection, Testing
|
||||
|
||||
Maintenance and servicing work on the machine may only be carried out by reliable, trained personnel. The minimum age required by law in the country of use must be observed!
|
||||
|
||||
Personnel to be trained, instructed or undergoing general training may only work on the Winder under the constant supervision of an experienced person!
|
||||
|
||||
<br>
|
||||
|
||||
**Updating the Winder software**
|
||||
|
||||
There are regular updates for the "QiTech Control" software, which may contain bug fixes as well as new functions or security updates. It is therefore recommended that you regularly check if new Updates are available and install them as described in section 2.2.
|
||||
|
||||
<br>
|
||||
|
||||
**Lubricating the traverse spindle**
|
||||
|
||||
The threaded spindle must be re-oiled after approx. 20 operating hours, but at the latest when the traverser no longer moves to the desired position because the motor is spinning.
|
||||
|
||||
To do this, loosen the plate on the top of the housing with an M5 Allen key and drizzle commercially available lubricating oil or grease, e.g. WD40, over the entire length of the threaded spindle.
|
||||
|
||||
<br>
|
||||
|
||||
**Retightening screw connections**
|
||||
|
||||
After approx. 100 operating hours, the rear panel of the winder should be removed using an M5 Allen key so that the screw connections inside the housing can be retightened. The following should be checked:
|
||||
|
||||
- Grub screws of the motor couplings (puller, traverse, coil)
|
||||
- Fastening screws of the motors (puller, traverse, coil)
|
||||
- Fastening screws of the angle sensor of the load arm
|
||||
|
||||
All should be tightened at least hand-tight.
|
||||
|
||||
<br>
|
||||
|
||||
**Replacing the puller rollers**
|
||||
|
||||
The polyurethane coating of the puller rollers will inevitably wear out over time. As soon as you notice the filament slipping through the rollers, they should be replaced. Suitable spare parts are available from the dealer. To replace them, unfasten the screws marked red in the picture.
|
||||
|
||||
Now remove the black plastic caps and simply pull out the puller rollers. To install the new rollers back on, slide them on the axes, put the plastic cap back on and screw the screws.
|
||||
|
||||
<img src="src/assets/markdown/winder/swapping_out_rollers.jpeg" width="300" height="400"/>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
| Picture | Caution! |
|
||||
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | Observe the instructions for maintenance work when servicing the machine! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## 2.5 Troubleshooting and fault rectification
|
||||
|
||||
<br>
|
||||
|
||||
**Machine is not recognized by QiTech Control**
|
||||
|
||||
- Is the LAN-cable connected?
|
||||
- Is the latest version of QiTech Control installed?
|
||||
|
||||
<br>
|
||||
|
||||
**Filament tears off frequently**
|
||||
|
||||
- Was the filament sufficiently cooled before winding?
|
||||
- Is the filament within the permitted diameter range of 1mm - 3mm?
|
||||
|
||||
<br>
|
||||
|
||||
**Puller rollers are scored**
|
||||
|
||||
It is normal for the puller rollers to wear out over time. However, some options can be checked:
|
||||
|
||||
- Is the surface temperature < 50°C when the filament runs through the rollers?
|
||||
- Is the filament slipping through the rollers instead of being conveyed by them, causing more abrasion?
|
||||
- Is the tension of the filament too strong?
|
||||
|
||||
<br>
|
||||
|
||||
**Filament is not wound perfectly parallel**
|
||||
|
||||
- How large is the inner diameter of the spool on which the filament is wound? The closer the winding is to the traverse, the better the winding pattern will be.
|
||||
- Have the spool sides been calibrated as described?
|
||||
- Has the correct filament diameter been set?
|
||||
- Have you tried to fine-tune the two settings independently of the procedure described here?
|
||||
- Does the problem already occur in the first layer, or only afterwards? If the first layer is not perfectly wound (e.g. due to skewed threading), the filament in the subsequent layers always slips into the gaps in the winding below and "perfect winding" can no longer take place.
|
||||
|
||||
<br>
|
||||
|
||||
**Winder suddenly switches off**
|
||||
|
||||
One of the motor drivers has probably overheated. To prevent uncontrolled winding behavior, the controller therefore switches off the entire winder. It is advisable to wait approx. 5 minutes and try winding again. If the error occurs frequently, a lower ambient temperature (optionally housing temperature) must be ensured.
|
||||
|
||||
<br>
|
||||
|
||||
**Load adjustment does not work correctly at high or low speeds**
|
||||
|
||||
The sensor inside the housing has probably twisted and the load adjustment range has therefore shifted upwards or downwards. To rectify this, turn the angle sensor back and tighten its screws as described in "Tightening screw connections".
|
||||
|
||||
<br><br>
|
||||
|
||||
# 2.6 Disassembly, decommissioning and Disposal
|
||||
|
||||
When the machine has reached the end of its planned service life, it is recommended that the manufacturer be contacted for the purpose of taking it back free of charge. If this is not possible, the machine must be taken out of service permanently and then disposed of or scrapped properly.
|
||||
|
||||
To do this, first disconnect the entire power supply. All components must then be separated by material class to enable subsequent recycling or differentiated scrapping.
|
||||
|
||||
The machine consists largely of aluminum and plastic and can be easily separated by loosening the screw connections. Electrical scrap, plastic, rubber seals, lubricants and other auxiliary materials must be disposed of separately by an authorized specialist company.
|
||||
|
||||
| Picture | Disposal Instructions |
|
||||
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="src/assets/markdown/winder/trashcan.jpeg" width="150" height="150"/> | Appliances with this logo on the packaging or on the appliance must be disposed of separately. These devices must not be disposed of with normal household waste. You are responsible for ensuring that any electrical or electronic waste is disposed of at the appropriate locations, e.g., recycling centers. |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
# 3 Hazard warning and information on Residual risks
|
||||
|
||||
| Picture | Danger! |
|
||||
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The winder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="src/assets/markdown/winder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the winder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="src/assets/markdown/winder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="src/assets/markdown/winder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="src/assets/markdown/winder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="src/assets/markdown/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="src/assets/markdown/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
|
||||
# 4 Glossary and Definitions
|
||||
|
||||
**Specialized personnel**
|
||||
|
||||
Due to their specialist training, knowledge and experience as well as knowledge of the relevant regulations, skilled personnel are able to carry out the work assigned to them and to recognize and avoid potential hazards independently. It is assumed that specialized personnel have also been instructed (see definition of instructed person).
|
||||
|
||||
<br>
|
||||
|
||||
**Instructed person**
|
||||
|
||||
The instructed person has been instructed by the operator on the tasks assigned to them and the potential hazards associated with improper behavior. The correct execution of the activities is monitored by specialized personnel.
|
||||
|
||||
<br>
|
||||
|
||||
**Qualified electrician**
|
||||
|
||||
A qualified electrician is someone who is able to assess the work assigned to them and recognize potential hazards based on their technical training, knowledge and experience as well as knowledge of the relevant regulations.
|
||||
|
||||
<br>
|
||||
|
||||
**Operator**
|
||||
|
||||
The operator is the person who uses, adjusts and monitors the machine in day-to-day operation. He is characterized by experience in handling the machine as well as in-depth knowledge of its functions and how to proceed in the event of a fault.
|
||||
|
||||
<br>
|
||||
|
||||
**Employment ban**
|
||||
|
||||
The following persons may not work on the machine:
|
||||
|
||||
- Persons with skin diseases
|
||||
- Persons with chronic respiratory problems or a tendency to allergic reactions
|
||||
- Pregnant women and nursing mothers
|
||||
- Children under 18 years
|
||||
|
||||
<br>
|
||||
|
||||
| Picture | Danger! |
|
||||
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="src/assets/markdown/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury if insufficiently qualified!** <br> Improper handling can lead to considerable personal injury and damage to property. Special activities may only be carried out by the persons specified in the relevant sections of these instructions. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## Life phases of the machine / system
|
||||
|
||||
| Life phase of the machine/system | User/qualification |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| A Construction | Trained and instructed specialized personnel of the manufacturer |
|
||||
| B Transport | Trained and instructed specialist personnel of the manufacturer or forwarding agent/transporter |
|
||||
| C Installation | Trained and instructed specialist personnel of the manufacturer or service technician of the manufacturer |
|
||||
| D Automatic operation | Instructed personnel of the operator |
|
||||
| E Semi-automatic operation | |
|
||||
| F Setting/adjusting/restarting the system | |
|
||||
| G Testing | |
|
||||
| H Eliminating faults in the work process | Trained and instructed specialized personnel of the operator or service technicians of the manufacturer |
|
||||
| I Observation of production processes | Instructed personnel of the operator |
|
||||
| J Troubleshooting and fault rectification | |
|
||||
| K Cleaning, maintenance | |
|
||||
| L Maintenance | Trained and instructed specialized personnel of the operator or service technicians of the manufacturer |
|
||||
| M Decommissioning | Trained and instructed specialized personnel of the operator |
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 42 KiB |