Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45c7b24420 | ||
|
|
082d5dc952 | ||
|
|
55af1120bf | ||
|
|
35864b4de7 | ||
|
|
621cb1913f | ||
|
|
3e3e3f1c0b | ||
|
|
cdf0374020 | ||
|
|
ec406889f7 | ||
|
|
b731a3bc36 | ||
|
|
27e15820f7 | ||
|
|
bb307bb9d7 | ||
|
|
39b05b056f | ||
|
|
8449045efb | ||
|
|
8a3de40501 | ||
|
|
853d0a4478 | ||
|
|
d6dccc1b1a | ||
|
|
5f636a6f15 | ||
|
|
ac8a8c37ed | ||
|
|
03e057f6a9 | ||
|
|
fb195eb7d4 | ||
|
|
874766633a | ||
|
|
047b0fbe55 | ||
|
|
e3e9412578 | ||
|
|
92ead0e775 | ||
|
|
06a6c81b55 | ||
|
|
7b858251ea | ||
|
|
04700d690c | ||
|
|
e4d950ae9b | ||
|
|
7e03148d88 | ||
|
|
5562cf5a53 | ||
|
|
8c77ea41fd | ||
|
|
ef1a2c3ca8 | ||
|
|
66e30e2085 | ||
|
|
6ca15dbebc | ||
|
|
7e5d44c329 | ||
|
|
dc287d61cc | ||
|
|
9aed1193a3 | ||
|
|
5fae6b628f | ||
|
|
88c6195d6c | ||
|
|
af0b5deff0 | ||
|
|
743a589c1d | ||
|
|
ee7550d89f | ||
|
|
ec13f715aa | ||
|
|
2af746e7a4 | ||
|
|
3af3fb9d79 | ||
|
|
ff3103bec2 | ||
|
|
5eb3c39f84 | ||
|
|
2c715d6a8e | ||
|
|
fb12001721 | ||
|
|
ad94754fcf | ||
|
|
1c0e06d4e1 | ||
|
|
b919721c3a | ||
|
|
a428a2aa02 | ||
|
|
7b479c6021 | ||
|
|
17ac24c79d | ||
|
|
8c7a04b616 | ||
|
|
fe248c79ae | ||
|
|
038df09611 | ||
|
|
9e43d8d431 | ||
|
|
d364263d6e | ||
|
|
376e4a4ea2 | ||
|
|
0b30ae9a85 | ||
|
|
528952238d | ||
|
|
8c32ce2a2b | ||
|
|
579ef0d2e2 | ||
|
|
b6263e90d6 | ||
|
|
f67f098ff3 | ||
|
|
1a3c26b789 | ||
|
|
49f0c9f289 | ||
|
|
e60daed274 | ||
|
|
9a4d41bf48 | ||
|
|
9a9120398e | ||
|
|
dec342498c | ||
|
|
bc8cc5dd3d | ||
|
|
4a1f3d125d | ||
|
|
1d1eeb3d48 | ||
|
|
c395d4e172 | ||
|
|
5009c5ca75 | ||
|
|
50dbc46b8c | ||
|
|
774e7e2f8b | ||
|
|
270872fe81 | ||
|
|
a749f59186 | ||
|
|
bae1de3c54 | ||
|
|
7681dae32c | ||
|
|
2d98d369e1 | ||
|
|
310ac42a88 | ||
|
|
cbffeff52d | ||
|
|
182df9c569 | ||
|
|
153a719d64 | ||
|
|
84603d1594 | ||
|
|
17b2e3e297 | ||
|
|
fb1cf33c7f | ||
|
|
ae2d0fa8b1 | ||
|
|
ed06b63b77 | ||
|
|
9f0f45d19c | ||
|
|
a75c0ff08a | ||
|
|
205432c926 | ||
|
|
781783464b | ||
|
|
4fa2a1a221 | ||
|
|
b734468741 | ||
|
|
1ed4ae3d2c | ||
|
|
8095222cad | ||
|
|
d19af0923c | ||
|
|
08ce5a37bd | ||
|
|
9d5e5cc7d0 | ||
|
|
78c7667d96 | ||
|
|
bef3389874 | ||
|
|
985937c227 | ||
|
|
c82d7347e2 | ||
|
|
22be4b79f0 | ||
|
|
be84edf0fd | ||
|
|
ff3e787a03 | ||
|
|
093663c4e6 | ||
|
|
1b2d6df459 | ||
|
|
cd2e15cb29 | ||
|
|
06a2284145 | ||
|
|
f1571c0daa | ||
|
|
96e0fb83ef | ||
|
|
94cffbf717 | ||
|
|
38b2e907f9 | ||
|
|
7e5498751a | ||
|
|
eef9d63de2 | ||
|
|
d9d9b9d344 | ||
|
|
2ff3d8df56 | ||
|
|
462672bb6a | ||
|
|
84d539a4fa | ||
|
|
850b2d411d | ||
|
|
008efe6ba1 | ||
|
|
3bd248b9fe | ||
|
|
d73cfe7a02 | ||
|
|
6a6c09e283 | ||
|
|
1b8b56fd02 | ||
|
|
fef727983e | ||
|
|
40239e0463 | ||
|
|
9c579a9fd4 | ||
|
|
6d2279d44b | ||
|
|
a3a343bbb8 | ||
|
|
373d97d12d | ||
|
|
e1842fa9d5 | ||
|
|
4fd8ddef51 | ||
|
|
61b9ac8a39 | ||
|
|
728dd6eb26 | ||
|
|
742fd35d5f | ||
|
|
5e1126e896 | ||
|
|
e91719f902 | ||
|
|
0efa9bcb07 | ||
|
|
37d897d240 | ||
|
|
7488e1d97a | ||
|
|
bb169e3b33 | ||
|
|
3d735f8abd | ||
|
|
0a87e45901 | ||
|
|
6205189b14 | ||
|
|
9d7da4f0af | ||
|
|
241b24be43 | ||
|
|
aa77d12879 | ||
|
|
78f183a8e5 | ||
|
|
9f6fc8184e | ||
|
|
ec3bdcb1df | ||
|
|
30292650bc | ||
|
|
1e9bcf9b19 | ||
|
|
1e88979ced | ||
|
|
a9a973e336 | ||
|
|
1112fbaae2 | ||
|
|
1abe0bbbdb | ||
|
|
3644d16cb5 | ||
|
|
cbd82ee7c3 | ||
|
|
b66a59dba9 | ||
|
|
9dbc962dbc | ||
|
|
93e2558803 | ||
|
|
8cf7480833 | ||
|
|
08d655effc | ||
|
|
c1c867fca1 | ||
|
|
1378c00e1a | ||
|
|
85f17befbe | ||
|
|
f2e764f93a | ||
|
|
9adeeda490 | ||
|
|
68bf075172 | ||
|
|
7d23190939 | ||
|
|
5c68e2c78a | ||
|
|
980e14a96e | ||
|
|
d5fb45e4e3 | ||
|
|
cdb7ab1e0c | ||
|
|
da934e0bb3 | ||
|
|
06ea89d500 | ||
|
|
0868a90513 | ||
|
|
39fb295b02 | ||
|
|
85dbae7458 | ||
|
|
6ce2174e25 | ||
|
|
3ccaa1cd26 | ||
|
|
72fec6dc9e | ||
|
|
547e4ac2bd | ||
|
|
5fbdf7b9ab | ||
|
|
85bd4b0d96 | ||
|
|
dfce760521 | ||
|
|
30c97bf9aa | ||
|
|
2c8373adcf | ||
|
|
d08972254c | ||
|
|
a76d39e0cd | ||
|
|
a74d0dbb80 | ||
|
|
17ab24739d | ||
|
|
09f0ea3454 | ||
|
|
0dd2494ea0 | ||
|
|
502c6d010e | ||
|
|
57c1cda229 | ||
|
|
8c2b20b148 | ||
|
|
12e7f245ce | ||
|
|
671e36fa0f | ||
|
|
1ffd7eade6 | ||
|
|
703bbfc9e6 | ||
|
|
057afb0b2e | ||
|
|
94bd29c734 | ||
|
|
d078b7b143 | ||
|
|
acbeb98f4f | ||
|
|
cab8bf062d | ||
|
|
9e04a7fa33 | ||
|
|
c92d4847ca | ||
|
|
aceeaa08c2 | ||
|
|
35946d0c88 | ||
|
|
e864a91caa | ||
|
|
a5b9a0b1d5 | ||
|
|
412d625138 | ||
|
|
1ec20074e9 | ||
|
|
4d796f8422 | ||
|
|
1406d6c724 | ||
|
|
33441b95a5 | ||
|
|
8a3f312675 | ||
|
|
5c327428d6 | ||
|
|
4e4d31cad1 | ||
|
|
031fdf25a9 | ||
|
|
857cda1455 | ||
|
|
de5f3a5cfc | ||
|
|
2da7af522c | ||
|
|
712284e2f0 | ||
|
|
402c47a81b | ||
|
|
54a000a280 | ||
|
|
41fe118144 | ||
|
|
aaa6ff604f | ||
|
|
a350bb378f | ||
|
|
aa2212820e | ||
|
|
578e70119d | ||
|
|
e8796e00af | ||
|
|
735f27770a | ||
|
|
700a255157 | ||
|
|
ec06275c08 | ||
|
|
11d0c8bb53 | ||
|
|
535f24f1d1 | ||
|
|
172913d8ec | ||
|
|
0f99c54587 | ||
|
|
f89d70c5f6 | ||
|
|
0fc070be69 | ||
|
|
b93eaf530e | ||
|
|
76d8504651 | ||
|
|
06acd43ff9 | ||
|
|
870090c950 | ||
|
|
65777f10ab | ||
|
|
67d145de56 | ||
|
|
d45d1ed01a | ||
|
|
0d6c7bc0a5 | ||
|
|
15f013f710 | ||
|
|
a32e26a041 | ||
|
|
41c7e1f74e | ||
|
|
5a856e6e2b | ||
|
|
8ca16d447e | ||
|
|
b99549895b | ||
|
|
07cbaa4518 | ||
|
|
c78f5ff5a0 | ||
|
|
33946a6fa3 | ||
|
|
02dc67bb85 | ||
|
|
b5ae86b19c | ||
|
|
35567b967f | ||
|
|
c45718e670 | ||
|
|
74dcd027e0 | ||
|
|
c793d3fa50 | ||
|
|
04fdc8a9d1 | ||
|
|
fd702ff0bc | ||
|
|
f25bf71753 | ||
|
|
4c33873095 | ||
|
|
8dbcb5efc3 | ||
|
|
36426aafe0 | ||
|
|
21cf9314be | ||
|
|
4f2af7b90d | ||
|
|
06795dcf78 | ||
|
|
1241d0a4a7 | ||
|
|
f36723f435 | ||
|
|
e96854a6bc | ||
|
|
04c5a673da | ||
|
|
40e17ef5f1 | ||
|
|
39200aac12 | ||
|
|
43a405eec4 | ||
|
|
0206a4ddca | ||
|
|
34da7178f8 | ||
|
|
2726c47280 | ||
|
|
63cf25b3d6 | ||
|
|
eae13207e4 | ||
|
|
f65d100cd5 | ||
|
|
13e4ea1ac7 | ||
|
|
49788246c8 | ||
|
|
d4e849d588 | ||
|
|
2689d19928 | ||
|
|
968e9327f6 | ||
|
|
3cbb74b3b0 | ||
|
|
50fcfd6e30 | ||
|
|
ce0f27787c | ||
|
|
08ab7fd404 | ||
|
|
f138b61a45 | ||
|
|
0ad0a58d46 | ||
|
|
2d2415a801 | ||
|
|
b4cce1dba4 | ||
|
|
f80cdb05d5 | ||
|
|
f24f31259b | ||
|
|
3d13e6cee2 | ||
|
|
86915c2987 | ||
|
|
6411f9ff80 | ||
|
|
ada8d0f12c | ||
|
|
735456b6eb | ||
|
|
b46a528adb | ||
|
|
80f2343a60 | ||
|
|
e030fbe23e | ||
|
|
1ef0cb608c | ||
|
|
789a84e511 | ||
|
|
28620b6a23 | ||
|
|
01609513fb | ||
|
|
379a4ec417 | ||
|
|
8b462d5a1d | ||
|
|
d8a0039db3 | ||
|
|
dec8ea481c | ||
|
|
0c8b8388d0 | ||
|
|
d3a56710d6 | ||
|
|
f9f67d3309 | ||
|
|
66122ad957 | ||
|
|
95feaf46bf | ||
|
|
88f14e0329 | ||
|
|
5bf19af844 | ||
|
|
19e2a337cf | ||
|
|
0fd47cfcb1 | ||
|
|
e0548d9a65 | ||
|
|
259ed30dd5 | ||
|
|
2e028f0c0f | ||
|
|
1328c6ce03 | ||
|
|
28d898e780 | ||
|
|
daabb170f5 | ||
|
|
adc06623cd | ||
|
|
c4803e63dd | ||
|
|
0022d86035 | ||
|
|
172ad0ab28 | ||
|
|
bfabb5aa13 | ||
|
|
94d5a844cf | ||
|
|
75c528a430 | ||
|
|
36045455db | ||
|
|
bb12f96723 | ||
|
|
8151c99944 | ||
|
|
6408010aff | ||
|
|
c96c96217a | ||
|
|
93d179a81d | ||
|
|
0f6f2240fc | ||
|
|
74505c6c22 | ||
|
|
3dfb879101 | ||
|
|
dc711a820f | ||
|
|
44d4c650cc | ||
|
|
3121679205 | ||
|
|
9fe5e582b0 | ||
|
|
3e6b838757 | ||
|
|
59cf2cea2a | ||
|
|
3d7bcfecca | ||
|
|
da69ad1c3e | ||
|
|
6ff434fb47 | ||
|
|
a63c57a0b9 | ||
|
|
21c43adcb4 | ||
|
|
afa23666e2 | ||
|
|
245ad64426 | ||
|
|
9665971150 | ||
|
|
3b056c8f26 | ||
|
|
62b35e0237 | ||
|
|
11bc62f40e | ||
|
|
116c76d218 | ||
|
|
4dcedb537d | ||
|
|
09022c9c86 | ||
|
|
80cf98b11d | ||
|
|
644e750353 | ||
|
|
aecabfde43 | ||
|
|
686678a1fe | ||
|
|
7ac0b788c2 | ||
|
|
3791ee785d | ||
|
|
7341dee976 | ||
|
|
63a472a1ba | ||
|
|
df6b9e72c0 | ||
|
|
6ad4b9c8bb | ||
|
|
4fcc8dfe12 | ||
|
|
1da64c9cea | ||
|
|
9b6912be8d | ||
|
|
3beed9478c | ||
|
|
3715728fc6 | ||
|
|
5beee78cad | ||
|
|
32fe774e62 | ||
|
|
dead77b49c | ||
|
|
d7aa976eb1 | ||
|
|
6f9e89d616 | ||
|
|
bb3f6c0d5a | ||
|
|
620974ac59 | ||
|
|
6c78ff95e4 | ||
|
|
a814236bfa | ||
|
|
2bee030410 | ||
|
|
285ad59b08 | ||
|
|
f8b861cb39 | ||
|
|
f6a3aa6458 | ||
|
|
dd5f3e8e73 | ||
|
|
f50e32af02 | ||
|
|
345e083edd | ||
|
|
5660ddd05f | ||
|
|
c4360850af | ||
|
|
ce2123c88c | ||
|
|
520f33bf29 | ||
|
|
c620f1db7f | ||
|
|
d352e88d6f | ||
|
|
cc009b31f3 | ||
|
|
b5c189ab49 | ||
|
|
64f57277f2 | ||
|
|
af8ee60fe8 | ||
|
|
beb2e3cc2d | ||
|
|
0e2bcfd7bc | ||
|
|
417ffb23c2 | ||
|
|
a57d4cdf36 | ||
|
|
b8168c6b43 | ||
|
|
6026d2a658 | ||
|
|
a301f3e228 | ||
|
|
0b6bd68a24 | ||
|
|
e7fd390263 | ||
|
|
eab675795a | ||
|
|
8c2dc9a507 | ||
|
|
f6c0744efa | ||
|
|
8c7e810359 | ||
|
|
f0fd9e8ce6 | ||
|
|
c5579a4471 | ||
|
|
de7a65bf12 | ||
|
|
e4d0b3c5be | ||
|
|
dad029ab0d | ||
|
|
9c780799d1 | ||
|
|
27a33f9def | ||
|
|
f44cd029f9 | ||
|
|
9abb7629fb | ||
|
|
a161d85a63 | ||
|
|
9df9e6df4a | ||
|
|
29d465c13e | ||
|
|
622c94f2d9 | ||
|
|
f31a2789ca | ||
|
|
e7632e8d1b | ||
|
|
2f4f3de32a | ||
|
|
c9f49198c7 | ||
|
|
e2c12a0168 | ||
|
|
cb6e066512 | ||
|
|
1dd0421f96 | ||
|
|
64855f2e7a | ||
|
|
855ae7b3cf | ||
|
|
5f9a8605c9 | ||
|
|
567730a8d4 | ||
|
|
6b2164b88b | ||
|
|
930ea10409 | ||
|
|
24b703e262 | ||
|
|
152dd3efa0 | ||
|
|
ebc5bae55f | ||
|
|
88214c46a8 | ||
|
|
c9485a92e9 | ||
|
|
9df42c4d13 | ||
|
|
fc9a439ae8 | ||
|
|
e451153406 | ||
|
|
500e2fe481 | ||
|
|
a5884d93e7 | ||
|
|
d2e32cdd78 | ||
|
|
f66c6d4f65 | ||
|
|
fb85cc6db7 | ||
|
|
4e128ded36 | ||
|
|
f50388b141 | ||
|
|
4025a68fe1 | ||
|
|
7a05fa206f | ||
|
|
8594b06c3b | ||
|
|
17523574d8 | ||
|
|
7971797178 | ||
|
|
a8b0837e58 | ||
|
|
e419974844 | ||
|
|
773d0f6c15 | ||
|
|
afc4e0d8b4 | ||
|
|
5e86988ac4 | ||
|
|
2d28c18b1a | ||
|
|
b19764281a | ||
|
|
c25d63f781 | ||
|
|
d79efc412f | ||
|
|
7ad74cf089 | ||
|
|
6073d37892 | ||
|
|
c04937553f | ||
|
|
e08bdfa3c7 | ||
|
|
d282678b19 |
No files matched your search
@@ -1,3 +0,0 @@
|
||||
.png filter=lfs diff=lfs merge=lfs -text
|
||||
.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'BUG: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Navigate to '...'
|
||||
2. Select/Press '....'
|
||||
3. Execute operation '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots/Photos**
|
||||
If applicable, add screenshots from the HMI display or photos of the machine screen to help explain your problem.
|
||||
|
||||
**Machine Information (please complete the following):**
|
||||
- Machine Type: [Winder2, Laser, Aquapath...]
|
||||
- Control Software Version: [e.g. 2.4.1]
|
||||
- Settings: [e.g. Different Gear Ratios, PID Settings, …]
|
||||
|
||||
**Device Information (if issue occurs on external device):**
|
||||
- Device Type: [e.g. Tablet, PLC, Remote Control Panel]
|
||||
- OS: [e.g. Windows 10, Linux, etc.]
|
||||
- Connection Type: [e.g. Ethernet, USB]
|
||||
|
||||
**Error Messages/Codes**
|
||||
If applicable, provide exact error codes or messages displayed or found in the troubleshooting terminal.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here (e.g., recent software updates, environmental factors).
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: 'Feat: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. During operation, I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions, workarounds, or features you've considered.
|
||||
|
||||
**Use Case/Application:**
|
||||
- Machine Type(s): [e.g. All machines, Winder, Aquapath, ...]
|
||||
- Frequency of Use: [e.g. Daily, During setup, Occasionally]
|
||||
|
||||
**Expected Benefits:**
|
||||
- Efficiency Improvement: [e.g. Reduces setup time by X minutes, Eliminates manual steps]
|
||||
- Safety Enhancement: [e.g. Prevents operator error, Adds safety checks]
|
||||
- Production Impact: [e.g. Increases throughput, Reduces downtime, Improves quality]
|
||||
- User Experience: [e.g. Simplifies workflow, Reduces training time]
|
||||
|
||||
**Mockups/Sketches**
|
||||
If applicable, add sketches, mockups, or photos showing how the feature would appear on the HMI display or integrate with the machine.
|
||||
|
||||
**Additional context**
|
||||
Add any other context, industry standards, or examples from other control systems about the feature request here.
|
||||
@@ -0,0 +1,14 @@
|
||||
## What does this PR do / add / change?
|
||||
|
||||
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
|
||||
|
||||
## Checklist before opening the pr
|
||||
|
||||
- [ ] Tested locally
|
||||
- [ ] Tested on the machine (if hardware interaction is involved)
|
||||
- [ ] No format errors (`cargo fmt` / `npm run format`)
|
||||
- [ ] No build errors (`cargo build` / `npm run build`)
|
||||
@@ -3,50 +3,76 @@ name: Electron
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
jobs:
|
||||
build-electron:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
- &checkout
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &setup-node
|
||||
name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18" # or '20' for latest LTS
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
electron/package-lock.json
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- &install
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
node_modules/
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Install
|
||||
run: npm i
|
||||
|
||||
- name: Build
|
||||
- name: Compile
|
||||
run: tsc
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Package
|
||||
run: npm run package
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
@@ -0,0 +1,213 @@
|
||||
name: Stale Assigned Issue Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Sunday at 9:00 AM UTC
|
||||
- cron: '0 9 * * 0'
|
||||
workflow_dispatch: # Allow manual triggering for testing
|
||||
|
||||
jobs:
|
||||
check-stale-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Check assigned issues for status updates
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
|
||||
// Helper function to check rate limits
|
||||
async function checkRateLimit() {
|
||||
try {
|
||||
const { data: rateLimit } = await github.rest.rateLimit.get();
|
||||
const remaining = rateLimit.resources.core.remaining;
|
||||
const reset = new Date(rateLimit.resources.core.reset * 1000);
|
||||
console.log(`Rate limit: ${remaining} requests remaining (resets at ${reset.toISOString()})`);
|
||||
return remaining > 10; // Stop if fewer than 10 requests remaining
|
||||
} catch (error) {
|
||||
console.log(`Could not check rate limit: ${error.message}`);
|
||||
return true; // Continue on error
|
||||
}
|
||||
}
|
||||
|
||||
// Get all open issues
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
console.log(`Found ${issues.length} open issues`);
|
||||
|
||||
// Helper function to check if issue has linked PRs using GraphQL
|
||||
async function hasLinkedPRs(issueNumber) {
|
||||
try {
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!, $issueNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $issueNumber) {
|
||||
timelineItems(first: 100, itemTypes: CONNECTED_EVENT) {
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
subject {
|
||||
... on PullRequest {
|
||||
number
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await github.graphql(query, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issueNumber: issueNumber
|
||||
});
|
||||
|
||||
const linkedPRs = result.repository.issue.timelineItems.nodes
|
||||
.filter(node => node.subject && node.subject.state === 'OPEN')
|
||||
.map(node => node.subject.number);
|
||||
|
||||
return linkedPRs.length > 0;
|
||||
} catch (error) {
|
||||
console.log(`Could not check linked PRs for issue #${issueNumber}: ${error.message}`);
|
||||
return false; // Assume no linked PRs on error to avoid false positives
|
||||
}
|
||||
}
|
||||
|
||||
let processedCount = 0;
|
||||
let commentedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const issue of issues) {
|
||||
try {
|
||||
// Check rate limit periodically
|
||||
if (processedCount % 10 === 0 && !(await checkRateLimit())) {
|
||||
console.log('Rate limit nearly exhausted, stopping to avoid hitting limit');
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip pull requests (they appear in issues list too)
|
||||
if (issue.pull_request) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not assigned
|
||||
if (!issue.assignees || issue.assignees.length === 0) {
|
||||
console.log(`Issue #${issue.number}: Not assigned, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Checking issue #${issue.number}: ${issue.title}`);
|
||||
processedCount++;
|
||||
|
||||
// Check if issue has linked PRs
|
||||
const hasOpenLinkedPR = await hasLinkedPRs(issue.number);
|
||||
|
||||
if (hasOpenLinkedPR) {
|
||||
console.log(`Issue #${issue.number}: Has linked open PR, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all comments on the issue
|
||||
let comments = [];
|
||||
try {
|
||||
comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100
|
||||
});
|
||||
} catch (commentError) {
|
||||
console.error(`Issue #${issue.number}: Could not fetch comments - ${commentError.message}`);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the most recent comment or update
|
||||
let lastActivity = new Date(issue.created_at);
|
||||
|
||||
if (issue.updated_at) {
|
||||
lastActivity = new Date(issue.updated_at);
|
||||
}
|
||||
|
||||
if (comments.length > 0) {
|
||||
const lastComment = comments[comments.length - 1];
|
||||
const lastCommentDate = new Date(lastComment.created_at);
|
||||
if (lastCommentDate > lastActivity) {
|
||||
lastActivity = lastCommentDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if last activity was more than a week ago
|
||||
const daysSinceActivity = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysSinceActivity >= 7) {
|
||||
console.log(`Issue #${issue.number}: No activity for ${daysSinceActivity} days`);
|
||||
|
||||
// Check if we already posted a status request in the last 7 days
|
||||
const recentComments = comments.filter(comment => {
|
||||
const commentDate = new Date(comment.created_at);
|
||||
return (now - commentDate) < ONE_WEEK_MS;
|
||||
});
|
||||
|
||||
const alreadyAsked = recentComments.some(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('status update')
|
||||
);
|
||||
|
||||
if (alreadyAsked) {
|
||||
console.log(`Issue #${issue.number}: Already requested status this week, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Post a status request comment
|
||||
try {
|
||||
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `Hi ${assigneeNames}! 👋\n\n` +
|
||||
`This issue has been assigned to you for ${daysSinceActivity} days with no recent activity or linked pull requests.\n\n` +
|
||||
`Could you please provide a status update? If you're:\n` +
|
||||
`- ✅ Working on it: Let us know your progress\n` +
|
||||
`- 🔄 Need help: Tag someone or describe what's blocking you\n` +
|
||||
`- 🚫 No longer working on it: Let us know so we can reassign\n\n` +
|
||||
`Thank you!`
|
||||
});
|
||||
|
||||
console.log(`Issue #${issue.number}: Posted status request comment`);
|
||||
commentedCount++;
|
||||
} catch (commentError) {
|
||||
console.error(`Issue #${issue.number}: Could not post comment - ${commentError.message}`);
|
||||
errorCount++;
|
||||
// Continue to next issue
|
||||
}
|
||||
} else {
|
||||
console.log(`Issue #${issue.number}: Activity ${daysSinceActivity} days ago, still fresh`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Issue #${issue.number}: Error processing - ${error.message}`);
|
||||
errorCount++;
|
||||
// Continue to next issue instead of failing the entire workflow
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nStale issue check complete!`);
|
||||
console.log(`Processed: ${processedCount} issues`);
|
||||
console.log(`Commented on: ${commentedCount} issues`);
|
||||
console.log(`Errors: ${errorCount}`);
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Nix CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
jobs:
|
||||
electron:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- &checkout
|
||||
name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &nix-install
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-electron-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './electron/package-lock.json') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
purge: true
|
||||
purge-prefixes: nix-electron-${{ runner.os }}-
|
||||
|
||||
- name: Build Electron in Nix
|
||||
run: nix build ".#packages.x86_64-linux.electron"
|
||||
|
||||
rust:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-server-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
purge: true
|
||||
purge-prefixes: nix-server-${{ runner.os }}-
|
||||
|
||||
- name: Build Server in Nix
|
||||
run: nix build ".#packages.x86_64-linux.server"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [electron, rust]
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Use both Electron and Server Nix Store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock', './electron/package-lock.json') }}
|
||||
|
||||
# if no hit on the primary key, merge individual caches
|
||||
restore-prefixes-all-matches: |
|
||||
nix-electron-${{ runner.os }}-
|
||||
nix-server-${{ runner.os }}-
|
||||
|
||||
- name: Build Flake and System Configuration
|
||||
run: nix build ".#nixosConfigurations.nixos.config.system.build.toplevel" --impure
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Format
|
||||
run: nix run 'nixpkgs#nixfmt-classic' $(find . nixos -name '*.nix')
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Build Flake and Configuration.nix
|
||||
run: nix flake check "." --impure
|
||||
@@ -3,27 +3,41 @@ name: Rust
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches: "*"
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_VERSION: "1.86"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install libudev development package
|
||||
- &udev-install
|
||||
name: Install libudev development package
|
||||
run: sudo apt-get update && sudo apt-get install -y libudev-dev
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set Rust version
|
||||
run: rustup override set ${{ env.RUST_VERSION }}
|
||||
- name: Install Formatter
|
||||
run: rustup component add rustfmt
|
||||
- name: Cache Rust dependencies
|
||||
|
||||
- &checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &setup-rust
|
||||
name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
profile: minimal
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- &cargo-cache
|
||||
name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
@@ -41,8 +55,36 @@ jobs:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
run: cargo build --verbose
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --check --verbose
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *udev-install
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
- *cargo-cache
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
mock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *udev-install
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
- *cargo-cache
|
||||
|
||||
- name: Mock Machine
|
||||
run: cargo build --features "development-build,mock-machine"
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Update npm deps hash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'electron/package-lock.json'
|
||||
- 'electron/package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
update-hash:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Calculate new npm deps hash
|
||||
id: calc-hash
|
||||
run: |
|
||||
# Calculate the hash using nix run with nixpkgs
|
||||
NEW_HASH=$(nix run 'nixpkgs#prefetch-npm-deps' electron/package-lock.json)
|
||||
echo "hash=$NEW_HASH" >> $GITHUB_OUTPUT
|
||||
echo "Calculated hash: $NEW_HASH"
|
||||
|
||||
- name: Update npmDepsHash in electron.nix
|
||||
run: |
|
||||
NEW_HASH="${{ steps.calc-hash.outputs.hash }}"
|
||||
sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$NEW_HASH\";|" nixos/packages/electron.nix
|
||||
|
||||
- name: Check if hash changed
|
||||
id: check-changes
|
||||
run: |
|
||||
if git diff --quiet nixos/packages/electron.nix; then
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes needed - hash is already correct"
|
||||
else
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "Hash updated in electron.nix"
|
||||
fi
|
||||
|
||||
- name: Commit and push if changed
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add nixos/packages/electron.nix
|
||||
git commit -m "chore: update npmDepsHash to ${{ steps.calc-hash.outputs.hash }}"
|
||||
git push
|
||||
|
||||
- name: Comment on PR
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Automatically updated `npmDepsHash` to `${{ steps.calc-hash.outputs.hash }}`\n\n**Next step:** Trigger the checks by pushing an empty commit:\n```bash\ngit commit --allow-empty -m "trigger ci" && git push\n```'
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
target/
|
||||
dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
shell.nix
|
||||
node_modules/
|
||||
runtime_metrics.csv
|
||||
compile_metrics.csv
|
||||
.idea/
|
||||
electron/dist-electron/
|
||||
@@ -1,3 +1,35 @@
|
||||
# `2.14.1`
|
||||
_26.01.2026_
|
||||
|
||||
## Laser
|
||||
-[#1079](https://github.com/qitechgmbh/control/pull/1079) Fixed Overflowing of buffer for Graphs causing crashes
|
||||
|
||||
# `2.14.0`
|
||||
_22.01.2026_
|
||||
|
||||
## General
|
||||
- [#1040](https://github.com/qitechgmbh/control/pull/1040) Added API for reading Machine Data
|
||||
|
||||
# `2.13.0`
|
||||
_07.01.2026_
|
||||
|
||||
## General
|
||||
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
|
||||
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
|
||||
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
|
||||
|
||||
## Extruder V3
|
||||
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
|
||||
|
||||
## Laser
|
||||
- [#924](https://github.com/qitechgmbh/control/pull/924) Roundness is now displayed as a percentage
|
||||
- [#885](https://github.com/qitechgmbh/control/pull/885) Added Min and Max Diameter
|
||||
- [#849](https://github.com/qitechgmbh/control/pull/849) Added a Warning if Filament is out of Spec
|
||||
|
||||
## Aquapath
|
||||
- [#976](https://github.com/qitechgmbh/control/pull/976) Improved Stability
|
||||
|
||||
|
||||
# `2.12.0`
|
||||
_17.11.2025_
|
||||
|
||||
|
||||
@@ -2,153 +2,76 @@
|
||||
|
||||
# QiTech Control
|
||||
|
||||
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
|
||||
QiTech Control is an open-source framework designed to bring modern software development practices to certified industrial hardware.
|
||||
|
||||
[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
|
||||
*Click here to watch a video demo of our software.*
|
||||
It frees developers from proprietary, license-heavy PLC ecosystems and rigid "point-and-click" workflows that are no longer adequate for today's complex automation challenges.
|
||||
|
||||
[](https://youtu.be/55egCAkQgyM)
|
||||
*Click here to watch a full explainer Video of our Software.*
|
||||
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
|
||||
|
||||
# Repo Structure
|
||||
## Documentation
|
||||
|
||||
Frontend
|
||||
**[View Full Documentation Wiki](https://github.com/qitechgmbh/control/wiki)**
|
||||
|
||||
- `/electron`: Frontend code for the control software built with React and Electron.
|
||||
- [Getting Started](https://github.com/qitechgmbh/control/wiki/Getting-Started)
|
||||
- [Architecture Overview](https://github.com/qitechgmbh/control/wiki/Architecture-Overview)
|
||||
- [EtherCAT Basics](https://github.com/qitechgmbh/control/wiki/Ethercat-Basics)
|
||||
- [Device Examples](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)
|
||||
- [REST API Reference](https://github.com/qitechgmbh/control/wiki/Rest-Api)
|
||||
|
||||
Backend
|
||||
## Videos
|
||||
|
||||
- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
|
||||
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
|
||||
- `/ethercat-hal-derive`: Macros for `ethercat-hal`
|
||||
- `/control-core`: Core control logic for the server.
|
||||
| Software Demo | Full Explainer |
|
||||
|---------------|----------------|
|
||||
| [](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Watch a demo of the control software in action* | [](https://youtu.be/55egCAkQgyM)<br/>*Watch a complete overview of QiTech Control* |
|
||||
|
||||
Operating System
|
||||
## Repository Structure
|
||||
|
||||
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
|
||||
- **`/electron`** - React + Electron frontend
|
||||
- **`/server`** - Rust backend implementing machine logic
|
||||
- **`/ethercat-hal`** - Hardware abstraction layer for EtherCAT devices
|
||||
- **`/control-core`** - Core control logic
|
||||
- **`/nixos`** - Custom Linux OS with realtime kernel
|
||||
|
||||
Other
|
||||
## Technology Stack
|
||||
|
||||
- `/docs`: Documentation for the project.
|
||||
**Backend:** Rust with [Ethercrab](https://github.com/ethercrab-rs/ethercrab) for EtherCAT, [SocketIO](https://socket.io/) for real-time communication, [axum](https://docs.rs/axum/latest/axum/) for REST API
|
||||
|
||||
# Technology Choices
|
||||
**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
|
||||
|
||||
## Backend
|
||||
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
## Hardware Examples
|
||||
|
||||
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
|
||||
QiTech Control supports a wide range of EtherCAT hardware from WAGO and Beckhoff. We provide complete step-by-step tutorials with wiring diagrams and software configuration for many common modules.
|
||||
|
||||
We use [Smol](https://github.com/smol-rs/smol) for EtherCAT IO in the control loop for it's performance and [Tokio](https://tokio.rs/) for server IO because of it's ecosystem and maturity.
|
||||
**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
|
||||
|
||||
## Frontend
|
||||
Quick links to popular examples:
|
||||
- [EL2004 - LED Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El2004) (simplest setup for beginners)
|
||||
- [EL3021 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El3021)
|
||||
- [EL7031 - Stepper Motor Control](https://github.com/qitechgmbh/control/wiki/Minimal-Example-El7031-Motor)
|
||||
- [WAGO 750-402 - Digital Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago750-402)
|
||||
- [WAGO 750-455 - Analog Input](https://github.com/qitechgmbh/control/wiki/Minimal-Example-Wago-750-455)
|
||||
|
||||
We could combine the code of the frontend and backend using [Doxius](https://dioxuslabs.com/) but it lacks good Linux support. We chose [Electron](https://www.electronjs.org/) with [React](https://react.dev/) for it's maturity and ecosystem. For the UI we use [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) for styling. For routing we use [TanStack Router](https://tanstack.com/router/v1).
|
||||
## QiTech Machines
|
||||
|
||||
# Dev Setup
|
||||
QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
|
||||
|
||||
[Developer Documentation](./docs/developer-docs/)
|
||||
**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
|
||||
|
||||
## Backend
|
||||
|
||||
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
|
||||
- `rust-analyzer` extension for VSCode
|
||||
- Set your interface in `server/src/ethercat/init.rs` like `en10`
|
||||
- Connect a Beckhoff EK1100 to your interface
|
||||
- run `cd server && cargo run` to start the server (localhost:3001)
|
||||
## Contributing
|
||||
|
||||
## Frontend
|
||||
This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
|
||||
|
||||
- nodejs and npm installed
|
||||
- run `cd electron && npm i && npm run start` to start the frontend
|
||||
|
||||
# Machines
|
||||
|
||||
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
|
||||
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
|
||||
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
|
||||
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
|
||||
## Additional Videos
|
||||
|
||||
# More Docs
|
||||
| Episode Highlights | |
|
||||
|--------------------|--------------------|
|
||||
| [](https://www.youtube.com/watch?v=U2wNN6sF4to&t=163s)<br/>**Day in the Software Team**<br/>*Behind the scenes with Robin and how the team ships control software.* | [](https://www.youtube.com/watch?v=R5WGAY3WRWA&t=1s)<br/>**Software Stack Day**<br/>*A fast tour through the tools, stack, and daily workflow.* |
|
||||
| [](https://www.youtube.com/watch?v=xyaHwxXguw4&t=32s)<br/>**First Client**<br/>*A milestone week building the first real client project.* | [](https://www.youtube.com/watch?v=moI7z2PVdF4)<br/>**Steelworks Week**<br/>*A deep-dive week on a demanding steelworks order.* |
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
## License
|
||||
|
||||
- [x] Example Winder V2
|
||||
|
||||
- [ ] Electron
|
||||
|
||||
- Folder Structure
|
||||
- Routing with TanStack Router
|
||||
- Design with Tailwind & Shadcn
|
||||
- ...
|
||||
|
||||
- [ ] Interfacing with Electron/Server
|
||||
|
||||
- [ ] SocketIO
|
||||
- Machine Namespace
|
||||
- Main Namespace
|
||||
- [ ] REST
|
||||
- Machine Mutations
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
- Maindevice
|
||||
- Group
|
||||
- Extracting Device Identifications
|
||||
- Identifying Groups
|
||||
- Validating Machines
|
||||
- Run Control Loop
|
||||
- [x] [Control Loop](./docs/control-loop.md)
|
||||
- [x] [Machine/Device Identification](./docs/identification.md)
|
||||
- [ ] Machines
|
||||
- When to create a new Machine?
|
||||
- Versioning
|
||||
- Code sharing
|
||||
- Creating/Validating a Machine
|
||||
- Validation
|
||||
- Configuration
|
||||
- [ ] Machine Implementation Guide
|
||||
- Link: How to create a Device
|
||||
- Link: How to create an Actor
|
||||
- Link: How to create a Machine
|
||||
- API (SocketIO + REST)
|
||||
- Creation/Validation Logic
|
||||
- Optional/Mandatory Devices
|
||||
- Validate Devices
|
||||
- Business Logic
|
||||
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
|
||||
- Forward `act` in winder.
|
||||
|
||||
- [ ] Control Core
|
||||
|
||||
- [x] [Actors](./docs/actors.md)
|
||||
- [ ] SocketIO
|
||||
- Namespaces & Caching
|
||||
- Joining leaving namespaces
|
||||
- NamespaceId
|
||||
- Caching
|
||||
- Serverside Caching
|
||||
- Clientside Caching
|
||||
- [ ] REST
|
||||
|
||||
- [x] Ethercat HAL
|
||||
|
||||
- [x] [Devices](./docs/devices.md)
|
||||
- [x] [Configuration (CoE)](./docs/coe.md)
|
||||
- [x] [IO](./docs/io.md)
|
||||
- [x] [PDO](./docs/pdo.md)
|
||||
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
See [LICENSE](LICENSE) file for details.
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_DIR="${BUILD_DIR:-target}"
|
||||
CSV_FILE="${CSV_FILE:-compile_metrics.csv}"
|
||||
DEFAULT_PKG="server"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- cargo build
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "cargo" ] && [ "${2:-}" = "build" ]; then
|
||||
cargo clean
|
||||
BUILD_CMD=(cargo rustc -p "$DEFAULT_PKG")
|
||||
shift 2
|
||||
BUILD_CMD+=("$@")
|
||||
BUILD_CMD+=(-- --emit=llvm-ir -C debuginfo=0)
|
||||
else
|
||||
BUILD_CMD=("$@")
|
||||
fi
|
||||
|
||||
compile_start_ns=$(date +%s%N)
|
||||
"${BUILD_CMD[@]}"
|
||||
compile_end_ns=$(date +%s%N)
|
||||
compile_ms=$(( (compile_end_ns - compile_start_ns) / 1000000 ))
|
||||
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
llvm_ir_lines=$(
|
||||
find "$BUILD_DIR" -type f -name '*.ll' -print0 \
|
||||
| xargs -0 cat \
|
||||
| wc -l
|
||||
)
|
||||
else
|
||||
llvm_ir_lines=0
|
||||
fi
|
||||
|
||||
bin_path=""
|
||||
|
||||
if [ -n "${BINARY_PATH:-}" ] && [ -f "$BINARY_PATH" ]; then
|
||||
bin_path="$BINARY_PATH"
|
||||
elif [ -f "./target/release/server" ]; then
|
||||
bin_path="./target/release/server"
|
||||
elif [ -f "./target/debug/server" ]; then
|
||||
bin_path="./target/debug/server"
|
||||
elif [ -f "./server/target/release/server" ]; then
|
||||
bin_path="./server/target/release/server"
|
||||
elif [ -f "./server/target/debug/server" ]; then
|
||||
bin_path="./server/target/debug/server"
|
||||
fi
|
||||
|
||||
if [ -n "$bin_path" ]; then
|
||||
binary_size_bytes=$(stat -c '%s' "$bin_path")
|
||||
else
|
||||
binary_size_bytes=0
|
||||
fi
|
||||
|
||||
finished_timestamp_ms=$(($(date +%s%N) / 1000000))
|
||||
finished_datetime=$(date -Iseconds)
|
||||
|
||||
if [ ! -f "$CSV_FILE" ]; then
|
||||
echo "datetime,finished_timestamp_ms,compile_ms,llvm_ir_lines,binary_size_bytes" > "$CSV_FILE"
|
||||
fi
|
||||
|
||||
echo "${finished_datetime},${finished_timestamp_ms},${compile_ms},${llvm_ir_lines},${binary_size_bytes}" \
|
||||
>> "$CSV_FILE"
|
||||
|
||||
echo "Metrics recorded in $CSV_FILE"
|
||||
@@ -9,11 +9,11 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
|
||||
axum = "0.8.6"
|
||||
axum = "0.8.8"
|
||||
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
units = { path = "../units" }
|
||||
ethercrab = "0.6"
|
||||
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
|
||||
interfaces = "0.0.9"
|
||||
serde = "1.0.219"
|
||||
serialport = "4.7.3"
|
||||
@@ -21,7 +21,7 @@ smol = "2.0.2"
|
||||
socketioxide = "0.17.2"
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
tracing = "0.1.41"
|
||||
tracing = "0.1.44"
|
||||
erased-serde = "0.4.8"
|
||||
core_affinity = "0.8.3"
|
||||
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use interfaces::Interface;
|
||||
|
||||
pub mod modbus_tcp_discovery;
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
Ok(reports_as_ethernet && !actually_wifi)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub fn get_interfaces() -> Result<Vec<Interface>> {
|
||||
let vec = Interface::get_all()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
|
||||
.into_iter()
|
||||
.filter(|iface| {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
|
||||
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::ethernet::get_interfaces;
|
||||
use crate::{futures::FutureIteratorExt, modbus::tcp::ModbusTcpDevice};
|
||||
use anyhow::{Result, bail};
|
||||
use interfaces::Interface;
|
||||
use std::cmp::min;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
pub struct ModbusTcpProbe {
|
||||
pub addr: SocketAddr,
|
||||
pub serial: u16,
|
||||
}
|
||||
|
||||
pub async fn probe_modbus_tcp() -> Vec<ModbusTcpProbe> {
|
||||
let interfaces = match get_interfaces() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
||||
interfaces
|
||||
.into_iter()
|
||||
.map(probe_modbus_tcp_addresses)
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn probe_modbus_tcp_addresses(interface: Interface) -> Vec<ModbusTcpProbe> {
|
||||
let out: Vec<ModbusTcpProbe> = interface
|
||||
.addresses
|
||||
.iter()
|
||||
.filter_map(|addr| {
|
||||
let a = addr.addr.map(|a| a.ip());
|
||||
let m = addr.mask.map(|a| a.ip());
|
||||
|
||||
match (a, m) {
|
||||
(Some(IpAddr::V4(addr)), Some(IpAddr::V4(mask))) => Some((addr, mask)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.flat_map(|(addr, mask)| {
|
||||
let a = u32::from(addr);
|
||||
let m = u32::from(mask);
|
||||
let network = a & m;
|
||||
|
||||
let prefix = min(24, m.count_ones()); // mask → prefix length
|
||||
let size = 1u32 << (32 - prefix); // number of addresses
|
||||
|
||||
(0..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
|
||||
})
|
||||
.map(|addr| smol::spawn(ping_modbus_device(addr)))
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|x| x.ok())
|
||||
.collect();
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
async fn ping_modbus_device(addr: SocketAddr) -> Result<ModbusTcpProbe> {
|
||||
let mut device = ModbusTcpDevice::new(addr).await?;
|
||||
|
||||
let module_number1 = device.get_u32(0x2).await?;
|
||||
let module_number2 = device.get_u32(0x4).await?;
|
||||
|
||||
if module_number1 != 0x2787_2144 || module_number2 != 0x0000_0000 {
|
||||
bail!("Unknown modbus tcp device!");
|
||||
}
|
||||
|
||||
let serial = device.get_u32(0x000A).await?;
|
||||
|
||||
Ok(ModbusTcpProbe {
|
||||
addr,
|
||||
serial: serial as u16,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use core::future::Future;
|
||||
|
||||
// Extension trait for iterators of futures
|
||||
pub trait FutureIteratorExt<F>: Iterator<Item = F> + Sized
|
||||
where
|
||||
F: Future,
|
||||
{
|
||||
/// Await all futures and return their outputs
|
||||
fn join_all(self) -> impl Future<Output = Vec<F::Output>>;
|
||||
}
|
||||
|
||||
impl<I, F> FutureIteratorExt<F> for I
|
||||
where
|
||||
I: Iterator<Item = F> + Sized,
|
||||
F: Future,
|
||||
{
|
||||
async fn join_all(self) -> Vec<F::Output> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for fut in self {
|
||||
out.push(fut.await);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod ethernet;
|
||||
pub mod futures;
|
||||
pub mod helpers;
|
||||
pub mod irq_handling;
|
||||
pub mod modbus;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod modbus_serial_interface;
|
||||
pub mod tcp;
|
||||
|
||||
use anyhow::Error;
|
||||
use crc::{CRC_16_MODBUS, Crc};
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use smol::Timer;
|
||||
use smol::future::FutureExt;
|
||||
use smol::io;
|
||||
use smol::io::AsyncReadExt;
|
||||
use smol::io::AsyncWriteExt;
|
||||
use smol::net::TcpStream;
|
||||
|
||||
const PROTOCOL_ID: u16 = 0;
|
||||
const UNIT_ID: u8 = 0;
|
||||
const READ_HOLDING_FUNCTION_CODE: u8 = 3;
|
||||
const WRITE_HOLDING_FUNCTION_CODE: u8 = 16;
|
||||
const READ_HOLDING_LENGTH: u16 = 6; // Unit ID 00 + Func Code 03 + Start Address XXXX + Quantity XXXX = 6 bytes
|
||||
const WRITE_HOLDING_LENGTH_WITHOUT_DATA: u16 = 7; // Unit ID 00 + Func Code 16 + Start Address XXXX + Quantity XXXX + Num Data Bytes XX = 7 bytes
|
||||
|
||||
struct Packet {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
pub const fn new() -> Self {
|
||||
Self { buf: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_u8(&mut self, x: u8) {
|
||||
self.buf.push(x);
|
||||
}
|
||||
|
||||
pub fn add_u16(&mut self, x: u16) {
|
||||
let buf = x.to_be_bytes();
|
||||
self.buf.push(buf[0]);
|
||||
self.buf.push(buf[1]);
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModbusTcpDevice {
|
||||
stream: TcpStream,
|
||||
transactions: u16,
|
||||
}
|
||||
|
||||
impl Debug for ModbusTcpDevice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.stream.peer_addr() {
|
||||
Ok(addr) => write!(f, "ModbusTcpDevice({:?})", addr),
|
||||
Err(_) => std::fmt::Result::Err(std::fmt::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModbusTcpDevice {
|
||||
pub async fn new(addr: SocketAddr) -> Result<Self> {
|
||||
let timeout = async {
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
Err(io::Error::from(ErrorKind::TimedOut))
|
||||
};
|
||||
|
||||
let stream = TcpStream::connect(addr)
|
||||
.or(timeout)
|
||||
.await
|
||||
.context("Could not connect to modbus device!")?;
|
||||
|
||||
Ok(Self {
|
||||
stream,
|
||||
transactions: 0,
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_and_check_responce_header(&mut self, func_code: u8) -> Result<()> {
|
||||
if self.read_u16().await? != self.transactions {
|
||||
bail!("Modbus device sent unexpected transaction id!");
|
||||
}
|
||||
|
||||
if self.read_u16().await? != PROTOCOL_ID {
|
||||
bail!("Modbus device sent unexpected protocol id!");
|
||||
}
|
||||
|
||||
let length = self.read_u16().await?;
|
||||
if length < 2 {
|
||||
bail!("Modbus device will send too few bytes to understand the response!");
|
||||
}
|
||||
|
||||
if self.read_u8().await? != UNIT_ID {
|
||||
bail!("Modbus device sent unexpected unit id!");
|
||||
}
|
||||
|
||||
let func_code_res = self.read_u8().await?;
|
||||
if func_code != func_code_res {
|
||||
let error_code = self.read_u8().await?;
|
||||
bail!(
|
||||
"Modbus device sent unexpected function code 0x{:x}! It probably encountered an error. Error code: 0x{:x}",
|
||||
func_code_res,
|
||||
error_code
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_read_holding_request(&mut self, addr: u16, count: u16) -> Result<u16> {
|
||||
let mut packet = Packet::new();
|
||||
|
||||
self.transactions += 1;
|
||||
packet.add_u16(self.transactions);
|
||||
packet.add_u16(PROTOCOL_ID);
|
||||
packet.add_u16(READ_HOLDING_LENGTH);
|
||||
|
||||
packet.add_u8(UNIT_ID);
|
||||
packet.add_u8(READ_HOLDING_FUNCTION_CODE);
|
||||
|
||||
packet.add_u16(addr);
|
||||
packet.add_u16(count);
|
||||
|
||||
self.send(packet).await?;
|
||||
self.read_and_check_responce_header(READ_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
let num_data_bytes = self.read_u8().await? as u16;
|
||||
if count * 2 != num_data_bytes {
|
||||
bail!(
|
||||
"Modbus device wants to send only a portion of the requested range - UNIMPLEMENTED!"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(num_data_bytes)
|
||||
}
|
||||
|
||||
pub async fn get_holding_registers(&mut self, addr: u16, count: u16) -> Result<Vec<u16>> {
|
||||
self.send_read_holding_request(addr, count).await?;
|
||||
|
||||
let mut res: Vec<u16> = Vec::new();
|
||||
while res.len() < count as usize {
|
||||
res.push(self.read_u16().await?);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn set_holding_registers(&mut self, addr: u16, values: &[u16]) -> Result<()> {
|
||||
let count = values.len();
|
||||
let num_bytes = count * 2;
|
||||
assert!(
|
||||
count <= 128,
|
||||
"Cannot send that many bytes to modbus device in one go! Trying to send {} bytes of data.",
|
||||
num_bytes
|
||||
);
|
||||
|
||||
let mut packet = Packet::new();
|
||||
|
||||
self.transactions += 1;
|
||||
packet.add_u16(self.transactions);
|
||||
packet.add_u16(PROTOCOL_ID);
|
||||
packet.add_u16(WRITE_HOLDING_LENGTH_WITHOUT_DATA + num_bytes as u16);
|
||||
|
||||
packet.add_u8(UNIT_ID);
|
||||
packet.add_u8(WRITE_HOLDING_FUNCTION_CODE);
|
||||
|
||||
packet.add_u16(addr);
|
||||
packet.add_u16(count as u16);
|
||||
|
||||
packet.add_u8(num_bytes as u8);
|
||||
|
||||
for value in values {
|
||||
packet.add_u16(value.to_owned());
|
||||
}
|
||||
|
||||
self.send(packet).await?;
|
||||
self.read_and_check_responce_header(WRITE_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
if self.read_u16().await? != addr {
|
||||
bail!("Modbus device wrote to wrong register address!");
|
||||
}
|
||||
|
||||
if self.read_u16().await? != count as u16 {
|
||||
bail!("Modbus device wrote wrong number of registers!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_string<const N: usize>(&mut self, addr: u16) -> Result<String> {
|
||||
assert!(N % 2 == 0, "Strings are always of even length!");
|
||||
assert!(N <= 256, "Cannot read long strings in a single swoop!");
|
||||
let count = N / 2;
|
||||
|
||||
self.send_read_holding_request(addr, count as u16).await?;
|
||||
let buf = self.read_bytes::<N>().await?;
|
||||
|
||||
let s = String::from_utf8_lossy(&buf);
|
||||
let s = match s.split_once('\0') {
|
||||
None => s.to_string(),
|
||||
Some((l, _)) => l.to_string(),
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub async fn set_string<const N: usize>(&mut self, _addr: u16, s: &str) -> Result<()> {
|
||||
assert!(s.len() <= N, "String is too long to fit!");
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_u16(&mut self, addr: u16) -> Result<u16> {
|
||||
let length = self.send_read_holding_request(addr, 1).await?;
|
||||
assert_eq!(
|
||||
length, 2,
|
||||
"Expected modbus device to send exactly 2 bytes of data!"
|
||||
);
|
||||
|
||||
self.read_u16().await
|
||||
}
|
||||
|
||||
pub async fn get_u32(&mut self, addr: u16) -> Result<u32> {
|
||||
let length = self.send_read_holding_request(addr, 2).await?;
|
||||
assert_eq!(
|
||||
length, 4,
|
||||
"Expected modbus device to send exactly 4 bytes of data!"
|
||||
);
|
||||
|
||||
let buf = self.read_bytes::<4>().await?;
|
||||
Ok(u32::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
// We have to send the request in a single packet,
|
||||
// otherwise the device may fail to respond correctly.
|
||||
async fn send(&mut self, packet: Packet) -> Result<()> {
|
||||
self.stream
|
||||
.write_all(packet.as_bytes())
|
||||
.await
|
||||
.context("Could write to modbus device!")
|
||||
}
|
||||
|
||||
async fn read_u8(&mut self) -> Result<u8> {
|
||||
let buf = self.read_bytes::<1>().await?;
|
||||
Ok(u8::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_u16(&mut self) -> Result<u16> {
|
||||
let buf = self.read_bytes::<2>().await?;
|
||||
Ok(u16::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
let mut buf = [0; N];
|
||||
self.stream
|
||||
.read_exact(&mut buf)
|
||||
.await
|
||||
.context("Could not read from modbus device!")?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Generate 18 random alphanumeric characters base64 encode It
|
||||
password=$(head -c 18 /dev/urandom | base64)
|
||||
|
||||
echo "Basic Authentication; user=machine password=$password"
|
||||
echo $password > /tmp/machine_password
|
||||
|
||||
caddy_hash=$(caddy hash-password --plaintext $password)
|
||||
|
||||
echo "(machine_basic_auth) {
|
||||
basic_auth {
|
||||
machine $caddy_hash
|
||||
}
|
||||
}" | sudo tee /var/lib/caddy/auth_snippet.conf
|
||||
|
||||
# Transfer ownership to caddy user
|
||||
sudo chown caddy:caddy /var/lib/caddy/auth_snippet.conf
|
||||
|
||||
# Tigther permissions (Only readable by root or caddy user)
|
||||
sudo chmod 600 /var/lib/caddy/auth_snippet.conf
|
||||
|
||||
sudo systemctl restart caddy.service
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 557 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 6.6 MiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 985 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 74 KiB |
@@ -0,0 +1,931 @@
|
||||
# Wago Stepper Controller - Developer Documentation
|
||||
|
||||
**Models:** 750-671 & 750-672
|
||||
**Last Updated:** February 2026
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Hardware Quick Reference](#hardware-quick-reference)
|
||||
3. [Architecture & Code Organization](#architecture--code-organization)
|
||||
4. [Control & Status Bytes](#control--status-bytes)
|
||||
5. [Implementation Details](#implementation-details)
|
||||
6. [Prescalers & Calculations](#prescalers--calculations)
|
||||
7. [Common Pitfalls & Debug Tips](#common-pitfalls--debug-tips)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Wago Stepper Controllers (750-671 and 750-672) drive stepper motors via EtherCAT. This documentation focuses on **velocity/speed control mode** implementation.
|
||||
|
||||
### Key Differences Between Models
|
||||
|
||||
| Feature | 750-671 | 750-672 |
|
||||
| --------------- | ------------------------------------------ | ------------------------------------ |
|
||||
| Max Voltage | 50 VDC | 70 VDC |
|
||||
| Max Current | 5 A | 7.5 A |
|
||||
| Speed Control | Via Application Selector in config | Via Control Byte C1 command |
|
||||
| Control Byte C1 | Different bit layout (M_Speed_Control bit) | Command field (0x18 = Speed Control) |
|
||||
|
||||
**For 750-671:** Must set `Application_Selector = 2` and `PWM_Period = 0` in WAGO I/O CHECK before using speed control.
|
||||
|
||||
**For 750-672:** Speed control is selected directly via command in Control Byte C1 (no config needed).
|
||||
|
||||
### Process Image
|
||||
|
||||
Both models use **12 bytes bidirectional** (96 bits each way):
|
||||
|
||||
```
|
||||
Input (TxPDO - Controller → PLC):
|
||||
[0] S0 (status)
|
||||
[1] Reserved
|
||||
[2-3] Actual Velocity (i16)
|
||||
[4-5] Reserved
|
||||
[6-8] Actual Position (24-bit: L, M, H)
|
||||
[9] S3 (digital inputs, warnings)
|
||||
[10] S2 (busy, error, on_speed, etc.)
|
||||
[11] S1 (acknowledgments)
|
||||
|
||||
Output (RxPDO - PLC → Controller):
|
||||
[0] C0 (control)
|
||||
[1] Reserved
|
||||
[2-3] Target Velocity (i16, clamped to ±25000)
|
||||
[4-5] Acceleration (u16)
|
||||
[6-8] Position (24-bit: L, M, H)
|
||||
[9] C3 (position/reset control)
|
||||
[10] C2 (prescalers, error quit)
|
||||
[11] C1 (main control: enable, start, mode)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hardware Quick Reference
|
||||
|
||||
### Connections
|
||||
|
||||
| Pin | Name | Purpose |
|
||||
|-----|------|---------|
|
||||
| DI1+ | Digital Input 1 | Enable input (connect to 24V or control via software) |
|
||||
| DI2+ | Digital Input 2 | Referencing input |
|
||||
| DI- | Ground | Reference potential for DI1/DI2 |
|
||||
| A1, A2 | Motor Winding A | Connect to motor phase A |
|
||||
| B1, B2 | Motor Winding B | Connect to motor phase B |
|
||||
|
||||
### Status LEDs
|
||||
|
||||
- **LED H:** Shows error codes via blink patterns (see Wago manual section 3.3 for error codes)
|
||||
- Other LEDs show Ready, Enable, Busy states (see manual section 2.1.2.3)
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Code Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
devices/
|
||||
wago_modules/
|
||||
wago_750_672.rs # Device struct, EtherCAT interface
|
||||
|
||||
io/
|
||||
stepper_velocity_wago_750_672.rs # High-level wrapper & byte helpers
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
#### 1. `Wago750_672` Struct (EtherCAT Device)
|
||||
|
||||
Located in `devices/wago_modules/wago_750_672.rs`
|
||||
|
||||
```rust
|
||||
pub struct Wago750_672 {
|
||||
is_used: bool,
|
||||
tx_bit_offset: usize, // Input data offset
|
||||
rx_bit_offset: usize, // Output data offset
|
||||
pub rxpdo: Wago750_672RxPdo, // Data we send
|
||||
pub txpdo: Wago750_672TxPdo, // Data we receive
|
||||
module: Option<Module>,
|
||||
pub state: InitState,
|
||||
pub initialized: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
- Implements EtherCAT device traits
|
||||
- Handles bit-level I/O with process image
|
||||
- Runs state machine in `input()` method
|
||||
- Thread-safe (used via `Arc<RwLock<>>`)
|
||||
|
||||
#### 2. `StepperVelocityWago750672` Wrapper
|
||||
|
||||
Located in `io/stepper_velocity_wago_750_672.rs`
|
||||
|
||||
```rust
|
||||
pub struct StepperVelocityWago750672 {
|
||||
pub device: Arc<RwLock<Wago750_672>>,
|
||||
pub state: InitState,
|
||||
pub target_velocity: i16,
|
||||
pub target_acceleration: u16,
|
||||
pub enabled: bool,
|
||||
pub freq_range_sel: u8,
|
||||
pub acc_range_sel: u8,
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
- Provides high-level API (`set_velocity()`, `set_enabled()`)
|
||||
- Manages prescaler settings
|
||||
- Abstracts away state machine complexity
|
||||
|
||||
### Why Two Layers?
|
||||
|
||||
1. **`Wago750_672`:** Low-level EtherCAT device that handles cycle-by-cycle I/O
|
||||
2. **`StepperVelocityWago750672`:** High-level wrapper for application logic
|
||||
|
||||
This separation allows:
|
||||
- EtherCAT device to run in tight real-time loop
|
||||
- Application code to use simple API without worrying about cycles/acknowledgments
|
||||
- Easy testing of each layer independently
|
||||
|
||||
---
|
||||
|
||||
## Control & Status Bytes
|
||||
|
||||
### Design Pattern: Type-Safe Byte Construction
|
||||
|
||||
Instead of hardcoding hex values like `0x1B`, we use builder pattern with enums:
|
||||
|
||||
```rust
|
||||
// ❌ BAD - magic numbers, error-prone
|
||||
dev.rxpdo.c1 = 0x1B;
|
||||
|
||||
// ✅ GOOD - explicit, self-documenting
|
||||
let c1 = ControlByteC1::new()
|
||||
.with_flag(C1Flag::Enable)
|
||||
.with_flag(C1Flag::Stop2N)
|
||||
.with_command(C1Command::SpeedControl)
|
||||
.bits();
|
||||
dev.rxpdo.c1 = c1;
|
||||
```
|
||||
|
||||
### Wago 750-672 Control Bytes
|
||||
|
||||
#### Control Byte C1 (Offset 11) - Main Control
|
||||
|
||||
```rust
|
||||
pub struct ControlByteC1(u8);
|
||||
|
||||
pub enum C1Flag {
|
||||
Enable = 0b0000_0001, // Bit 0: Must be 1 to enable
|
||||
Stop2N = 0b0000_0010, // Bit 1: Must be 1 for normal operation
|
||||
Start = 0b0000_0100, // Bit 2: Rising edge triggers action
|
||||
}
|
||||
|
||||
pub enum C1Command {
|
||||
Idle = 0b0000_0000, // Bits 3-7: Operating mode
|
||||
SinglePosition = 0b0000_1000,
|
||||
RunProgram = 0b0001_0000,
|
||||
SpeedControl = 0b0001_1000, // ← We use this
|
||||
Reference = 0b0010_0000,
|
||||
JogMode = 0b0010_1000,
|
||||
Mailbox = 0b0011_0000,
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```rust
|
||||
impl ControlByteC1 {
|
||||
pub const fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub const fn with_flag(mut self, flag: C1Flag) -> Self {
|
||||
self.0 |= flag as u8; // OR in the bit
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn with_command(mut self, cmd: C1Command) -> Self {
|
||||
self.0 = (self.0 & 0b0000_0111) | (cmd as u8); // Mask lower 3 bits, set upper 5
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn bits(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- `const fn` allows compile-time evaluation
|
||||
- Chainable builder pattern
|
||||
- Command overwrites bits 3-7 while preserving 0-2
|
||||
- Flags are additive (multiple can be set)
|
||||
|
||||
#### Control Byte C2 (Offset 10) - Prescalers & Error Handling
|
||||
|
||||
```rust
|
||||
pub struct ControlByteC2(u8);
|
||||
|
||||
pub enum C2Flag {
|
||||
FreqRangeSelL = 0b0000_0001, // Bit 0: Freq prescaler LSB
|
||||
FreqRangeSelH = 0b0000_0010, // Bit 1: Freq prescaler MSB
|
||||
AccRangeSelL = 0b0000_0100, // Bit 2: Acc prescaler LSB
|
||||
AccRangeSelH = 0b0000_1000, // Bit 3: Acc prescaler MSB
|
||||
PreCalc = 0b0100_0000, // Bit 6: Pre-calculation
|
||||
ErrorQuit = 0b1000_0000, // Bit 7: Acknowledge error
|
||||
}
|
||||
|
||||
impl ControlByteC2 {
|
||||
pub const fn with_freq_range(mut self, sel: u8) -> Self {
|
||||
self.0 = (self.0 & 0b1111_1100) | (sel & 0b0000_0011);
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn with_acc_range(mut self, sel: u8) -> Self {
|
||||
self.0 = (self.0 & 0b1111_0011) | ((sel & 0b0000_0011) << 2);
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Critical:** Prescalers can only be changed when controller is **disabled** (before Enable state).
|
||||
|
||||
#### Control Byte C3 (Offset 9) - Position & Reset
|
||||
|
||||
```rust
|
||||
pub struct ControlByteC3(u8);
|
||||
|
||||
pub enum C3Flag {
|
||||
SetActualPos = 0b0000_0001, // Bit 0: Set current position
|
||||
DirectionPos = 0b0000_0010, // Bit 2: Force positive direction
|
||||
DirectionNeg = 0b0000_0100, // Bit 3: Force negative direction
|
||||
ResetQuit = 0b1000_0000, // Bit 7: Acknowledge reset
|
||||
}
|
||||
```
|
||||
|
||||
### Status Bytes (Read-Only)
|
||||
|
||||
#### Status Byte S1 (Offset 11) - Acknowledgments
|
||||
|
||||
```rust
|
||||
pub struct StatusByteS1(u8);
|
||||
|
||||
pub enum S1Flag {
|
||||
Ready = 0b0000_0001, // Bit 0: Acknowledges Enable
|
||||
Stop2NAck = 0b0000_0010, // Bit 1: Acknowledges Stop2_N
|
||||
StartAck = 0b0000_0100, // Bit 2: Acknowledges Start
|
||||
}
|
||||
// Bits 3-7: Mirrors C1 command when acknowledged
|
||||
|
||||
impl StatusByteS1 {
|
||||
pub const fn has_flag(self, flag: S1Flag) -> bool {
|
||||
(self.0 & flag as u8) != 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Pattern:**
|
||||
```rust
|
||||
// Wait for acknowledgment
|
||||
if self.txpdo.s1 == c1 {
|
||||
// All bits acknowledged, proceed
|
||||
}
|
||||
```
|
||||
|
||||
#### Status Byte S2 (Offset 10) - Operation Status
|
||||
|
||||
```rust
|
||||
pub struct StatusByteS2(u8);
|
||||
|
||||
pub enum S2Flag {
|
||||
OnTarget = 0b0000_0001, // Bit 0: Position reached (positioning mode)
|
||||
Busy = 0b0000_0010, // Bit 1: Executing command
|
||||
StandStill = 0b0000_0100, // Bit 2: Motor not moving
|
||||
OnSpeed = 0b0000_1000, // Bit 3: Target velocity reached
|
||||
Direction = 0b0001_0000, // Bit 4: Rotation direction (0=neg, 1=pos)
|
||||
ReferenceOk = 0b0010_0000, // Bit 5: Referencing complete
|
||||
PreCalcAck = 0b0100_0000, // Bit 6: Pre-calc acknowledged
|
||||
Error = 0b1000_0000, // Bit 7: Error occurred (check LED H)
|
||||
}
|
||||
```
|
||||
|
||||
**Critical for velocity control:** Monitor `OnSpeed` bit to know when target velocity is reached.
|
||||
|
||||
#### Status Byte S3 (Offset 9) - Digital Inputs & Warnings
|
||||
|
||||
```rust
|
||||
pub struct StatusByteS3(u8);
|
||||
|
||||
pub enum S3Flag {
|
||||
Input1 = 0b0000_0001, // Bits 0-5: Digital input states
|
||||
Input2 = 0b0000_0010,
|
||||
Input3 = 0b0000_0100,
|
||||
Input4 = 0b0000_1000,
|
||||
Input5 = 0b0001_0000,
|
||||
Input6 = 0b0010_0000,
|
||||
Warning = 0b0100_0000, // Bit 6: Warning condition
|
||||
Reset = 0b1000_0000, // Bit 7: Reset requested
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Critical Initialization Sequence
|
||||
|
||||
⚠️ **This sequence MUST be followed exactly or the controller won't work:**
|
||||
|
||||
1. **Set non-zero acceleration** (before enabling)
|
||||
2. **Set Enable + Stop2_N** (wait for ack)
|
||||
3. **Set operating mode** (0x18 for speed control, wait for ack)
|
||||
4. **Issue Start pulse** (set to 1, wait for ack, set to 0, wait for ack clear)
|
||||
5. **Running** (repeat step 4 every time velocity changes)
|
||||
|
||||
### State Machine (Why It Exists)
|
||||
|
||||
The state machine exists because **acknowledgments happen in different EtherCAT cycles**. You can't just set everything in one cycle.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InitState {
|
||||
Off, // Controller disabled
|
||||
Enable, // Setting Enable + Stop2_N, waiting for ack
|
||||
SetMode, // Setting speed control mode, waiting for ack
|
||||
StartPulseStart, // Setting Start bit, waiting for ack
|
||||
StartPulseEnd, // Clearing Start bit, waiting for ack clear
|
||||
Running, // Normal operation
|
||||
ErrorQuit, // Handling error
|
||||
ResetQuit, // Handling reset
|
||||
}
|
||||
```
|
||||
|
||||