Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fb62acab4 | ||
|
|
db5edc1832 | ||
|
|
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 |
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,73 @@ name: Electron
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
- master
|
||||
|
||||
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,96 @@
|
||||
name: Nix CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
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,9 +3,10 @@ name: Rust
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches: "*"
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,14 +17,24 @@ jobs:
|
||||
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 +52,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
|
||||
}
|
||||
```
|
||||
|
||||
### State Machine Implementation
|
||||
|
||||
Located in `Wago750_672::input()` method (runs every EtherCAT cycle):
|
||||
|
||||
```rust
|
||||
fn input(&mut self, input: &BitSlice) -> Result<(), anyhow::Error> {
|
||||
// 1. Read process image from controller
|
||||
let base = self.tx_bit_offset;
|
||||
let mut b = [0u8; 12];
|
||||
for i in 0..12 {
|
||||
b[i] = input[base + i * 8..base + (i + 1) * 8].load_le();
|
||||
}
|
||||
|
||||
self.txpdo = Wago750_672TxPdo {
|
||||
s0: b[0],
|
||||
actual_velocity: i16::from_le_bytes([b[2], b[3]]),
|
||||
position_l: b[6],
|
||||
position_m: b[7],
|
||||
position_h: b[8],
|
||||
s3: b[9],
|
||||
s2: b[10],
|
||||
s1: b[11],
|
||||
};
|
||||
|
||||
// 2. State machine logic
|
||||
match self.state {
|
||||
InitState::Off => {
|
||||
self.initialized = false;
|
||||
// Do nothing until external enable request
|
||||
}
|
||||
|
||||
InitState::Enable => {
|
||||
// Build C1 byte with Enable and Stop2_N
|
||||
let c1 = ControlByteC1::new()
|
||||
.with_flag(C1Flag::Enable)
|
||||
.with_flag(C1Flag::Stop2N)
|
||||
.bits();
|
||||
self.rxpdo.c1 = c1;
|
||||
|
||||
// Wait for acknowledgment
|
||||
if self.txpdo.s1 == c1 {
|
||||
self.state = InitState::SetMode;
|
||||
}
|
||||
}
|
||||
|
||||
InitState::SetMode => {
|
||||
// Add speed control command
|
||||
let c1 = ControlByteC1::new()
|
||||
.with_flag(C1Flag::Enable)
|
||||
.with_flag(C1Flag::Stop2N)
|
||||
.with_command(C1Command::SpeedControl)
|
||||
.bits();
|
||||
self.rxpdo.c1 = c1;
|
||||
|
||||
// Wait for mode acknowledgment
|
||||
if self.txpdo.s1 == c1 {
|
||||
self.initialized = true;
|
||||
self.state = InitState::StartPulseStart;
|
||||
}
|
||||
}
|
||||
|
||||
InitState::StartPulseStart => {
|
||||
// Set Start bit
|
||||
let c1 = ControlByteC1::new()
|
||||
.with_flag(C1Flag::Enable)
|
||||
.with_flag(C1Flag::Stop2N)
|
||||
.with_flag(C1Flag::Start) // ← Rising edge
|
||||
.with_command(C1Command::SpeedControl)
|
||||
.bits();
|
||||
self.rxpdo.c1 = c1;
|
||||
|
||||
// Wait for Start acknowledgment
|
||||
if self.txpdo.s1 == c1 {
|
||||
self.state = InitState::StartPulseEnd;
|
||||
}
|
||||
}
|
||||
|
||||
InitState::StartPulseEnd => {
|
||||
// Clear Start bit
|
||||
let c1 = ControlByteC1::new()
|
||||
.with_flag(C1Flag::Enable)
|
||||
.with_flag(C1Flag::Stop2N)
|
||||
.with_command(C1Command::SpeedControl)
|
||||
.bits();
|
||||
self.rxpdo.c1 = c1;
|
||||
|
||||
// Wait for acknowledgment to clear
|
||||
if self.txpdo.s1 == c1 {
|
||||
self.state = InitState::Running;
|
||||
}
|
||||
}
|
||||
|
||||
InitState::Running => {
|
||||
// Monitor for errors and resets
|
||||
let c2_error_mask = ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
|
||||
let c3_reset_mask = ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
|
||||
|
||||
if self.txpdo.s2 & c2_error_mask != 0 {
|
||||
self.state = InitState::ErrorQuit;
|
||||
} else if self.txpdo.s3 & c3_reset_mask != 0 {
|
||||
self.state = InitState::ResetQuit;
|
||||
}
|
||||
}
|
||||
|
||||
InitState::ErrorQuit => {
|
||||
// Set Error_Quit bit to acknowledge
|
||||
self.rxpdo.c2 |= ControlByteC2::new().with_flag(C2Flag::ErrorQuit).bits();
|
||||
tracing::error!("Stepper Controller Errored. Trying to reenable...");
|
||||
self.state = InitState::Enable; // Restart initialization
|
||||
}
|
||||
|
||||
InitState::ResetQuit => {
|
||||
// Set Reset_Quit bit to acknowledge
|
||||
self.rxpdo.c3 |= ControlByteC3::new().with_flag(C3Flag::ResetQuit).bits();
|
||||
tracing::error!("Stepper Controller Reset. Trying to reenable...");
|
||||
self.state = InitState::Enable; // Restart initialization
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Key Implementation Notes:**
|
||||
|
||||
1. **State changes happen AFTER acknowledgment:** We wait for `self.txpdo.s1 == c1` before moving to next state
|
||||
2. **Enable and Stop2_N stay high:** They're included in every C1 byte after Enable state
|
||||
3. **Start pulse is a rising edge:** Must go 1 → 0, not just stay at 1
|
||||
4. **Error recovery:** Automatically attempts to re-enable on error/reset
|
||||
|
||||
### High-Level API Implementation
|
||||
|
||||
Located in `StepperVelocityWago750672`:
|
||||
|
||||
```rust
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
// Prevent redundant enables
|
||||
if self.enabled && enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety check: acceleration must be non-zero
|
||||
if self.get_target_acceleration() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.enabled = enabled;
|
||||
if enabled {
|
||||
self.change_init_state(InitState::Enable);
|
||||
} else {
|
||||
self.change_init_state(InitState::Off);
|
||||
self.write_control_byte(ControlByte::C1, 0b00000000);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_velocity(&mut self, velocity: i16) {
|
||||
self.target_velocity = velocity;
|
||||
|
||||
let mut dev = block_on(self.device.write());
|
||||
|
||||
// Clamp to valid range
|
||||
dev.rxpdo.velocity = velocity.clamp(-25000, 25000);
|
||||
|
||||
// Trigger Start pulse if already initialized
|
||||
if dev.initialized {
|
||||
dev.state = InitState::StartPulseStart;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_acceleration(&mut self, acceleration: u16) {
|
||||
self.target_acceleration = acceleration;
|
||||
|
||||
let mut dev = block_on(self.device.write());
|
||||
dev.rxpdo.acceleration = acceleration;
|
||||
}
|
||||
```
|
||||
|
||||
**Why the wrapper?**
|
||||
- Application code doesn't need to know about state machine
|
||||
- Handles common errors (zero acceleration check)
|
||||
- Thread-safe access to device
|
||||
- Clamping and validation in one place
|
||||
|
||||
### Changing Velocity at Runtime
|
||||
|
||||
⚠️ **Critical:** Must issue a **new Start pulse** for every velocity change:
|
||||
|
||||
```rust
|
||||
// User calls this
|
||||
stepper.set_velocity(15000);
|
||||
|
||||
// Internally, set_velocity() does:
|
||||
dev.rxpdo.velocity = 15000;
|
||||
if dev.initialized {
|
||||
dev.state = InitState::StartPulseStart; // ← Triggers new pulse
|
||||
}
|
||||
|
||||
// State machine then handles:
|
||||
// StartPulseStart → StartPulseEnd → Running
|
||||
// (Takes 2-3 EtherCAT cycles)
|
||||
```
|
||||
|
||||
**Common mistake:** Setting velocity without triggering Start pulse - motor won't change speed.
|
||||
|
||||
---
|
||||
|
||||
## Prescalers & Calculations
|
||||
|
||||
Prescalers scale the velocity and acceleration values. They **can only be changed when disabled**.
|
||||
|
||||
### Frequency Prescaler (Bits 0-1 of C2)
|
||||
|
||||
Controls velocity scaling:
|
||||
|
||||
| Freq_Range_Sel | Prescaler | Max Frequency | Resolution |
|
||||
|----------------|-----------|---------------|------------|
|
||||
| 00 | 200 (default) | 10 kHz | Configurable |
|
||||
| 01 | 80 | 25 kHz | 1 Hz |
|
||||
| 10 | 20 | 100 kHz | 20 Hz |
|
||||
| 11 | 4 | 500 kHz | 20 Hz |
|
||||
|
||||
**Formula:**
|
||||
```
|
||||
Output Frequency (fp) = Velocity × 80 / Freq_Prescaler [Hz]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
// Freq_Range_Sel = 01 (prescaler = 80)
|
||||
// Velocity = 10000
|
||||
// Output: 10000 × 80 / 80 = 10000 Hz
|
||||
|
||||
stepper.set_freq_range_sel(1); // Must be done BEFORE enable
|
||||
stepper.set_velocity(10000);
|
||||
```
|
||||
|
||||
### Acceleration Prescaler (Bits 2-3 of C2)
|
||||
|
||||
Controls acceleration scaling:
|
||||
|
||||
| Acc_Range_Sel | Multiplier | Time to Max Speed |
|
||||
|---------------|------------|-------------------|
|
||||
| 00 | 8 (default) | 7600 ms |
|
||||
| 01 | 80 | 760 ms |
|
||||
| 10 | 800 | 76 ms |
|
||||
| 11 | 8000 | 7.6 ms |
|
||||
|
||||
**Formula:**
|
||||
```
|
||||
Acceleration (a) = Acceleration_Value × Acc_Multiplier / Freq_Prescaler [Hz/s]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
// Acc_Range_Sel = 01 (multiplier = 80)
|
||||
// Freq_Range_Sel = 01 (prescaler = 80)
|
||||
// Acceleration_Value = 10000
|
||||
// Output: 10000 × 80 / 80 = 10000 Hz/s
|
||||
|
||||
stepper.set_acc_range_sel(1); // Must be done BEFORE enable
|
||||
stepper.set_acceleration(10000);
|
||||
```
|
||||
|
||||
### Setting Prescalers (Implementation)
|
||||
|
||||
```rust
|
||||
pub fn set_freq_range_sel(&mut self, factor: u8) {
|
||||
// Safety checks
|
||||
if self.enabled || factor > 3 {
|
||||
return; // Can't change while enabled
|
||||
}
|
||||
|
||||
self.freq_range_sel = factor;
|
||||
|
||||
let mut dev = block_on(self.device.write());
|
||||
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
|
||||
.with_freq_range(factor)
|
||||
.bits();
|
||||
dev.rxpdo.c2 = c2;
|
||||
}
|
||||
|
||||
pub fn set_acc_range_sel(&mut self, factor: u8) {
|
||||
if self.enabled || factor > 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.acc_range_sel = factor;
|
||||
|
||||
let mut dev = block_on(self.device.write());
|
||||
let c2 = ControlByteC2::from_bits(dev.rxpdo.c2)
|
||||
.with_acc_range(factor)
|
||||
.bits();
|
||||
dev.rxpdo.c2 = c2;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```rust
|
||||
let mut stepper = StepperVelocityWago750672::new(device);
|
||||
|
||||
// Set prescalers FIRST (before enabling)
|
||||
stepper.set_freq_range_sel(1);
|
||||
stepper.set_acc_range_sel(1);
|
||||
stepper.set_acceleration(10000);
|
||||
|
||||
// Then enable
|
||||
stepper.set_enabled(true);
|
||||
|
||||
// Now can set velocity
|
||||
stepper.set_velocity(15000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls & Debug Tips
|
||||
|
||||
### 1. Controller Won't Enable
|
||||
|
||||
**Symptoms:**
|
||||
- State stuck in `Enable`
|
||||
- `initialized` never becomes true
|
||||
- Motor doesn't move
|
||||
|
||||
**Debug checklist:**
|
||||
```rust
|
||||
// Check acceleration is non-zero
|
||||
println!("Acceleration: {}", dev.rxpdo.acceleration); // Must be > 0
|
||||
|
||||
// Check DI1 if hardware enable is configured
|
||||
println!("DI1 state: {}", (dev.txpdo.s3 & 0x01) != 0); // Should be 1
|
||||
|
||||
// Check status byte acknowledgment
|
||||
println!("C1: {:#010b}", dev.rxpdo.c1);
|
||||
println!("S1: {:#010b}", dev.txpdo.s1); // Should match C1
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
```rust
|
||||
// ❌ Forgot to set acceleration
|
||||
stepper.set_acceleration(0); // Won't enable!
|
||||
|
||||
// ✅ Always set non-zero acceleration
|
||||
stepper.set_acceleration(10000);
|
||||
stepper.set_enabled(true);
|
||||
```
|
||||
|
||||
### 2. Velocity Won't Change
|
||||
|
||||
**Symptoms:**
|
||||
- Motor running but won't change speed
|
||||
- `set_velocity()` called but no effect
|
||||
|
||||
**Debug:**
|
||||
```rust
|
||||
// Check initialized flag
|
||||
println!("Initialized: {}", dev.initialized);
|
||||
|
||||
// Check state after velocity change
|
||||
println!("State: {:?}", dev.state); // Should become StartPulseStart
|
||||
|
||||
// Manually trigger Start pulse (bypass wrapper)
|
||||
dev.state = InitState::StartPulseStart;
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
```rust
|
||||
// ❌ Setting velocity before initialization
|
||||
stepper.set_velocity(10000); // Controller not ready yet
|
||||
|
||||
// ✅ Check initialized first
|
||||
if stepper.device.read().initialized {
|
||||
stepper.set_velocity(10000);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Prescaler Not Working
|
||||
|
||||
**Symptoms:**
|
||||
- Velocity calculation seems wrong
|
||||
- Can't reach expected frequency
|
||||
|
||||
**Debug:**
|
||||
```rust
|
||||
// Check prescalers were set BEFORE enabling
|
||||
println!("Freq prescaler: {}", stepper.freq_range_sel);
|
||||
println!("Acc prescaler: {}", stepper.acc_range_sel);
|
||||
println!("Enabled: {}", stepper.enabled);
|
||||
|
||||
// Check C2 byte
|
||||
let dev = stepper.device.read();
|
||||
println!("C2: {:#010b}", dev.rxpdo.c2);
|
||||
// Bits 0-1: freq, Bits 2-3: acc
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
```rust
|
||||
// ❌ Setting prescaler after enable
|
||||
stepper.set_enabled(true);
|
||||
stepper.set_freq_range_sel(1); // Too late! Ignored.
|
||||
|
||||
// ✅ Set prescalers FIRST
|
||||
stepper.set_freq_range_sel(1);
|
||||
stepper.set_acc_range_sel(1);
|
||||
stepper.set_enabled(true);
|
||||
```
|
||||
|
||||
### 4. State Machine Stuck
|
||||
|
||||
**Symptoms:**
|
||||
- State doesn't progress
|
||||
- Acknowledgments not received
|
||||
|
||||
**Debug:**
|
||||
```rust
|
||||
// Add logging to state machine
|
||||
match self.state {
|
||||
InitState::Enable => {
|
||||
println!("Enable state: C1={:#010b}, S1={:#010b}", self.rxpdo.c1, self.txpdo.s1);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// Check EtherCAT communication
|
||||
// - Is cycle running?
|
||||
// - Are other devices working?
|
||||
// - Check bus topology
|
||||
```
|
||||
|
||||
### 5. Error Bit Set
|
||||
|
||||
**Symptoms:**
|
||||
- Error bit (S2 bit 7) is 1
|
||||
- LED H blinking
|
||||
- Controller stops
|
||||
|
||||
**Debug:**
|
||||
```rust
|
||||
// Check error state
|
||||
let s2 = StatusByteS2::from_bits(dev.txpdo.s2);
|
||||
if s2.has_flag(S2Flag::Error) {
|
||||
println!("ERROR DETECTED");
|
||||
|
||||
// Count LED H blinks for error code
|
||||
// See Wago manual section 3.3
|
||||
|
||||
// Check other status flags for clues
|
||||
println!("Busy: {}", s2.has_flag(S2Flag::Busy));
|
||||
println!("StandStill: {}", s2.has_flag(S2Flag::StandStill));
|
||||
}
|
||||
```
|
||||
|
||||
**Common error codes:**
|
||||
- **2 blinks:** Overcurrent
|
||||
- **3 blinks:** Position error
|
||||
- **4 blinks:** Encoder fault
|
||||
- See Wago manual for complete list
|
||||
|
||||
---
|
||||
|
||||
### Supporting Wago 750-671
|
||||
|
||||
The 750-671 uses different C1 bit layout:
|
||||
|
||||
1. **Create separate enum:**
|
||||
```rust
|
||||
// For 750-671
|
||||
pub enum C1Flag671 {
|
||||
Enable = 0b0000_0001, // Bit 0
|
||||
Stop2N = 0b0000_0010, // Bit 1
|
||||
Start = 0b0000_0100, // Bit 2
|
||||
MSpeedControl = 0b0000_1000, // Bit 3 (instead of command field)
|
||||
MProgram = 0b0001_0000, // Bit 4
|
||||
MReference = 0b0010_0000, // Bit 5
|
||||
MJog = 0b0100_0000, // Bit 6
|
||||
MDriveByMBX = 0b1000_0000, // Bit 7
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
### Initialization Checklist
|
||||
```rust
|
||||
// 1. Create and configure
|
||||
let stepper = StepperVelocityWago750672::new(device);
|
||||
|
||||
// 2. Set prescalers (optional, before enable)
|
||||
stepper.set_freq_range_sel(1); // 0-3
|
||||
stepper.set_acc_range_sel(1); // 0-3
|
||||
|
||||
// 3. Set non-zero acceleration (REQUIRED)
|
||||
stepper.set_acceleration(10000);
|
||||
|
||||
// 4. Enable
|
||||
stepper.set_enabled(true);
|
||||
|
||||
// 5. Set velocity (after initialized)
|
||||
stepper.set_velocity(15000); // ±25000 max
|
||||
```
|
||||
|
||||
### Key Constants
|
||||
```rust
|
||||
const SPEED_CONTROL_CMD: u8 = 0x18; // C1 command for 750-672
|
||||
const MAX_VELOCITY: i16 = 25000;
|
||||
const MIN_VELOCITY: i16 = -25000;
|
||||
const DEFAULT_ACCELERATION: u16 = 10000;
|
||||
```
|
||||
|
||||
### Critical Rules
|
||||
1. ⚠️ **Never enable with zero acceleration**
|
||||
2. ⚠️ **Always wait for acknowledgments**
|
||||
3. ⚠️ **Issue Start pulse for every velocity change**
|
||||
4. ⚠️ **Prescalers can only be changed when disabled**
|
||||
5. ⚠️ **Keep Enable and Stop2_N high during operation**
|
||||
|
||||
### Common Bit Patterns
|
||||
```rust
|
||||
// Enable + Stop2_N
|
||||
0b0000_0011
|
||||
|
||||
// Enable + Stop2_N + Speed Control
|
||||
0b0001_1011
|
||||
|
||||
// Enable + Stop2_N + Speed Control + Start
|
||||
0b0001_1111
|
||||
```
|
||||
|
||||
### Debug Commands
|
||||
```bash
|
||||
# View byte values
|
||||
println!("C1: {:#010b}", dev.rxpdo.c1);
|
||||
println!("S1: {:#010b}", dev.txpdo.s1);
|
||||
|
||||
# Check specific flags
|
||||
let s2 = StatusByteS2::from_bits(dev.txpdo.s2);
|
||||
println!("Error: {}", s2.has_flag(S2Flag::Error));
|
||||
println!("OnSpeed: {}", s2.has_flag(S2Flag::OnSpeed));
|
||||
|
||||
# Monitor state
|
||||
println!("State: {:?}", dev.state);
|
||||
println!("Initialized: {}", dev.initialized);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Wago Manual:** Section 2.1.2.3 (LEDs), 3.3 (Error Codes)
|
||||
- **WAGO I/O CHECK:** Configuration software for 750-671 application selector
|
||||
- **EtherCAT Specification:** www.ethercat.org
|
||||
- **Code Location:**
|
||||
- Device: `devices/wago_modules/wago_750_672.rs`
|
||||
- Wrapper: `io/stepper_velocity_wago_750_672.rs`
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Version | Date | Changes |
|
||||
| ------- | ------- | ------------------------------------------- |
|
||||
| 1.0 | 2025 | Initial implementation with state machine |
|
||||
| 2.0 | 2026-02 | Added comprehensive developer documentation |
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Check the troubleshooting section or add debug logging using the template above.
|
||||
@@ -0,0 +1,139 @@
|
||||
# XTREM Communication Protocol
|
||||
|
||||
**Version:** 3.007
|
||||
**Device:** XTREM / XTREM-S Weighing Module
|
||||
**Standard:** OIML R76:2006 / EN45501:2015 compliant
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **XTREM communication protocol** allows a terminal or host controller to exchange data and commands with the XTREM weighing module.
|
||||
The device transmits and receives ASCII-coded frames over a serial interface (RS232, RS485, Wi-Fi, or Ethernet, depending on configuration).
|
||||
|
||||
Since we are using the Ethernet interface we need to provide some kind of DHCP to assign an IP - Address to each XTREM Device.
|
||||
They then may be addressed via a **broadcast** IP (e.g.: 192.168.4.255/24).
|
||||
A **Unicast** communication is applied through the device ID inside the XTREM Frame.
|
||||
|
||||
---
|
||||
|
||||
## Frame Structure
|
||||
|
||||
Each message—both query and response—has the same format:
|
||||
|
||||
```
|
||||
STX ID_O ID_D F D_ADDRESS D_L DATA LRC ETX
|
||||
```
|
||||
|
||||
| Field | Description | Length (bytes) |
|
||||
| :------------ | :------------------------------------------------ | :------------: |
|
||||
| **STX** | Start of message (ASCII `02h`) | 1 |
|
||||
| **ID_O** | Sender address (2 ASCII hex chars) | 2 |
|
||||
| **ID_D** | Destination address (2 ASCII hex chars) | 2 |
|
||||
| **F** | Function code (`R`, `W`, `E`, etc.) | 1 |
|
||||
| **D_ADDRESS** | Data address (register, 4 ASCII hex chars) | 4 |
|
||||
| **D_L** | Data length (2 ASCII hex chars) | 2 |
|
||||
| **DATA** | Optional data field | Variable |
|
||||
| **LRC** | Longitudinal Redundancy Check (2 ASCII hex chars) | 2 |
|
||||
| **ETX** | End of message (ASCII `03h`) | 1 |
|
||||
|
||||
In the default configuration the Frame also requires a CRLF at the end.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
STX ‘1’ ‘7’ ‘0’ ‘1’ ‘R’ ‘0’ ‘1’ ‘0’ ‘1’ ‘0’ ‘0’ LRC ETX CR LF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Function Codes
|
||||
|
||||
| Function | ASCII | Hex | Description |
|
||||
| :------- | :---- | :---| :-------------- |
|
||||
| **R** | R | 52h | Read request |
|
||||
| **r** | r | 72h | Read response |
|
||||
| **W** | W | 57h | Write request |
|
||||
| **w** | w | 77h | Write response |
|
||||
| **E** | E | 45h | Execute command |
|
||||
| **e** | e | 65h | Execute response|
|
||||
|
||||
### Write response codes
|
||||
|
||||
* `'0'` – OK
|
||||
* `'1'` – Write denied (sealed)
|
||||
* `'2'` – Read-only register
|
||||
* `'3'` – Invalid value / out of range
|
||||
|
||||
### Execute response codes
|
||||
|
||||
* `'0'` – Executed successfully
|
||||
* `'1'` – Blocked by seal switch
|
||||
|
||||
---
|
||||
|
||||
## LRC Calculation
|
||||
|
||||
The **Longitudinal Redundancy Check** is a one-byte XOR of all message bytes, excluding:
|
||||
|
||||
* `STX`
|
||||
* `ETX`
|
||||
* The LRC itself
|
||||
|
||||
Example C function:
|
||||
|
||||
```c
|
||||
unsigned char LRC(unsigned char* s) {
|
||||
unsigned char lrc = 0;
|
||||
while (*s) {
|
||||
lrc ^= *(s++);
|
||||
}
|
||||
return lrc;
|
||||
}
|
||||
```
|
||||
|
||||
Example Rust function:
|
||||
```rust
|
||||
pub fn compute_lrc(data: &[u8]) -> u8 {
|
||||
data.iter().fold(0u8, |acc, &b| acc ^ b)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Data Registers
|
||||
|
||||
| Address (hex) | Description | Type | Notes |
|
||||
| :------------ | :------------------------- | :--: | :------------------------------------ |
|
||||
| **0000h** | Serial number | R | Factory-set |
|
||||
| **0001h** | Device ID | RW | Network address |
|
||||
| **0007h** | Hardware version | R | Read-only |
|
||||
| **0008h** | Software version | R | Read-only |
|
||||
| **0009h** | Sealing switch status | R | `0=UNLOCK`, `1=LOCK` |
|
||||
| **0010h** | Baud rate | RW | `0–4` → 9600–115200 baud |
|
||||
| **0013h** | Stream mode output rate | RW | Time in ms |
|
||||
| **0100h** | Device state | R | Status bits (power, Wi-Fi, ADC, etc.) |
|
||||
| **0101h** | Weight value | R | Gross weight |
|
||||
| **0102h** | Tare value | RE | Read/Execute |
|
||||
| **0103h** | Net weight | R | Gross − Tare |
|
||||
| **0110h** | ADC counts (instant) | R | Raw ADC data |
|
||||
| **0111h** | ADC counts (filtered) | R | Post-filter data |
|
||||
| **1010h** | Stop stream mode | E | Execute |
|
||||
| **1011h** | Start stream mode (weight) | E | Continuous output |
|
||||
| **9999h** | Device reset | E | Software reset |
|
||||
| **EEEEh** | Factory reset | E | Restores defaults |
|
||||
|
||||
---
|
||||
|
||||
## Post-Factory Reset Configuration
|
||||
|
||||
After performing a factory reset, several registers must be configured to ensure proper operation of the device.
|
||||
|
||||
Note: The unit may remain set to 'g' after a factory reset, but the values received may be in 'kg' depending on the device configuration.
|
||||
|
||||
The following registers should be reviewed and set as required:
|
||||
1. **Address: 0022 | Max (Max1) (scale maximum capacity)**
|
||||
The default value for this register may vary between devices (e.g., 60000 or 6000). Set this value to 6000 to ensure correct operation.
|
||||
2. **Address: 0023 | e (e1) (scale interval)**
|
||||
3. **Address: 0026 | Decimal position**
|
||||
@@ -82,3 +82,93 @@ git push
|
||||
# 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.
|
||||
|
||||
# Minimal Hardware Examples
|
||||
|
||||
To get started with actual hardware, check out these step-by-step tutorials:
|
||||
|
||||
- **[LED Control with EL2004](./minimal-example-el2004.md)** - Digital output control, the simplest possible hardware setup
|
||||
- **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements
|
||||
- **[Stepper Motor Control with EL7031-0030](./minimal-example-el7031-motor.md)** - Complete motor control integration with velocity control
|
||||
|
||||
These examples provide complete hardware wiring diagrams and software setup instructions.
|
||||
|
||||
|
||||
# More Docs
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
|
||||
- [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)
|
||||
@@ -0,0 +1,224 @@
|
||||
# Minimal Example — LED Control on the EL2004
|
||||
A complete hardware + software walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
7. [Acknowledgements](#7-acknowledgements)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
The EL2004 LED Toggle Example is a minimal demonstration showing how to control digital outputs on a **Beckhoff EL2004 EtherCAT terminal** using the QiTech machine framework.
|
||||
It represents the simplest possible hardware interaction in the system:
|
||||
**toggling LED outputs using the QiTech Control Dashboard.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- Beckhoff **EL2004 EtherCAT Terminal** (4-channel digital output)
|
||||
- Beckhoff **EK1100 EtherCAT Coupler**
|
||||
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
|
||||
- Jumper / bridge wires (0.5–1.5 mm² recommended)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
### Software
|
||||
*(Installation steps in Section 4)*
|
||||
- Rust toolchain
|
||||
- Node.js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL (included inside repo)
|
||||
|
||||
---
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Schematic
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 3.2 EK1100 Wiring
|
||||
|
||||
This wiring configuration powers the EL2004 and prepares it for LED control.
|
||||
It is not the only possible wiring but is the **simplest functional setup**.
|
||||
|
||||
#### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT terminals can cause serious damage or electrical shock.
|
||||
|
||||
---
|
||||
|
||||
#### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
|
||||
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### 3.2.2 Wiring Steps (Used in This Example)
|
||||
|
||||
We supply power using a **DC hollow-plug adapter**, like this one:
|
||||
https://www.amazon.de/dp/B093FTFZ8Q
|
||||
|
||||
Perform the following wiring on the EK1100:
|
||||
|
||||
1. Red wire **(+24 V)** → Terminal **2**
|
||||
2. Black wire **(0 V)** → Terminal **3**
|
||||
3. Jumper wire from **Terminal 1 → Terminal 6**
|
||||
4. Jumper wire from **Terminal 5 → Terminal 7**
|
||||
|
||||
After wiring, your module should look like **Figure 1**.
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 1 — EK1100 Minimal Wiring**
|
||||
<img src="../assets/ek1100.jpeg" width="400">
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3.3 EL2004 Integration
|
||||
|
||||
Slide the EL2004 onto the **right side of the EK1100** until it locks.
|
||||
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
|
||||
|
||||
#### **Figure 2 — EL2004 Terminal**
|
||||
<img src="../assets/el2004.jpeg" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Final Assembled Setup
|
||||
|
||||
#### **Figure 3 — EK1100 + EL2004 Connected**
|
||||
<img src="../assets/complete.jpeg" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Power & Ethernet
|
||||
|
||||
#### Power
|
||||
Connect the 24 V adapter to the hollow plug used earlier.
|
||||
**Example AC/DC Adapter (Figure 4):**
|
||||
|
||||
<img src="../assets/adapter.jpeg" width="400">
|
||||
|
||||
|
||||
#### Ethernet
|
||||
Use a standard LAN cable to connect your PC → EK1100.
|
||||
The final powered up and connected setup should look like this:
|
||||
|
||||
<img src="../assets/power.jpeg" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
### 4.2 Running the Backend
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
This script:
|
||||
|
||||
- Builds the backend
|
||||
|
||||
- Grants required system capabilities (raw sockets)
|
||||
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the EK1100 is connected.
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
```bash
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
|
||||
Once the backend + frontend are running, you should see:
|
||||
|
||||
- EK1100 Coupler
|
||||
|
||||
- EL2004 Digital Output Terminal
|
||||

|
||||
|
||||
Steps:
|
||||
|
||||
- Click Assign on the EK1100
|
||||
|
||||
- Select TestMachine V1
|
||||
|
||||

|
||||
|
||||
|
||||
- Enter a serial number (use the same for EK1100 + EL2004)
|
||||
|
||||

|
||||
|
||||
- Click Write
|
||||
|
||||
- Repeat for the EL2004
|
||||
|
||||
### 5.2 Testing LED Control
|
||||
Navigate to:
|
||||
|
||||
Machines → TestMachine
|
||||

|
||||
You will see this interface:
|
||||
|
||||

|
||||
|
||||
**You can now toggle the four digital outputs of the EL2004.**
|
||||
|
||||
## 6. References
|
||||
|
||||
This guide incorporates information from official Beckhoff documentation.
|
||||
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
|
||||
|
||||
Referenced Manuals
|
||||
|
||||
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
|
||||
|
||||
[Beckhoff EL2004 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el20xx_el2124_de.pdf)
|
||||
|
||||
## 7. Acknowledgements
|
||||
|
||||
This tutorial is inspired by the clarity and educational quality of Beckhoff manuals.
|
||||
All wiring illustrations and hardware descriptions in this guide are provided for demonstration purposes only and do not replace official Beckhoff installation guidelines.
|
||||
|
||||
Special thanks to the QiTech engineering team for providing the backend architecture, EtherCAT HAL abstraction, and the TestMachine framework that makes this example possible.
|
||||
@@ -0,0 +1,178 @@
|
||||
# Minimal Example — Analog Input EL3021
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
## 1. Introduction
|
||||
The EL3021 Analog Input example demonstrates how to measure analog current on a **Beckhoff EL3021 EtherCAT terminal** using the QiTech machine framework.
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- Beckhoff **EL3021 EtherCAT Terminal** (1-channel analog input)
|
||||
- Beckhoff **EK1100 EtherCAT Coupler**
|
||||
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
|
||||
- **Adjustable DC power supply** (or any other means of producing 4-20mA DC @ <10V )
|
||||
- Jumper / bridge wires (0.5–1.5 mm² recommended)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Schematic
|
||||

|
||||
The 24V supply powers the EL2004, while the adjustable power supply generates the analog input signal for the EL3021.
|
||||
|
||||
#### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT terminals can cause serious damage or electrical shock.
|
||||
|
||||
### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
|
||||
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 3.2.2 Wiring Steps (Used in This Example)
|
||||
|
||||
We supply power using a **DC hollow-plug adapter**, like this one:
|
||||
https://www.amazon.de/dp/B093FTFZ8Q
|
||||
|
||||
Perform the following wiring on the EK1100:
|
||||
|
||||
1. Red wire **(+24 V)** → Terminal **2**
|
||||
2. Black wire **(0 V)** → Terminal **3**
|
||||
3. Jumper wire from **Terminal 1 → Terminal 6**
|
||||
4. Jumper wire from **Terminal 5 → Terminal 7**
|
||||
|
||||
After wiring, your module should look like **Figure 1**.
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 1 — EK1100 Minimal Wiring**
|
||||
<img src="../assets/ek1100.jpeg" width="400">
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3.3 EL3021 Integration
|
||||
|
||||
Slide the EL3021 onto the **right side of the EK1100** until it locks.
|
||||
The EtherCAT E-Bus and power contacts connect automatically.
|
||||
Then you need to wire the adjustable power supply to the EL3021.
|
||||
|
||||
#### **Figure 2 — EL3021 Terminal**
|
||||
<img src="../assets/EL3021.jpg" width="400">
|
||||
|
||||
1. Wire ⊕ to **Terminal 1**
|
||||
2. Wire ⊖ to **Terminal 2**
|
||||
---
|
||||
|
||||
### 3.4 Final Assembled Setup
|
||||
|
||||
#### **Figure 3 — EK1100 + EL3021 Connected**
|
||||
<img src="../assets/EK1100+EL3021.jpg" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 3.5 Power & Ethernet
|
||||
|
||||
### 3.5.1 Power
|
||||
Connect the 24 V adapter to the hollow plug used earlier.
|
||||
|
||||
#### Example AC/DC Adapter (Figure 4):
|
||||
|
||||
<img src="../assets/adapter.jpeg" width="400">
|
||||
|
||||
|
||||
### 3.5.2 Ethernet
|
||||
Use a standard LAN cable to connect your PC → EK1100.
|
||||
The final powered up and connected setup should look like this:
|
||||
|
||||
<img src="../assets/EL3021_power+eth.jpg" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
### 4.2 Running the Backend
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
This script:
|
||||
|
||||
- Builds the backend
|
||||
|
||||
- Grants required system capabilities (raw sockets)
|
||||
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the EK1100 is connected.
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
```bash
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
Once both backend and frontend are running, the Setup -> EtherCat page should look something like this:
|
||||

|
||||
Listing the EK1100 and the EL3021.
|
||||
|
||||
To assign the two devices to a machine, click "Assign" on both devices. This should open the following pop-up modal:
|
||||
|
||||
**EK1100**
|
||||

|
||||
1. Select "AnalogTest V1" under "Machine"
|
||||
2. Choose a serial number to uniquely identify the machine (1 in this example)
|
||||
3. Under "Device Role" select "0 - Bus Coupler"
|
||||
|
||||
**EL3021**
|
||||

|
||||
1. Select "AnalogTest V1" under "Machine"
|
||||
2. Use the same serial number you chose for the EK1100
|
||||
3. Under "Device Role" select "1 - EL3021"
|
||||
---
|
||||
The machine is now configured.
|
||||
A new Tab "AnalogTest" should appear in the right sidebar. Clicking that tab should show the machine control interface:
|
||||

|
||||
Results should show the current measured by the analog input - the current set on the adjustable power supply.
|
||||
|
||||
|
||||
## 6. References
|
||||
This guide incorporates information from official Beckhoff documentation.
|
||||
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
|
||||
|
||||
Referenced Manuals
|
||||
|
||||
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
|
||||
[Beckhoff EL3021 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el30xx_de.pdf)
|
||||
@@ -0,0 +1,236 @@
|
||||
# Minimal Example — Beckhoff EL7031-0030 Stepper Motor Integration
|
||||
A complete hardware + software walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
- [3.2 EK1100 Wiring](#32-ek1100-wiring)
|
||||
- [3.2.1 Safe Wiring Procedure](#321-safe-wiring-procedure-beckhoff-recommended)
|
||||
- [3.2.2 Wiring (EL7031-0030)](#322-wiring-el7031-0030)
|
||||
- [3.3 Safety Warning](#33--safety-warning)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
- [4.1 Installing on Linux](#41-installing-on-linux-this-depends-on-your-distro)
|
||||
- [4.2 Running the Backend](#42-running-the-backend)
|
||||
- [4.3 Running the Frontend](#43-running-the-frontend)
|
||||
5. [Demo](#5-demo)
|
||||
- [5.1 Assigning Devices in the Dashboard](#51-assigning-devices-in-the-dashboard)
|
||||
6. [Documentation](#6-documentation)
|
||||
7. [Software Architecture](#7-software-architecture)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
This project documents the successful integration of a **Beckhoff EL7031-0030 Stepper Motor Terminal** into the QiTech control software stack.
|
||||
The goal was to control a 24V stepper motor via EtherCAT using a custom Rust backend and a React/Electron frontend.
|
||||
|
||||
---
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### Software
|
||||
- Rust toolchain
|
||||
- Node.js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL (included inside repo)
|
||||
|
||||
### Hardware
|
||||
- **EtherCAT Master:** Linux PC
|
||||
- **Bus Coupler:** EtherCAT Beckhoff EK1100
|
||||
- **Stepper Driver:** Beckhoff EL7031-0030 (THIS IS DIFFERENT FROM EL7031)
|
||||
- **Motor:** Standard 4-wire Stepper Motor
|
||||
- **Power Supply:** 24V DC
|
||||
- **Ethernet Cable:** Standard Ethernet cable
|
||||
- **Wiring Tools:** Screwdriver, wires
|
||||
|
||||
---
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.2 EK1100 Wiring
|
||||
|
||||
This wiring configuration powers the EL7031-0030.
|
||||
It is not the only possible wiring but is the **simplest functional setup**.
|
||||
|
||||
#### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT terminals can cause serious damage or electrical shock.
|
||||
|
||||
---
|
||||
|
||||
#### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
|
||||
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
We supply power using a **DC hollow-plug adapter**, like this one:
|
||||
https://www.amazon.de/dp/B093FTFZ8Q
|
||||
|
||||
Perform the following wiring on the EK1100:
|
||||
|
||||
1. Red wire **(+24 V)** → Terminal **2**
|
||||
2. Black wire **(0 V)** → Terminal **3**
|
||||
3. Jumper wire from **Terminal 1 → Terminal 6**
|
||||
4. Jumper wire from **Terminal 5 → Terminal 7**
|
||||
|
||||
After wiring, your module should look like **Figure 1**.
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 1 — EK1100 Minimal Wiring**
|
||||
<img src="../assets/ek1100.jpeg" width="400">
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 2 — EL7031-0030 Terminal**
|
||||
|
||||
<img src="../assets/EL7031_0030.jpg" width="300">
|
||||
|
||||
Slide the EL7031-0030 onto the right side of the EK1100 until it locks.
|
||||
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 3 — Motor**
|
||||
<img src="../assets/motor.jpg" width="300">
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 4 — Motor Wiring**
|
||||
<img src="../assets/motor_wiring.jpg" width="300">
|
||||
|
||||
Now the motor is wired via the pins 4, 5, 12, 13 on the EL7031-0030.
|
||||
|
||||
---
|
||||
|
||||
#### **Figure 5 — EL7031 Integration Connected**
|
||||
|
||||
<img src="../assets/EL7031_0030_connected.jpg" width="400">
|
||||
|
||||
That's what the pin should look like.
|
||||
|
||||
---
|
||||
|
||||
### 3.2.2 Wiring (EL7031-0030)
|
||||
|
||||
**Crucial:** The EL7031 requires two power sources: E-Bus (side contacts) for logic, and Front Terminal (6 & 14) for motor power.
|
||||
|
||||
| Terminal Point | Function | Cable Color (Example) |
|
||||
| :------------- | :--------------- | :-------------------- |
|
||||
| **4** | Motor Coil A1 | Red |
|
||||
| **12** | Motor Coil A2 | Blue |
|
||||
| **5** | Motor Coil B1 | Green |
|
||||
| **13** | Motor Coil B2 | Black |
|
||||
| **6** | **Power +24V** | PSU Red (+) |
|
||||
| **14** | **Power 0V/GND** | PSU Black (-) |
|
||||
|
||||
---
|
||||
|
||||
### 3.3 ⚠️ Safety Warning
|
||||
|
||||
- **Mandatory Power Supply:** Without 24V connected to **Pin 6 (+24V)** and **Pin 14 (GND)**, the terminal will remain in `PREOP` state or show a "Warning" LED (No Power). The motor will **not** move without this external supply.
|
||||
|
||||
#### Risk of Destruction (Short Circuit):
|
||||
- **Never** connect the 24V Power Supply to the Motor Output pins (**4, 5, 12, 13**). This will instantly destroy the terminal (causing pins 12 & 13 to glow red/burn).
|
||||
- Ensure strict separation: **Pins 4/5/12/13 are for the MOTOR ONLY**.
|
||||
- **Pins 6/14 are for POWER ONLY**.
|
||||
|
||||
Also, see the documentation of the EtherCAT terminal ([EL7031-0030](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)) for the different power modes (page 46 ff.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Linux (this depends on your distro)
|
||||
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4.2 Running the Backend
|
||||
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
- Builds the backend
|
||||
- Grants required system capabilities (raw sockets)
|
||||
- Starts EtherCAT communication
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
|
||||
```bash
|
||||
# move to the control directory
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
|
||||
Once the backend + frontend are running, you should see:
|
||||
|
||||

|
||||
|
||||
Make sure that under "Assign" → "Machine Assignment"
|
||||
the correct serial number is selected (each device should have the same one, here it is 1).
|
||||
Under "Machine" select your new Machine however you named it (I named it "TestMotor V1"), else it will detect that there is something connected to it but the connection won't work.
|
||||
|
||||

|
||||
|
||||
**NOW THE MOTOR SHOULD TURN!**
|
||||
|
||||
In the interface "TestMotor" on the left side, you can now control the motor (its state and its speed).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 6. Documentation
|
||||
|
||||
Use the official documentation of the EL7031-0030 for more information:
|
||||
[Beckhoff EL7031-0030 Documentation](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)
|
||||
|
||||
---
|
||||
|
||||
## 7. Software Architecture
|
||||
|
||||
### Backend (Rust)
|
||||
Located in [machines/src/motor_test_machine/](/machines/src/motor_test_machine/).
|
||||
|
||||
1. **`mod.rs`**: Defines the `MotorTestMachine` struct and holds the state (driver wrapper, enabled state, target velocity).
|
||||
2. **`api.rs`**: Handles incoming JSON commands from the frontend (Enable/Disable, Set Velocity) via WebSockets/SocketIO.
|
||||
3. **`act.rs`**: The real-time control loop. It updates the `StepperVelocityEL70x1` driver wrapper in every cycle based on the current state.
|
||||
4. **`new.rs`**: Initializes the hardware.
|
||||
|
||||
### Frontend (TypeScript/React)
|
||||
Located in [electron/src/machines/motor_test_machine/](/electron/src/machines/motor_test_machine/) and [electron/src/routes/routes.tsx](/electron/src/routes/routes.tsx).
|
||||
|
||||
1. **`useTestMotor.ts`**: Custom hook managing the optimistic state and communication with the backend.
|
||||
2. **`TestMotorControlPage.tsx`**: The UI using QiTech UI components (`ControlCard`, `EditValue`, `SelectionGroupBoolean`) to match the look and feel of the Winder2.
|
||||
3. **Routing**: Integrated into `routes.tsx` using TanStack Router, ensuring the machine appears in the sidebar and navigation works correctly.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Minimal Example — Analog Input Wago 750-455
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
## 1. Introduction
|
||||
The Wago 750-455 Analog Input example demonstrates how to measure analog current on a **Wago 750-455 EtherCAT module** using the QiTech machine framework. This module provides 4 independent analog input channels that can read 4-20mA signals.
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- **Wago 750-354 EtherCAT Coupler**
|
||||
- **Wago 750-602 Power Supply** (24V VDC)
|
||||
- **Wago 750-455 EtherCAT Module** (4-channel analog input, 4-20mA)
|
||||
- **Wago 750-600 End Module**
|
||||
- **24 V DC power supply** for the Wago system
|
||||
- **Adjustable DC power supply** (or any other means of producing 4-20mA DC @ <10V)
|
||||
- Jumper / bridge wires (0.5–1.5 mm² recommended)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
### Software
|
||||
*(Installation steps in Section 4)*
|
||||
- Rust toolchain
|
||||
- Node.js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL (included inside repo)
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Overview
|
||||
The Wago 750-455 is a 4-channel analog input module that measures current signals in the 4-20mA range. Each channel has its own input terminals and can operate independently.
|
||||
|
||||
#### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT modules can cause serious damage or electrical shock.
|
||||
|
||||
### 3.2 Wiring Procedure
|
||||
|
||||
#### 3.2.1 Safe Wiring (Wago Spring Clamp)
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 3.3 Wago 750-455 Module Wiring
|
||||
|
||||
The Wago 750-455 has 4 analog input channels. Each channel requires two terminals:
|
||||
|
||||
**Channel 1 (AI1):**
|
||||
- Terminal 1: ⊕ (Positive input)
|
||||
- Terminal 2: ⊖ (Negative input/Ground)
|
||||
|
||||
**Channel 2 (AI2):**
|
||||
- Terminal 3: ⊕ (Positive input)
|
||||
- Terminal 4: ⊖ (Negative input/Ground)
|
||||
|
||||
**Channel 3 (AI3):**
|
||||
- Terminal 5: ⊕ (Positive input)
|
||||
- Terminal 6: ⊖ (Negative input/Ground)
|
||||
|
||||
**Channel 4 (AI4):**
|
||||
- Terminal 7: ⊕ (Positive input)
|
||||
- Terminal 8: ⊖ (Negative input/Ground)
|
||||
|
||||
For this minimal example, we'll connect at least one channel to an adjustable power supply:
|
||||
|
||||
1. Wire adjustable power supply ⊕ to **Terminal 1** (Channel 1 positive)
|
||||
2. Wire adjustable power supply ⊖ to **Terminal 2** (Channel 1 negative)
|
||||
|
||||
*Repeat for additional channels as needed.*
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Wago I/O System Integration
|
||||
|
||||
The Wago 750-455 module plugs into a Wago fieldbus system. Consult your specific Wago controller/coupler documentation for:
|
||||
- Power supply requirements
|
||||
- Network configuration
|
||||
- Module placement in the I/O rack
|
||||
|
||||
The module automatically connects to the backplane for EtherCAT communication and power.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Power & Ethernet
|
||||
|
||||
#### Power
|
||||
Ensure your Wago system has adequate 24V DC power connected per manufacturer specifications.
|
||||
|
||||
#### Ethernet
|
||||
Connect your PC to the Wago controller/coupler using a standard Ethernet cable.
|
||||
|
||||
---
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4.2 Running the Backend
|
||||
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
- Builds the backend
|
||||
- Grants required system capabilities (raw sockets)
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the Wago controller is connected and powered.
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
|
||||
```bash
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
|
||||
Once both backend and frontend are running:
|
||||
|
||||
1. Navigate to **Setup → EtherCAT** page
|
||||
2. You should see your Wago controller and the 750-455 module listed
|
||||
3. Select the Device called 750-354 and assign the Wago AI Test V1 Machine, set the Serial to 1 and set the device role to 0 (Wago Bus Coupler)
|
||||
4. Press write thn restart the backend to load the changes
|
||||
|
||||
### 5.2 Testing Analog Input
|
||||
|
||||
1. Set your adjustable power supply to output a current in the 4-20mA range
|
||||
2. In the dashboard, navigate to the monitoring view
|
||||
3. Observe the analog input value for the connected channel(s)
|
||||
|
||||
### 5.3 Channel Specifications
|
||||
|
||||
The Wago 750-455 provides:
|
||||
- **Input Range:** 4-20mA
|
||||
- **Wiring Error Detection:** Built-in detection for open/short circuit conditions
|
||||
- **4 Independent Channels:** Each can be read separately
|
||||
|
||||
### 5.4 Error Detection
|
||||
|
||||
The module includes wiring error detection. If you see a wiring error flag:
|
||||
- Check that your current source is within the 4-20mA range
|
||||
- Verify all wire connections are secure
|
||||
- Ensure the current loop is properly closed
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
### Wago Documentation
|
||||
- [Wago 750-455 Product Page](https://www.wago.com/de-en/io-systems/4-channel-analog-input/p/750-455)
|
||||
- Wago I/O System manual
|
||||
- EtherCAT configuration guide for Wago systems
|
||||
|
||||
### QiTech Control
|
||||
- [Architecture Overview](../architecture-overview.md)
|
||||
- [EtherCAT Basics](../ethercat-basics.md)
|
||||
- [Device Documentation](../devices.md)
|
||||
- [Getting Started Guide](getting-started.md)
|
||||
|
||||
### Related Examples
|
||||
- [Minimal Example — EL3021](minimal-example-el3021.md) (Single channel analog input)
|
||||
- [Minimal Example — EL2004](minimal-example-el2004.md) (Digital output)
|
||||
@@ -0,0 +1,240 @@
|
||||
# Minimal Example — LED Control on the WAGO750-402
|
||||
A complete hardware + software walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
The WAGO 750-402 Display Example is a minimal demonstation showing how to display the digital inputs on a WAGO 750-402 EtherCAT terminal using the QiTech machine framework.
|
||||
It represents the simplest possible hardware interaction in the system:
|
||||
Displaying the Digital Inputs using the QiTech Control Dashboard.
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- WAGO **2787-2144 EtherCAT**
|
||||
- WAGO **750-354 EtherCAT Coupler**
|
||||
- WAGO **750-602 EtherCAT Power Terminal**
|
||||
- WAGO **750-402 EtherCAT Terminal** (4.channle digital input)
|
||||
- WAGO **750-600 EtherCAT Endmodule**
|
||||
- WAGO **852-1322 Ethernet-Switch**
|
||||
- **24V DC Lab power supply**(AC/DC adapter)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Software
|
||||
*(Installation steps in Section 4)*
|
||||
|
||||
- Rust tollchain
|
||||
- Node-js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL *(include inside repo)*
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Schematic
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT terminals can cause serious damage or electrical shock.
|
||||
|
||||
### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 3.2.2 Wiring Steps (Used in This Example)
|
||||
|
||||
We supply power to the other devices by using the **750-602 EtherCAT Power Terminal**
|
||||
|
||||
Perform the following wiring on the Ethernet-Switch:
|
||||
|
||||
1. Red wire (+24V) → PWR+
|
||||
2. Black wire (0V) → RPS-
|
||||
|
||||
#### **Figure 1 —**
|
||||
<img src="../assets/WAGO750_402_switch.JPG" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.3 WAGO Wiring the Coupler
|
||||
|
||||
Wire the **750-354 EtherCAT Coupler** to the power supply
|
||||
1. red wire (+24V)
|
||||
2. yellow (0V)
|
||||
|
||||
#### **Figure 2 —**
|
||||
<img src="../assets/WAGO750_402_coupler.JPG" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.4 750-602 EtherCAT Power Terminal Integration
|
||||
|
||||
Slide the **750-602 EtherCAT Power Terminal** onto the **right side of the 750-354 Coupler** until it locks.
|
||||
|
||||
Next We need to wire the EtherCat Power Terminal
|
||||
|
||||
Perform the next wiring Steps:
|
||||
|
||||
1. Green wire (Ground) → **Terminal4**
|
||||
2. Red wire (+24V) from **750-602 EtherCAT Power Terminal** → **Terminal 2**
|
||||
3. Black wire(0V) from **750-602 EtherCAT Power Terminal** → **Terminal 3**
|
||||
4. Red wire (+24V) from **750-354 EtherCAT Coupler** → **Terminal 6**
|
||||
5. Yellow wire (0V) from **750-354 EtherCAT Coupler** → **Terminal 7**
|
||||
|
||||
#### **Figure 3 — Wiring**
|
||||
<img src="../assets/WAGO750_402_power_terminal.JPG" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 3.5 750-402 EtherCAT Terminal Integration
|
||||
|
||||
Slide the **750-402 EtherCAT Terminal** onto to **right side of the 750-602 EtherCAT Power Terminal** until it locks.
|
||||
|
||||
Wire from the **24V DC Lab Adapter** → **Terminal 1**
|
||||
|
||||
#### **Figure 4 — Wiring**
|
||||
<img src="../assets/WAGO750_402_terminal.JPG" width="400">
|
||||
|
||||
## 3.6 750-600 Final Assemly
|
||||
|
||||
Slide the **750-600 EtherCAT Endmodule** onto to **right side of the 7750-402 EtherCAT Terminal** until it locks.
|
||||
|
||||
No wiring is required
|
||||
|
||||
#### **Figure 5 — 750-402 + 750-600 Connected**
|
||||
<img src="../assets/WAGO750_402_end_modul.JPG" width="400">
|
||||
|
||||
## 3.7 Power & Etherneet
|
||||
|
||||
### Power
|
||||
|
||||
Connect the 2787-2144 power supply to the power outlet
|
||||
|
||||
### Ehternet
|
||||
|
||||
Use a standard LAN cable to connect to your PC → **852-1322 Ethernet-Switch**
|
||||
|
||||
And another from **852-1322 Ethernet-Switch** → **750-354 EtherCAT Coupler**
|
||||
|
||||
#### **Figure 6 — Ethernet**
|
||||
<img src="../assets/WAGO750_402_ethernet.JPG" width="400">
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
### 4.2 Running the Backend
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
This script:
|
||||
|
||||
- Builds the backend
|
||||
|
||||
- Grants required system capabilities (raw sockets)
|
||||
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the WAGO750-402 is connected.
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
```bash
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
|
||||
Once the backend + frontend are running, you should see:
|
||||
|
||||
- WAGO750-354 (Coupler)
|
||||
|
||||
- wago module (Digital Input Terminal)
|
||||
|
||||

|
||||
|
||||
Steps:
|
||||
|
||||
- Click Assign on the wago module
|
||||
|
||||
- Select Digital Input Machine V1
|
||||
|
||||

|
||||
|
||||
|
||||
- Enter a serial number except for 0
|
||||
|
||||

|
||||
|
||||
- Click Write
|
||||
|
||||
### 5.2 Diyplaying LED
|
||||
Navigate to:
|
||||
|
||||
Machines → DigitalInputMachine
|
||||
|
||||

|
||||
|
||||
You will see this interface:
|
||||
|
||||

|
||||
|
||||
Now you can apply a Voltage of the Range of 15V-30V (+24V recommended) from the DC lab power supply to the Terminal.
|
||||
The Terminal can read the digital input, which is signaled by the LED on the Terminal and should be displayed in the Control software.
|
||||
|
||||
In our example it lights up the first LED
|
||||
|
||||

|
||||
|
||||
## 6. References
|
||||
This guide incorporates information from official WAGO documentation.
|
||||
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
|
||||
|
||||
Referenced Manuals
|
||||
|
||||
[WAGO 750-402 Documentation](https://www.wago.com/de/io-systeme/4-kanal-digitaleingang/p/750-402#downloads)
|
||||
|
||||
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/feldbuskoppler-ethercat/p/750-354#details)
|
||||
|
||||
[WAGO 750-602 Documentation](https://www.wago.com/de/io-systeme/potentialeinspeisung/p/750-602#details)
|
||||
|
||||
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/endmodul/p/750-600#details)
|
||||
@@ -136,7 +136,7 @@ A slave recognizes message data as one message when a **3.5 character long no-da
|
||||
|
||||
# Inverter settings
|
||||
|
||||
## Motor Constants
|
||||
## Motor Constants for ExtruderV2:
|
||||
|
||||
The following is a table that shows which Registers need to be set on the Inverter for the safe operation of the Motor.
|
||||
|
||||
@@ -151,6 +151,22 @@ The following is a table that shows which Registers need to be set on the Invert
|
||||
| `3` | Base frequency | 5000 (50 Hz) |
|
||||
| `19` | Base frequency voltage | 400 (Volt) |
|
||||
|
||||
|
||||
## Motor Constants for ExtruderV3:
|
||||
|
||||
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 | 22 (2.2 kw) |
|
||||
| `96` | Auto tuning setting/status | 1 |
|
||||
| `1` | Max Frequency | 5000 |
|
||||
| `2` | Min Frequency | 0 |
|
||||
| `9` | Electronic Thermal O/L Relay | 4.40 (Ampere) |
|
||||
| `3` | Base frequency | 5000 (50 Hz) |
|
||||
| `19` | Base frequency voltage | 400 (Volt) |
|
||||
|
||||
## All Settings needed for the Inverter Communication/Operation with Extruder
|
||||
|
||||
The Following settings are required to get the inverter Communication and Operation working properly
|
||||
|
||||
@@ -10,7 +10,7 @@ This is why we chose nixos
|
||||
# How we do updates
|
||||
1. The user selects a version (commit/branch/tag) of the software they want to install
|
||||
2. The repo is cloned to the local machine
|
||||
3. the `installInfo.nix` is created and commited locally. It contains infos like the commit hash, branch, tag, timestamp etc. These are later available as environment variables `QITECH_OS...`
|
||||
3. Information like the commit hash, branch, tag, timestamp etc. is passed to the nix build process as environment variables `QITECH_OS...`
|
||||
4. The `nixos-switch` command is run. After rebuilding & compiling the software the system reboots.
|
||||
- The nixos-switch command does the following:
|
||||
1. The system is built with the new configuration
|
||||
@@ -21,4 +21,4 @@ This is why we chose nixos
|
||||
|
||||
# More Docs
|
||||
- [Implementation Details](./details.md)
|
||||
- [Fresh Installation Quick Start guide](./quick-start.md)
|
||||
- [Fresh Installation Quick Start guide](./quick-start.md)
|
||||
@@ -0,0 +1,241 @@
|
||||
# REST API `/api/v2`
|
||||
|
||||
The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at:
|
||||
|
||||
- **Base URL:** `http://10.10.10.1:3001`
|
||||
|
||||
> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below).
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. It is therefore suggested to run the entire production line in isolated operation.
|
||||
|
||||
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity. To let the outside world communicate with the production line, a router should be placed at the network bondary bridging the production line's subnet and the rest of the network. The router can run in **client/bridge mode** on the line's subnet and expose the panel's API via **port forwading** or through a **reverse proxy** where one can add authentication, logging, rate limiting, etc.
|
||||
|
||||
The following graphic depicts the suggested network topology for a single production line. Many lines could potentially share the same router.
|
||||
|
||||
<p align="center">
|
||||
<img width="769" height="528" alt="Suggested network topology" src="./assets/subnet.png" />
|
||||
</p>
|
||||
|
||||
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
|
||||
|
||||
---
|
||||
|
||||
## List machines `GET /api/v2/machine`
|
||||
|
||||
Returns the set of machines currently known/connected to the panel.
|
||||
|
||||
Machines are identified by:
|
||||
|
||||
- `slug`: the machine type / model identifier (string)
|
||||
- `serial`: the specific machine instance identifier (int)
|
||||
|
||||
Each machine also includes a `legacy_id` to support older **v1** workflows. If a machine reports an issue, the `error` field may be present and non-null (containing an error message).
|
||||
|
||||
### Example request
|
||||
|
||||
```bash
|
||||
curl -X GET "http://10.10.10.1:3001/api/v2/machine"
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
```json
|
||||
{
|
||||
"machines": [
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 7
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "mock",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 4
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "extruder_v1",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 2
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "winder_v1",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 10
|
||||
},
|
||||
"serial": 48879
|
||||
},
|
||||
"serial": 48879,
|
||||
"vendor": "QiTech",
|
||||
"slug": "wago_power_v1",
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get current values `GET /api/v2/machine/<slug>/<serial>`
|
||||
|
||||
Returns all currently known values for a single machine.
|
||||
|
||||
Values are categorized into two groups:
|
||||
|
||||
- **State**: requested/commanded values (these typically change only after a state-change request, or if another controller updates them)
|
||||
- **Live Values**: measured/observed values coming from the machine and potentially changing quickly
|
||||
|
||||
This REST endpoint returns **only the current snapshot**, not a stream of live values.
|
||||
To receive continuous updates (via WebSockets), subscribe to the machine namespace (see **WebSockets** below).
|
||||
|
||||
### Example request (mock machine)
|
||||
|
||||
```bash
|
||||
curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922"
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
```json
|
||||
{
|
||||
"machine": {
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 7
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "mock",
|
||||
"error": null
|
||||
},
|
||||
"state": {
|
||||
"frequency1": 100.0,
|
||||
"frequency2": 200.0,
|
||||
"frequency3": 500.0,
|
||||
"is_default_state": false,
|
||||
"mode_state": {
|
||||
"mode": "Running"
|
||||
}
|
||||
},
|
||||
"live_values": {
|
||||
"amplitude1": -0.03438523433309566,
|
||||
"amplitude2": -0.06872980145477608,
|
||||
"amplitude3": -0.1711138370170743,
|
||||
"amplitude_sum": -0.27422887280494607
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Change machine state `POST /api/v1/machine/<slug>/<serial>`
|
||||
|
||||
State changes are submitted as **mutations**. The mutation payload is defined per machine type in Rust. Conceptually, each item in the mutation list represents a setter-style operation that is applied by the real-time control loop.
|
||||
|
||||
The API does **not** return the newly-applied state in the POST response. The panel runs a real-time loop and generally won’t block waiting for the physical system to converge. Instead:
|
||||
|
||||
- Submit the mutation via `POST`
|
||||
- Poll `GET /api/v2/machine/<slug>/<serial>` to observe the updated state and/or any reported errors
|
||||
|
||||
### Example request (mock machine)
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-d \
|
||||
-H "Content-Type: application/json" \
|
||||
"http://10.10.10.1:3001/api/v1/machine/mock/57922"
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
```json
|
||||
null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSockets
|
||||
|
||||
For continuous updates, subscribe to a machine-specific namespace derived from its `legacy_id`:
|
||||
|
||||
- Namespace: `/machine/<vendor>/<id>/<serial>`
|
||||
|
||||
The stream emits events for:
|
||||
|
||||
- state changes (`StateEvent`)
|
||||
- live value updates (`LiveValuesEvent`)
|
||||
|
||||
Both event payloads use the same machine-specific schema as the `/api/v2` REST responses.
|
||||
|
||||
---
|
||||
|
||||
## List of all machines
|
||||
|
||||
Below is a template you can fill with links to the relevant Rust types (mutations + state/live structs).
|
||||
For each machine, link to:
|
||||
|
||||
- **Mutations:** the request payload type used by `POST /api/v1/machine/<slug>/<serial>`
|
||||
- **State / Live Values:** the response payload types returned by `GET /api/v2/machine/<slug>/<serial>`
|
||||
|
||||
### Machines
|
||||
|
||||
- **winder_v1**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L87>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L163>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L143>
|
||||
|
||||
- **extruder_v1**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L214>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L89>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L55>
|
||||
|
||||
- **laser_v1**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L88>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L32>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L17>
|
||||
|
||||
- **mock**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L84>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L38>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L24>
|
||||
|
||||
- **extruder_v2**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L126>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L93>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L59>
|
||||
@@ -73,7 +73,7 @@ typings/
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
# Serverless directoriesProcessMetrics
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
@@ -88,6 +88,7 @@ typings/
|
||||
# Vite
|
||||
.vite/
|
||||
dist/
|
||||
dist-electron/
|
||||
|
||||
# Electron-Forge
|
||||
out/
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
||||
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||
import { MakerDeb } from "@electron-forge/maker-deb";
|
||||
import { MakerRpm } from "@electron-forge/maker-rpm";
|
||||
import { VitePlugin } from "@electron-forge/plugin-vite";
|
||||
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
||||
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
||||
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
executableName: "electron-shadcn",
|
||||
asar: true,
|
||||
icon: "src/assets/icon",
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({}),
|
||||
new MakerZIP({}, ["darwin"]),
|
||||
new MakerRpm({}),
|
||||
new MakerDeb({}),
|
||||
],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
// If you are familiar with Vite configuration, it will look really familiar.
|
||||
build: [
|
||||
{
|
||||
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
|
||||
entry: "src/main.ts",
|
||||
config: "vite.main.config.ts",
|
||||
},
|
||||
{
|
||||
entry: "src/preload.ts",
|
||||
config: "vite.preload.config.ts",
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
name: "main_window",
|
||||
config: "vite.renderer.config.ts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,35 +0,0 @@
|
||||
export {}; // Make this a module
|
||||
|
||||
declare global {
|
||||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
|
||||
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
|
||||
// whether you're running in development or production).
|
||||
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
const MAIN_WINDOW_VITE_NAME: string;
|
||||
|
||||
namespace NodeJS {
|
||||
interface Process {
|
||||
// Used for hot reload after preload scripts.
|
||||
viteDevServers: Record<string, import("vite").ViteDevServer>;
|
||||
}
|
||||
}
|
||||
|
||||
type VitePluginConfig = ConstructorParameters<
|
||||
typeof import("@electron-forge/plugin-vite").VitePlugin
|
||||
>[0];
|
||||
|
||||
interface VitePluginRuntimeKeys {
|
||||
VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`;
|
||||
VITE_NAME: `${string}_VITE_NAME`;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "vite" {
|
||||
interface ConfigEnv<
|
||||
K extends keyof VitePluginConfig = keyof VitePluginConfig,
|
||||
> {
|
||||
root: string;
|
||||
forgeConfig: VitePluginConfig;
|
||||
forgeConfigSelf: VitePluginConfig[K][number];
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
{
|
||||
"name": "qitech-control-electron",
|
||||
"productName": "QiTech Control",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"version": "2.4.1",
|
||||
"description": "Next Generation Machine Interface ",
|
||||
"main": ".vite/build/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"lint:write": "eslint . --fix",
|
||||
"format": "prettier --check .",
|
||||
@@ -22,15 +20,6 @@
|
||||
},
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.1",
|
||||
"@electron-forge/maker-deb": "^7.8.0",
|
||||
"@electron-forge/maker-rpm": "^7.8.3",
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.10.2",
|
||||
"@electron-forge/plugin-vite": "^7.6.1",
|
||||
"@electron-forge/shared-types": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@eslint/compat": "^1.2.6",
|
||||
"@eslint/js": "^9.18.0",
|
||||
@@ -47,7 +36,7 @@
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -55,14 +44,16 @@
|
||||
"globals": "^16.2.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.4.1",
|
||||
"vitest": "^3.0.6"
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.6",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
@@ -84,7 +75,6 @@
|
||||
"@tanstack/react-router": "^1.130.17",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -98,20 +88,20 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"socket.io-msgpack-parser": "^3.0.2",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uplot": "^1.6.32",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zod-validation-error": "^3.4.0",
|
||||
"zustand": "^5.0.8"
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^4.3.5",
|
||||
"zod-validation-error": "^5.0.0",
|
||||
"zustand": "^5.0.11"
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,22 @@ 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";
|
||||
import { GlobalLaserToastManager } from "./setup/GlobalLaserToastManager";
|
||||
|
||||
export default function App() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
// Start global log streaming
|
||||
useGlobalLogStreaming();
|
||||
|
||||
useEffect(() => {
|
||||
syncThemeWithLocal();
|
||||
updateAppLanguage(i18n);
|
||||
}, [i18n]);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
return (
|
||||
<>
|
||||
<GlobalLaserToastManager />
|
||||
<RouterProvider router={router} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// enable immer MapSet plugin
|
||||
|
||||
@@ -15,25 +15,31 @@ import {
|
||||
machineIdentificationUnique,
|
||||
} from "@/machines/types";
|
||||
import { useRef } from "react";
|
||||
import { rustEnumSchema } from "@/lib/types";
|
||||
import { rustEnum } from "@/lib/types";
|
||||
|
||||
export type EthercatDevices = z.infer<typeof ethercatDevicesSchema>;
|
||||
|
||||
export const ethercatDevicesSchema = z.object({
|
||||
devices: z.array(
|
||||
z.object({
|
||||
configured_address: z.number().int(),
|
||||
name: z.string(),
|
||||
vendor_id: z.number().int(),
|
||||
product_id: z.number().int(),
|
||||
revision: z.number().int(),
|
||||
device_identification: deviceIdentification,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Update the EthercatDevicesEventData schema
|
||||
export const ethercatDevicesEventDataSchema = rustEnumSchema({
|
||||
Initializing: z.boolean(),
|
||||
Done: z.object({
|
||||
devices: z.array(
|
||||
z.object({
|
||||
configured_address: z.number().int(),
|
||||
name: z.string(),
|
||||
vendor_id: z.number().int(),
|
||||
product_id: z.number().int(),
|
||||
revision: z.number().int(),
|
||||
device_identification: deviceIdentification,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
Error: z.string(),
|
||||
});
|
||||
export const ethercatDevicesEventDataSchema = z
|
||||
.object({
|
||||
Initializing: z.boolean(),
|
||||
Done: ethercatDevicesSchema,
|
||||
Error: z.string(),
|
||||
})
|
||||
.check(rustEnum);
|
||||
|
||||
export type EthercatDevicesEventData = z.infer<
|
||||
typeof ethercatDevicesEventDataSchema
|
||||
@@ -62,10 +68,12 @@ export const machinesEventSchema = eventSchema(machinesEventDataSchema);
|
||||
export type MachinesEvent = z.infer<typeof machinesEventSchema>;
|
||||
|
||||
// Keep the EthercatInterfaceDiscovery event
|
||||
export const ethercatInterfaceDiscoveryEventDataSchema = rustEnumSchema({
|
||||
Discovering: z.boolean(),
|
||||
Done: z.string(),
|
||||
});
|
||||
export const ethercatInterfaceDiscoveryEventDataSchema = z
|
||||
.object({
|
||||
Discovering: z.boolean(),
|
||||
Done: z.string(),
|
||||
})
|
||||
.check(rustEnum);
|
||||
|
||||
export type EthercatInterfaceDiscoveryEventData = z.infer<
|
||||
typeof ethercatInterfaceDiscoveryEventDataSchema
|
||||
@@ -110,23 +118,19 @@ export function mainMessageHandler(
|
||||
try {
|
||||
// Apply appropriate caching strategy based on event type
|
||||
if (eventName === "EthercatDevicesEvent") {
|
||||
const validatedEvent = ethercatDevicesEventSchema.parse(event);
|
||||
console.log("EthercatDevicesEvent", validatedEvent);
|
||||
const validatedEvent = event;
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
ethercatDevices: validatedEvent,
|
||||
}));
|
||||
} else if (eventName === "MachinesEvent") {
|
||||
const validatedEvent = machinesEventSchema.parse(event);
|
||||
console.log("MachinesEvent", validatedEvent);
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
machines: validatedEvent,
|
||||
}));
|
||||
} else if (eventName === "EthercatInterfaceDiscoveryEvent") {
|
||||
const validatedEvent =
|
||||
ethercatInterfaceDiscoveryEventSchema.parse(event);
|
||||
console.log("EthercatInterfaceDiscoveryEvent", validatedEvent);
|
||||
const validatedEvent = event;
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
ethercatInterfaceDiscovery: validatedEvent,
|
||||
|
||||
@@ -8,8 +8,7 @@ import { z } from "zod";
|
||||
import { toastError, toastZodError } from "@/components/Toast";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { FPS_30 } from "@/lib/constants";
|
||||
import { mainNamespaceStore, createMainNamespaceStore } from "./mainNamespace";
|
||||
import { softReloadMainNamespaceStore } from "./softReload";
|
||||
import { mainNamespaceStore } from "./mainNamespace";
|
||||
|
||||
/**
|
||||
* Simple buffer-based store updater to limit React re-renders to ~30 FPS
|
||||
@@ -164,6 +163,7 @@ export const handleEventValidationError = (
|
||||
error: z.ZodError,
|
||||
eventName: string,
|
||||
) => {
|
||||
console.log(error);
|
||||
toastZodError(error, `Event Validation Error for ${eventName}`);
|
||||
throw new Error(`Event validation failed for ${eventName}`);
|
||||
};
|
||||
@@ -385,11 +385,11 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
},
|
||||
|
||||
decrementNamespace: (namespaceId: NamespaceId) => {
|
||||
const namespace_path = serializeNamespaceId(namespaceId);
|
||||
/*const namespace_path = serializeNamespaceId(namespaceId);
|
||||
|
||||
// check if the namespace exists
|
||||
const namespace = get().namespaces[namespace_path];
|
||||
/*if (namespace) {
|
||||
if (namespace) {
|
||||
set(
|
||||
produce((state: SocketioStore) => {
|
||||
// decrement the count
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { OutsideCorner } from "@/components/OutsideCorner";
|
||||
import { useMachines } from "@/client/useMachines";
|
||||
import { useOnSubpath } from "@/lib/useOnSubpath";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from "@tanstack/react-router";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { Icon, IconName } from "./Icon";
|
||||
import { useMainNamespace } from "@/client/mainNamespace";
|
||||
|
||||
type SidebarItemContent = {
|
||||
link: string;
|
||||
@@ -57,7 +63,51 @@ export function useSidebarlessWidth() {
|
||||
|
||||
export function SidebarLayout() {
|
||||
const machines = useMachines();
|
||||
const router = useRouter();
|
||||
const routerState = useRouterState();
|
||||
const { machines: machinesEvent } = useMainNamespace();
|
||||
const [contentWidth, setContentWidth] = React.useState<number>(0);
|
||||
|
||||
// Machine connection guard: redirect to setup if selected machine disconnects
|
||||
useEffect(() => {
|
||||
// Wait for machines data to be loaded
|
||||
if (!machinesEvent?.data) return;
|
||||
|
||||
// Extract serial from current path (e.g., /machines/laser1/12345/control)
|
||||
const pathMatch = routerState.location.pathname.match(
|
||||
/\/machines\/[^/]+\/(\d+)/,
|
||||
);
|
||||
if (!pathMatch) return; // Not on a machine page
|
||||
|
||||
// Safety check: ensure pathMatch has at least 2 elements (full match + capture group)
|
||||
if (pathMatch.length < 2) {
|
||||
console.warn("[ConnectionGuard] Invalid pathMatch structure:", pathMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse serial number with error handling
|
||||
const serialString = pathMatch[1];
|
||||
const serialNumber = parseInt(serialString, 10);
|
||||
|
||||
// Check if parseInt succeeded (NaN means parsing failed)
|
||||
if (isNaN(serialNumber)) {
|
||||
console.warn(
|
||||
`[ConnectionGuard] Failed to parse serial number from "${serialString}"`,
|
||||
{ pathname: routerState.location.pathname },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if machine still exists in the raw machines data
|
||||
const machineExists = machinesEvent.data.machines.some(
|
||||
(m) => m.machine_identification_unique.serial === serialNumber,
|
||||
);
|
||||
|
||||
if (!machineExists) {
|
||||
router.navigate({ to: "/_sidebar/setup/machines" });
|
||||
}
|
||||
}, [machines, machinesEvent, routerState.location.pathname, router]);
|
||||
|
||||
const items: SidebarItemContent[] = [
|
||||
...machines.map((machine) => ({
|
||||
link: `/_sidebar/machines/${machine.slug}/${machine.machine_identification_unique.serial}/control`,
|
||||
|
||||
@@ -80,7 +80,7 @@ export function GraphControls({
|
||||
)}
|
||||
|
||||
<TouchButton
|
||||
onClick={onSwitchToHistorical}
|
||||
onClick={() => onSwitchToHistorical("button")}
|
||||
variant="outline"
|
||||
className={`h-auto px-3 py-3 text-base font-medium transition-colors ${
|
||||
!isLiveMode
|
||||
@@ -197,7 +197,7 @@ export function FloatingControlPanel({
|
||||
)}
|
||||
|
||||
<TouchButton
|
||||
onClick={onSwitchToHistorical}
|
||||
onClick={() => onSwitchToHistorical("button")}
|
||||
variant="outline"
|
||||
className={`h-auto px-3 py-3 text-base font-medium transition-colors ${
|
||||
!isLiveMode
|
||||
|
||||
@@ -5,8 +5,10 @@ import {
|
||||
AnimationRefs,
|
||||
AnimationState,
|
||||
BigGraphProps,
|
||||
GraphLine,
|
||||
SeriesData,
|
||||
} from "./types";
|
||||
import { alignTargetSeriesToTimestamps } from "@/lib/timeseries";
|
||||
|
||||
export function useAnimationRefs(): AnimationRefs {
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
@@ -50,7 +52,9 @@ export function buildUPlotData(
|
||||
values: number[],
|
||||
realPointsCount: number | undefined,
|
||||
realPointsCountRef: React.RefObject<number>,
|
||||
config: { lines?: Array<{ show?: boolean; value: number }> },
|
||||
config: {
|
||||
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
|
||||
},
|
||||
allSeriesData?: number[][],
|
||||
): uPlot.AlignedData {
|
||||
const uData: uPlot.AlignedData = [timestamps];
|
||||
@@ -72,7 +76,14 @@ export function buildUPlotData(
|
||||
// Add config lines
|
||||
config.lines?.forEach((line) => {
|
||||
if (line.show !== false) {
|
||||
uData.push(timestamps.map(() => line.value));
|
||||
const lineData = line.targetSeries
|
||||
? alignTargetSeriesToTimestamps(
|
||||
line.targetSeries,
|
||||
timestamps,
|
||||
line.value,
|
||||
)
|
||||
: timestamps.map(() => line.value);
|
||||
uData.push(lineData);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +99,9 @@ export function animateNewPoint(
|
||||
viewMode: string,
|
||||
selectedTimeWindow: number | "all",
|
||||
startTimeRef: React.RefObject<number | null>,
|
||||
config: { lines?: Array<{ show?: boolean; value: number }> },
|
||||
config: {
|
||||
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
|
||||
},
|
||||
updateYAxisScale: (xMin?: number, xMax?: number) => void, // Updated signature
|
||||
getAllSeriesData?: () => number[][],
|
||||
): void {
|
||||
|
||||