Compare commits

..
Author SHA1 Message Date
just-some-entity 2110f42268 update main.rs 2026-01-27 16:02:45 +00:00
just-some-entity 5a50c3b3b9 add backend code from v1 2026-01-27 15:59:58 +00:00
just-some-entity d67863cedb update cargo.lock for machines 2026-01-27 15:50:21 +00:00
just-some-entity e3adeae9ac update cargo lock for dep 2026-01-27 15:48:53 +00:00
just-some-entity 625750f620 update cargo.Toml to include modbus and enum deps 2026-01-27 15:36:12 +00:00
just-some-entity 1dc782fe45 Revert "call npm init"
This reverts commit 127c55c1dc.
2026-01-27 15:27:57 +00:00
just-some-entity 127c55c1dc call npm init 2026-01-27 15:23:16 +00:00
just-some-entity 61f417735e add electron components and docs 2026-01-27 15:16:58 +00:00
82 changed files with 3606 additions and 1982 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
.png filter=lfs diff=lfs merge=lfs -text
.jpg filter=lfs diff=lfs merge=lfs -text
.jpeg filter=lfs diff=lfs merge=lfs -text
-40
View File
@@ -1,40 +0,0 @@
---
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).
-33
View File
@@ -1,33 +0,0 @@
---
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.
-80
View File
@@ -1,80 +0,0 @@
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 -2
View File
@@ -7,5 +7,4 @@ build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
.idea/
compile_metrics.csv
View File
Whitespace-only changes.
Generated
+174 -137
View File
@@ -127,16 +127,16 @@ dependencies = [
"futures-lite",
"parking",
"polling",
"rustix 1.1.2",
"rustix 1.1.3",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.1"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
@@ -169,7 +169,7 @@ dependencies = [
"cfg-if",
"event-listener",
"futures-lite",
"rustix 1.1.2",
"rustix 1.1.3",
]
[[package]]
@@ -184,7 +184,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
"rustix 1.1.2",
"rustix 1.1.3",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
@@ -266,9 +266,9 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
@@ -372,9 +372,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytemuck"
@@ -390,9 +390,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
dependencies = [
"serde",
]
@@ -405,9 +405,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.46"
version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -427,9 +427,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.42"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"num-traits",
@@ -465,18 +465,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.51"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.51"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstyle",
"clap_lex",
@@ -484,9 +484,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.6"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "colored"
@@ -580,9 +580,9 @@ dependencies = [
[[package]]
name = "crc"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
@@ -668,9 +668,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.9.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "deluxe"
@@ -705,7 +705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117"
dependencies = [
"deluxe-core",
"heck",
"heck 0.4.1",
"if_chain",
"proc-macro-crate",
"proc-macro2",
@@ -968,7 +968,8 @@ dependencies = [
[[package]]
name = "ethercrab"
version = "0.6.0"
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb13becfa15713adb09b27fdb74468b827cdc78aa99aea638a89c1b0db3ce94"
dependencies = [
"async-io",
"atomic-waker",
@@ -997,7 +998,8 @@ dependencies = [
[[package]]
name = "ethercrab-wire"
version = "0.2.0"
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15828cb020fb44c92ca67e1ee3c122c9621c2e00d383d066e488f0619023ba2"
dependencies = [
"ethercrab-wire-derive",
"heapless",
@@ -1006,7 +1008,8 @@ dependencies = [
[[package]]
name = "ethercrab-wire-derive"
version = "0.2.0"
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e790b8011211ed70babfcacbd4277b91ee21402ad5ba6a2c293db02e2f12919"
dependencies = [
"criterion",
"proc-macro2",
@@ -1052,9 +1055,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "fnv"
@@ -1167,9 +1170,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "h2"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
dependencies = [
"atomic-waker",
"bytes",
@@ -1220,9 +1223,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heapless"
@@ -1240,6 +1243,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
@@ -1248,12 +1257,11 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "http"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"fnv",
"itoa",
]
@@ -1336,9 +1344,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"bytes",
"futures-channel",
@@ -1349,7 +1357,7 @@ dependencies = [
"hyper",
"libc",
"pin-project-lite",
"socket2 0.6.1",
"socket2 0.6.2",
"tokio",
"tower-service",
"tracing",
@@ -1387,9 +1395,9 @@ checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb"
[[package]]
name = "indexmap"
version = "2.12.0"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
@@ -1463,15 +1471,15 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.82"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1485,9 +1493,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libloading"
@@ -1548,9 +1556,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.28"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mach2"
@@ -1574,11 +1582,14 @@ dependencies = [
"ethercrab",
"euclid",
"lazy_static",
"modbus",
"serde",
"serde_json",
"serialport",
"smol",
"socketioxide",
"strum",
"strum_macros 0.26.4",
"textplots",
"tonic",
"tracing",
@@ -1657,15 +1668,25 @@ checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536"
[[package]]
name = "mio"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "modbus"
version = "0.1.0"
source = "git+https://github.com/just-some-entity/modbus-rs.git#f1fff0ebd09334d6de29b2faa00336d031741dbb"
dependencies = [
"crc",
"heapless",
"strum_macros 0.27.2",
]
[[package]]
name = "nb"
version = "0.1.3"
@@ -1818,17 +1839,11 @@ dependencies = [
"windows-link",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pcap"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83cdabc34a80d9ec3563694cc31423fba6bb9bab4f31a9a5d5b85f29bd6d660a"
checksum = "bbd2eecc2ddc671ec563b5b39f846556aade68a65d1afb14d8fe6b30b0457d75"
dependencies = [
"bitflags 1.3.2",
"errno 0.2.8",
@@ -1847,9 +1862,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.3"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4"
checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7"
dependencies = [
"memchr",
"ucd-trie",
@@ -1857,9 +1872,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.8.3"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de"
checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed"
dependencies = [
"pest",
"pest_generator",
@@ -1867,9 +1882,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.8.3"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843"
checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5"
dependencies = [
"pest",
"pest_meta",
@@ -1880,9 +1895,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.8.3"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a"
checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365"
dependencies = [
"pest",
"sha2",
@@ -1979,7 +1994,7 @@ dependencies = [
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix 1.1.2",
"rustix 1.1.3",
"windows-sys 0.61.2",
]
@@ -2004,9 +2019,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -2069,9 +2084,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.3"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
@@ -2125,31 +2140,28 @@ dependencies = [
[[package]]
name = "rmp"
version = "0.8.14"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]]
name = "rustc-hash"
@@ -2172,9 +2184,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.10.0",
"errno 0.3.14",
@@ -2191,9 +2203,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "same-file"
@@ -2329,7 +2341,6 @@ dependencies = [
"smol",
"socketioxide",
"spin_sleep",
"ta",
"textplots",
"tokio",
"tower-http",
@@ -2379,10 +2390,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.6"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno 0.3.14",
"libc",
]
@@ -2451,9 +2463,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
@@ -2558,6 +2570,37 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.114",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -2586,12 +2629,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "ta"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "609409d472a0a7d8d4dd9e19891bbdef546b9dce670c3057d0e02192dc541226"
[[package]]
name = "tap"
version = "1.0.1"
@@ -2610,18 +2647,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -2664,15 +2701,15 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.48.0"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2 0.6.1",
"socket2 0.6.2",
"tokio-macros",
"windows-sys 0.61.2",
]
@@ -2690,9 +2727,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.17"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -2713,9 +2750,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.17"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
@@ -2772,9 +2809,9 @@ dependencies = [
[[package]]
name = "tower"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
@@ -2791,9 +2828,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.10.0",
"bytes",
@@ -2862,9 +2899,9 @@ dependencies = [
[[package]]
name = "tracing-journald"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657"
checksum = "2d3a81ed245bfb62592b1e2bc153e77656d94ee6a0497683a65a12ccaf2438d0"
dependencies = [
"libc",
"tracing-core",
@@ -2944,18 +2981,18 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unescaper"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26"
checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e"
dependencies = [
"thiserror",
]
[[package]]
name = "unicase"
version = "2.8.1"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
@@ -3039,18 +3076,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
@@ -3061,9 +3098,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.105"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3071,9 +3108,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.105"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -3084,9 +3121,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.105"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
@@ -3400,9 +3437,9 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.46.0"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "wyz"
@@ -3415,18 +3452,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.27"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
dependencies = [
"proc-macro2",
"quote",
@@ -3435,6 +3472,6 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.19"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
+84 -42
View File
@@ -2,67 +2,109 @@
# QiTech Control
QiTech Control is an open-source framework designed to bring modern software development practices to certified industrial hardware.
QiTech Control is an open-source framework designed to bring modern software paradigms to certified industrial hardware.
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.
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
## Documentation
**[View Full Documentation Wiki](https://github.com/qitechgmbh/control/wiki)**
[![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click here to watch a video demo of our software.*
- [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)
[![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)
*Click here to watch a full explainer Video of our Software.*
## Videos
# Repo Structure
| Software Demo | Full Explainer |
|---------------|----------------|
| [![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Click to watch a demo of the control software in action* | [![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)<br/>*Click to watch a complete overview of QiTech Control* |
**Frontend**
## Repository Structure
- `/electron`: Frontend code for the control software built with React and Electron.
- **`/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
**Backend**
## Technology Stack
- `/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.
**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
**Operating System**
**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
**Other**
- `/docs`: Documentation for the project.
# Technology Choices
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master software. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
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.
## Frontend
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).
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
[API Documentation](https://github.com/qitechgmbh/control/blob/master/docs/rest-api.md)
-> 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)
-> Frontend
- nodejs and npm installed
- run `cd electron && npm i && npm run start` to start the frontend
## Hardware Examples
## Minimal Hardware Examples
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.
For complete step-by-step tutorials on setting up your first hardware, including wiring diagrams and software configuration, see the [Getting Started Guide](./docs/developer-docs/getting-started.md#minimal-hardware-examples).
**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
| Example | Vendor | Hardware | Description | How-To Video | Docs |
| :----------------- | :------- | :------------ | :-------------------------------------- | :----------- | :---------------------------- |
| **Digital Input** | WAGO | 750-402 | 4-Channel Digital Input | [ ] | - |
| **Digital Output** | WAGO | 750-753 | 8-Channel Digital Output | [ ] | - |
| **Analog Input** | WAGO | 750-455 | 4-Channel Analog Input | [ ] | - |
| **Analog Output** | WAGO | 750-553 | 4-Channel Analog Output | [ ] | - |
| **Serial Comms** | WAGO | 750-652 | RS-485 Modbus Module | [ ] | - |
| **Stepper Drive** | WAGO | 750-67x | Stepper Controller | [ ] | - |
| **Power Supply** | WAGO | 2787-214 | Power Supply Unit | [ ] | - |
| **LED Control** | Beckhoff | EL2004 | Digital output control (simplest setup) | [ ] | [](./docs/hardware/el2004.md) |
| **Analog Input** | Beckhoff | EL3021 | Reading analog current measurements | [ ] | [](./docs/hardware/el3021.md) |
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)
# QiTech Machines
## QiTech Machines
| Machine Name | Description | Machine ID | Implemented | Video | Docs |
| :---------------- | :-------------------------- | :--------- | :---------- | :-------------------------------------------------------------- | :------------------------------ |
| Winder V1 | Winding Filaments & Similar | 1 | Legacy | [Video](https://youtu.be/4aE4cFhioKA?si=Xdj_LVnFrAYWnLm6) | - |
| Winder V2 | Winding Filaments & Similar | 2 | Yes | [Video](https://youtu.be/f2kzh6kpQWE?si=HEYcLOaC9gWSp2Wo) | [](./docs/machines/winder-1.md) |
| XL Winder V1 | Large Scale Winder | 3 | Yes | [Video](https://youtu.be/ynI6ioWIQQY?si=4EDVKXbgIqrvGGtY) | - |
| Buffer V1 | Filament buffering system | 4 | In Progress | [Video](https://youtu.be/VR5mEdZDPA0?si=eip7dQXU4zaXK7Ev) | - |
| Extruder V1 | Single Screw Extruder | 5 | Legacy | [Video](https://youtu.be/gchoG-yGczI?si=bEM4daf5eOVaU5_2) | - |
| Extruder V2 | Single Screw Extruder | 6 | Yes | [Video](https://youtu.be/mexRYGDNWa4?si=h7JBz_XKMwKLPenG) | |
| Waterway V1 | Filament Water Cooling | 7 | In Progress | [Video](https://youtu.be/_T5z1J8bl_k?si=uOC1hIQ1EAP0cHIF) | |
| Laser V1 | Diameter Measuring Laser | 8 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | |
| 2-Axis-Laser V1 | Dual Axis Laser | 9 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | - |
| Mock | Mock Machine for Testing | 10 | Yes | | - |
| Extruder V3 | Single Screw Extruder | 11 | Yes | [Video](https://youtu.be/ipHHuPzCvn4?si=hkZ1b93rVuwDxhFD) | |
| Mini Schredder V1 | mini plastic crusher | 12 | Legacy | [Video](https://youtu.be/m8NplNqdu2Q?si=x6zjDktJtpbSMSmu) | - |
| Pro Schredder V1 | large plastic crusher | 13 | Legacy | [Video](https://youtu.be/pSaVMqp06pU?si=y61enutRpxKscBm4) | - |
| Dryer V1 | polymer dryer | 14 | Reserved | [Video](https://youtu.be/6hdmUUAdZp0?si=eKOW1WlgkqTpnfdS) | - |
| Pelletizier V1 | Filament chopper | 15 | Reserved | | - |
QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
# Current Restructuring Efforts
**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
## Contributing
This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
## License
See [LICENSE](LICENSE) file for details.
[![](https://img.youtube.com/vi/UlbnSVIhfLI/maxresdefault.jpg)](https://www.youtube.com/live/UlbnSVIhfLI?si=ZTotC5B7gd87tUim)
*Video: Live stream of software dev meeting*
+2 -2
View File
@@ -9,11 +9,11 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
axum = "0.8.8"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serialport = "4.7.3"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

-1
View File
@@ -89,7 +89,6 @@ 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.
@@ -1,236 +0,0 @@
# 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.
![](../assets/wiring.png)
---
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:
![](../assets/UI_start_motor.png)
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.
![](../assets/machine_assigment_motor.png)
**NOW THE MOTOR SHOULD TURN!**
In the interface "TestMotor" on the left side, you can now control the motor (its state and its speed).
![](../assets/UI_motor.png)
---
## 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.
+344
View File
@@ -0,0 +1,344 @@
# Documentation for the US-3202510 220 V Single‑Phase to Three‑Phase 0.75 kW VFD Inverter
## 1. Specification, Features and Technical Data
### 1.1 Specification
- **Control Mode:** V/F Control
- **Nominal Voltage:** 220 V
- **Rated Power:** 750 W, 400 W, 200 W, 300 W
- **Input Voltage:** Single Phase 220 V
- **Output Voltage:** Three Phase 220 V
- **Dimensions:** 52.00 / 80.00 / 90.00 / 147.00 mm (reference)
---
### 1.2 Features
- Output frequency range: **1.0–99.0 Hz**, suitable for general speed control
- Built-in intelligent logic controller for simple logic control
- Compact size, easy installation, simple commissioning
- Clear and easy-to-understand parameters for general applications
- Equipped with a built-in speed potentiometer; external potentiometer supported
- Specifically designed for **220 V, ≤0.75 kW three-phase AC motors**
- Integrated solution for equipment manufacturers, reducing system cost and improving reliability
- Electronic thermal overload protection, replacing traditional motor protection devices
- External LED indicator output supported for on-site indication
- User-friendly interface with simple parameter setting
- Adjustable V/F curve for special applications
- Shift key allows viewing real-time parameters
- Uses a new-generation **IPM module** with comprehensive protection functions
---
### 1.3 Models and Technical Data
**Single-phase power supply: 200–240 VAC, 50/60 Hz**
| Inverter Model | Power Capacity (kVA) | Input Current (A) | Output Current (A) | Compatible Motor (kW) |
| ----------------| ----------------------| -------------------| --------------------| -----------------------|
| US00R2G1 | 3.0 | 3.2 | 1.6 | 0.2 |
| US00R4G1 | 3.8 | 4.3 | 2.1 | 0.4 |
| US00R7G1 | 5.6 | 6.0 | 3.1 | 0.75 |
---
### 1.4 Front Panel and Terminal Diagram
<img src="../assets/US-3202510_spec.png" alt="VFD Front Panel and Terminal Layout" style="max-width:600px;">
---
### 1.5 Dimensions
<img src="../assets/US-3202510_dimensions.png" alt="VFD Front Panel and Terminal Layout" style="max-width:600px;">
---
## 2. Terminals and Wiring
### 2.1 Main Terminal Wiring
| Terminal Mark | Name | Description |
| ---------------| --------------------------| --------------------------------------------------|
| AC, AC | Single-phase power input | Connection point for single-phase 220 V AC power |
| U, V, W | Inverter output | Connect to three-phase AC motor |
| FG | Ground terminal | Protective earth connection |
---
### 2.2 Signal Terminal Wiring
| Category | Terminal Symbol | Terminal Name | Function Description |
| ----------------| -----------------| ------------------------| ----------------------------------------------|
| Power | 5V, COM | External 5V power | Provides 5V power for external potentiometer |
| Analog input | VR, COM | Analog input | Input voltage range: DC 0–5 V |
| Digital input | M1, COM | Forward run | Forward rotation control |
| Digital input | M2 / D3, COM | Reverse / multi-speed | Reverse rotation or multi-speed stage 3 |
| Digital input | D2, COM | Multi-speed terminal 2 | Multi-speed function 2 |
| Digital input | D1, COM | Multi-speed terminal 1 | Multi-speed function 1 |
| Digital output | M0, 5V | Digital output | For external 5V relay or indicator |
| Communication | RS−, RS+ | RS485 interface | MODBUS control |
---
### 2.2.1 Signal Terminal Layout
| Index | Symbol |
| -------| --------|
| 1 | +5V |
| 2 | VR |
| 3 | COM |
| 4 | M0 |
| 5 | M1 |
| 6 | M2/D3 |
| 7 | RS485- |
| 8 | RS485+ |
| 9 | D2 |
| 10 | D1 |
---
## 3. Control Panel
### 3.1 Indicator Lights and Display
| Indicator | Function | Description |
| -----------| -------------------| -------------------------------------------------------------|
| POWER | Power indicator | Steady ON = power on; Red flashing = keypad locked |
| FWD | Forward indicator | Steady ON = motor running forward; Flashing = motor stopped |
| REV | Reverse indicator | Steady ON = motor running reverse; Flashing = motor stopped |
| Display | Display | Shows current parameters and fault information |
---
### 3.2 Physical Controls
| Key / Control | Type | Function (Run Mode) | Function (Parameter Setting Mode) |
| ---------------| ---------------| -----------------------------------------------------------| ---------------------------------------------------------------------------|
| P.K / SHIFT | Button | View IPM temperature, DC bus voltage/current, motor speed | Shift digit |
| MENU / ESC | Button | Enter parameter setting mode | Exit |
| SAVE / LOCK | Button | — | Long press to lock/unlock keypad; auto-lock after 3 minutes of inactivity |
| FWD / REV | Button | Toggle forward/reverse rotation | — |
| ↑ | Button | — | Increase value |
| ON / OFF | Button | Start/stop motor | Confirm value |
| ↓ | Button | — | Decrease value |
| SPEED | Potentiometer | Adjust motor speed | — |
---
## 4. Parameters
### 4.1 Editing Parameters
1. **Enter Edit Mode**
- Press the **MENU/ESC** key.
- The digital display will flash `-0.1-`.
2. **Select Function Code**
- Use the **↑/↓ keys** to adjust the value and choose the desired **function code**.
- Refer to **Table 4.1** below for available function codes.
3. **Set Sub-Code**
- During the setting process, use the **P.K/SHIFT** and **↑/↓ keys** to select the sub-code.
- Press **ON/OFF** to enter the sub-code selection.
- After selecting the sub-code, press **ON/OFF** again to return to the function code screen (display flashes `-X.X-`).
- Repeat for the next function code as needed.
4. **Save Settings**
- After all settings are selected, press the **SAVE/LOCK** key (display flashes `SAVE`).
- Press **SAVE/LOCK** again to confirm saving.
- When the display stops flashing, the data is saved.
5. **Operation After Saving**
- The VFD will run according to the saved parameters **without needing a power cycle**.
6. **Exit Without Saving**
- Press **MENU/ESC** to exit without saving — previous parameters remain unchanged.
- If no key is pressed for **20 seconds**, the interface will automatically return to the run screen.
---
### 4.2 Parameter Table
| Function Code | Definition | Factory Value | Setting Range | Description |
| ---------------| -------------------------------------------------| ---------------| ---------------| ---------------------------------------------------------------------------------------------------|
| 0.1 | Acceleration Time | 7 | 1–15 | 1–15 corresponds to 5–0.1 s; smaller value = faster (each step is about twice as fast) |
| 0.2 | Deceleration Time | 7 | 1–15 | 1–15 corresponds to 5–0.1 s; smaller value = faster (each step is about twice as fast) |
| 0.4 | V/F Frequency Compensation | 20 | 5–30 Hz | V/F frequency compensation |
| 0.5 | V/F Voltage Compensation | 41 | 25–85 | V/F voltage compensation |
| 0.7 | MODBUS Communication Baud Rate | 4 | 024–384 | 096 = 9600 bps |
| 0.9 | MODBUS Address | 1 | 1–249 | MODBUS address |
| 1.0 | Frequency Source Selection | 2 | 0–4 | 0: Panel keys, 1: Panel potentiometer, 2: External analog VR, 3: MODBUS 485, 4: Multi-speed input |
| 1.1 | Command Source Selection | 0 | 0–4 | 2: Power-on forward, 3: Power-on reverse, 4: External port, 0: free stop |
| 1.2 | Stop Mode | 1 | 0–2 | 1: Deceleration stop, 2: Immediate stop; 0: MI1 forward/stop, MI2 reverse/stop |
| 1.3 | MI Function Selection | 0 | 0–2 | 1: MI1 run/stop, MI2 reverse/forward; 2: MI1 run/stop, MI2 multi-speed |
| 1.4 | MO Function Selection | 0 | 0–2 | 1: Setting reached indicator, 2: Fault indicator |
| 1.6 | Overheat Protection Value | 90 | 40–100 °C | Overheat protection threshold |
| 1.7 | Maximum Operating Frequency | 50.0 | 1.0–99.0 Hz | Maximum frequency setting |
| 1.8 | Minimum Operating Frequency | 0 | 0.0–30.0 Hz | Minimum frequency setting |
| 1.9 | Rated Operating Frequency | 50.0 | 1.0–99.0 Hz | Default power-on frequency |
| 2.0 | Output Voltage Corresponding Frequency | 50.0 | 35–99.0 Hz | Output voltage corresponding frequency |
| 2.1 | Multi-speed 1 Set Frequency | 5.0 | 1.0–99.0 Hz | Multi-speed 1 frequency |
| 2.2 | Multi-speed 2 Set Frequency | 10.0 | 1.0–99.0 Hz | Multi-speed 2 frequency |
| 2.3 | Multi-speed 3 Set Frequency | 20.0 | 1.0–99.0 Hz | Multi-speed 3 frequency |
| 2.4 | Multi-speed 4 Set Frequency | 25.0 | 1.0–99.0 Hz | Multi-speed 4 frequency |
| 2.5 | Multi-speed 5 Set Frequency | 35.0 | 1.0–99.0 Hz | Multi-speed 5 frequency |
| 2.6 | Multi-speed 6 Set Frequency | 40.0 | 1.0–99.0 Hz | Multi-speed 6 frequency |
| 2.7 | Multi-speed 7 Set Frequency | 45.0 | 1.0–99.0 Hz | Multi-speed 7 frequency |
| 2.8 | Frequency Reached During Run | 45.0 | 1.0–99.0 Hz | Frequency reached during operation |
| 3.5 | Motor Pole Pairs | 2 | 1–6 | 2 corresponds to 1500 rpm |
| 3.7 | Motor Speed | 1500 | 1–9999 | Motor operating speed |
| 3.8 | Multi-speed 0 Set Frequency | 1.0 | 1.0–99.0 Hz | Multi-speed 0 frequency |
| 4.0 | Overcurrent Protection Value | 8.00 A | 2.00–9.00 A | Motor protection current setting |
| 4.1 | Overvoltage Protection Value | 520.0 V | 100–900 V | Motor protection voltage setting |
| 4.2 | Overload Protection Value | 2400 W | 400–2700 W | Motor protection power setting |
| 4.3 | Carrier Frequency Setting | 0 | 0–1 | 0: 10 kHz, 1: 20 kHz |
| 4.4 | Power-loss Detection Enable | 1 | 0–1 | 0: Off, 1: On; 0 = fan and motor start/stop together |
| 4.5 | Fan Operation Mode | 0 | 0–2 | 1: Fan stops 5 s after motor stops; 2: Fan runs continuously until power off |
| 4.6 | Motor Speed Synchronization Mode | 0–2 | 0–2 | 1: Motor speed sync within 300 ms after speed change; 2: Motor sync every 1 s |
| 5.0 | Bluetooth / RS485 / External Terminal Selection | 0–2 | 0–2 | 1: Serial = Bluetooth; 2: Serial = external expansion |
| 6.0 | Expansion Board – Year/Month | — | — | 0x23 = 2023, 0x10 = October |
| 6.1 | Expansion Board – Day/Version | — | — | 0x25 = 25th, 0x10 = version 1.0 |
| 6.2 | Expansion Board – Response Interval | 1000 | 100–5000 ms | Time interval for expansion board reporting |
| 6.3 | Expansion Board – Response Enable | 0–1 | 0–1 | 0: Do not report; 1: Enable reporting |
| 6.8 | Expansion Board – Voltage | — | — | Voltage reading, e.g., 503 = 5.03 V |
| 9.1 | Restore Factory Settings | — | — | Display flashes `CL`; press confirm to restore |
| 9.2 | Firmware Version | 107 | — | Display version number, e.g., V107 |
| 9.3 | Output Waveform Selection | 0–1 | 0–1 | 0: Standard sine; 1: Enhanced sine control waveform |
| 9.4 | MCU Reset | — | — | Display flashes 8.88; press confirm to reset MCU |
| 9.6 | Continuous Motor Speed Display | 0 | 0–1 | 0: Auto return to frequency display after inactivity; 1: auto return to speed display |
| 9.7 | Panel 3-Minute Lock Switch | 0 | 0–1 | 0: Auto lock after 3 minutes no operation; 1: No lock |
| 9.8 | Motor Speed Display Multiplier | 0 | 0–100 | 0: display off; other: divides speed, e.g., 4 → 1500 rpm becomes 375 rpm (requires 9.6 = 1) |
---
## 5. Fault Alarms and Countermeasures
The VFD has multiple warning indicators and protection functions.
When an abnormal fault occurs, the protection function activates and the VFD stops output. The display panel shows a fault code.
### Table 5.1 – Fault Codes
| Fault Code | Fault Name | Fault Description | Countermeasure |
| ------------| ------------------------------------| ----------------------------------------------------------------------------------------------------| ---------------------------------------------|
| E-0.1 | VFD Overheat | VFD temperature exceeds 1.6× set protection value; display flashes; max frequency limited to 30 Hz | After clearing the fault, restart the motor |
| E-0.2 | Pulse Overcurrent | VFD current exceeds 4.0× set protection value; motor stops | After clearing the fault, restart the motor |
| E-0.4 | VFD Overload | VFD voltage × current exceeds 4.2× set power; display flashes; operation continues | After clearing the fault, restart the motor |
| E-0.5 | Approaching IGBT Temperature Limit | VFD temperature >120°C, near IGBT limit; motor stops | After clearing the fault, restart the motor |
| E-0.6 | Temperature Sensor Open | VFD temperature sensor open; display flashes; operation continues | After clearing the fault, restart the motor |
| E-0.7 | Temperature Sensor Short | VFD temperature sensor shorted; display flashes; operation continues | After clearing the fault, restart the motor |
| E-0.8 | 100% Overload | VFD overload warning lasts 6 s; motor stops | After clearing the fault, restart the motor |
| E-0.9 | VFD Thermal Protection | Overheat protection warning >1 minute; motor stops | After clearing the fault, restart the motor |
| E-1.0 | Bus Overvoltage Protection | VFD voltage exceeds 4.1× set protection value; motor stops | After clearing the fault, restart the motor |
| E-1.1 | IGBT VF0 Protection | IGBT module VF0 fault; module overload; motor stops | After clearing the fault, restart the motor |
## 6. Multi-speed Settings
| D1 | D2 | D3 | Set Frequency | Corresponding Parameter |
| -----| -----| -----| ---------------| -------------------------|
| OFF | OFF | OFF | Multi-speed 0 | -3.8- |
| ON | OFF | OFF | Multi-speed 1 | -2.1- |
| OFF | ON | OFF | Multi-speed 2 | -2.2- |
| ON | ON | OFF | Multi-speed 3 | -2.3- |
| OFF | OFF | ON | Multi-speed 4 | -2.4- |
| ON | OFF | ON | Multi-speed 5 | -2.5- |
| OFF | ON | ON | Multi-speed 6 | -2.6- |
| ON | ON | ON | Multi-speed 7 | -2.7- |
---
## 7. Low-Frequency V/F Compensation
The VFD allows **low-frequency V/F compensation** to improve motor torque under load.
- Based on the connected load, you can set values in parameters `-0.3-`, `-0.4-`, and `-0.5-` according to the **linear V/F curve** (see Table 7.1).
- Low-frequency compensation **increases motor torque**.
- You must choose the **upper frequency limit for torque boosting**:
- Set the **maximum voltage ratio** in `-0.3-` and `-0.4-` for the desired frequency.
- Reference the corresponding or nearest frequency in Table 2.
**Behavior:**
- When operating **above this frequency**, the V/F slope increases → torque boost.
- When operating **below this frequency**, the V/F slope decreases → torque decreases.
**Example:**
- Parameter `-0.3-` = 20.0 Hz
- Parameter `-0.4-` = 60, 50, 30
- Parameter `-0.5-` default = 8
- The three V/F curves correspond to different voltage ratios at 20 Hz:
- Curve 1: 60% voltage ratio
- Curve 2: 55% voltage ratio
- Curve 3: 30% voltage ratio
This allows fine-tuning of torque at low speeds while keeping proper V/F control at higher frequencies.
---
### Table 7.1 – Linear V/F Curve Values for Low-Frequency Compensation
<img src="../assets/US-3202510_freq.png" alt="VFD Frequency Compensation Curve" style="max-width:600px;">
---
## 8. Modbus Interface
### 8.1 Internal Register List
- Communication format: MODBUS-RTU
- Data bits: 8
- Stop bits: 1
- Parity: None
- Default baud rate: 9600 bps
---
### 8.2 Holding Register Group (Read/Write)
| Hex Addr | Dec Addr | Data Type | Description | Notes |
| ---------:| ---------:| -----------------| ----------------------| ---------------------------------------|
| 0x0002 | 2 | Unsigned 16-bit | MODBUS set frequency | 500 / 10 = 50.0 Hz |
| 0x0003 | 3 | Unsigned 16-bit | MODBUS run command | 1: Forward, 2: Stop, 3: Reverse |
| 0x0004 | 4 | Unsigned 16-bit | Acceleration time | 1–15 → 5–0.1s (1 = 5s, 2 = 2.5s etc.) |
| 0x0005 | 5 | Unsigned 16-bit | Deceleration time | 1–15 → 5–0.1s (1 = 5s, 2 = 2.5s etc.) |
---
### 8.3 Input Registers (Read-only)
| Hex Addr | Dec Addr | Data Type | Description | Notes |
| ---------:| ---------:| -----------------| ---------------------------| -------------------------------|
| 0x0008 | 8 | Unsigned 16-bit | DC bus voltage | 3100 / 10 = 310.0 V |
| 0x0009 | 9 | Unsigned 16-bit | DC bus current | 132 / 100 = 1.32 A |
| 0x000A | 10 | Unsigned 16-bit | Drive temperature | 43 = 43 °C |
| 0x000B | 11 | Unsigned 16-bit | System status | 0: Idle, 1: Running, 2: Fault |
| 0x000C | 12 | Unsigned 16-bit | Error code | See error table below |
| 0x000D | 13 | Unsigned 16-bit | Current running frequency | 500 / 10 = 50.0 Hz |
---
### 8.4 Error Codes
| Code | Description |
| -----:| -------------------------------|
| 0 | No fault |
| 1 | Pulse overcurrent |
| 2 | IGBT overcurrent protection |
| 3 | DC bus overvoltage protection |
| 4 | Temperature near IGBT limit |
| 5 | Inverter thermal protection |
| 6 | Inverter overload 100% |
| 7 | Inverter power cut |
+241 -130
View File
@@ -1,12 +1,12 @@
{
"name": "qitech-control-electron",
"version": "2.4.1",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qitech-control-electron",
"version": "2.4.1",
"version": "1.0.0",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
@@ -40,12 +40,12 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^16.5.4",
"react-i18next": "^15.4.1",
"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.3",
"socket.io-client": "^4.8.1",
"socket.io-msgpack-parser": "^3.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
@@ -54,7 +54,7 @@
"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"
"zustand": "^5.0.8"
},
"devDependencies": {
"@electron/fuses": "^1.8.0",
@@ -73,7 +73,7 @@
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.4",
@@ -90,7 +90,7 @@
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^4.0.18"
"vitest": "^3.0.6"
}
},
"node_modules/@adobe/css-tools": {
@@ -1305,9 +1305,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2889,13 +2889,6 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
@@ -4123,40 +4116,39 @@
}
},
"node_modules/@vitest/expect": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.0.18",
"@vitest/utils": "4.0.18",
"chai": "^6.2.1",
"tinyrainbow": "^3.0.3"
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.0.18",
"@vitest/spy": "3.2.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
"magic-string": "^0.30.17"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0-0"
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
@@ -4168,41 +4160,42 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^3.0.3"
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.0.18",
"pathe": "^2.0.3"
"@vitest/utils": "3.2.4",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.18",
"magic-string": "^0.30.21",
"@vitest/pretty-format": "3.2.4",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
"funding": {
@@ -4210,24 +4203,28 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^4.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.18",
"tinyrainbow": "^3.0.3"
"@vitest/pretty-format": "3.2.4",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -4642,6 +4639,16 @@
"node": "*"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -4762,11 +4769,18 @@
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=18"
}
@@ -4828,6 +4842,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -5110,6 +5134,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -5778,9 +5812,9 @@
}
},
"node_modules/eslint": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5791,7 +5825,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.2",
"@eslint/js": "9.39.1",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -8035,6 +8069,13 @@
"loose-envify": "cli.js"
}
},
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -9191,17 +9232,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -9390,6 +9420,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -9746,17 +9786,16 @@
}
},
"node_modules/react-i18next": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz",
"integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"i18next": ">= 23.4.0",
"react": ">= 16.8.0",
"typescript": "^5"
},
@@ -10470,13 +10509,13 @@
"license": "ISC"
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
@@ -10484,6 +10523,23 @@
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-msgpack-parser": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/socket.io-msgpack-parser/-/socket.io-msgpack-parser-3.0.2.tgz",
@@ -10739,6 +10795,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/strip-literal/node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/style-to-js": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
@@ -10879,14 +10955,11 @@
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
@@ -10934,10 +11007,30 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11577,6 +11670,29 @@
}
}
},
"node_modules/vite-node": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.1",
"es-module-lexer": "^1.7.0",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-plugin-electron": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.29.0.tgz",
@@ -11645,50 +11761,51 @@
}
},
"node_modules/vitest": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
"@vitest/pretty-format": "4.0.18",
"@vitest/runner": "4.0.18",
"@vitest/snapshot": "4.0.18",
"@vitest/spy": "4.0.18",
"@vitest/utils": "4.0.18",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
"@vitest/mocker": "3.2.4",
"@vitest/pretty-format": "^3.2.4",
"@vitest/runner": "3.2.4",
"@vitest/snapshot": "3.2.4",
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^3.10.0",
"picomatch": "^4.0.2",
"std-env": "^3.9.0",
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3",
"vite": "^6.0.0 || ^7.0.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.14",
"tinypool": "^1.1.1",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.4",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.0.18",
"@vitest/browser-preview": "4.0.18",
"@vitest/browser-webdriverio": "4.0.18",
"@vitest/ui": "4.0.18",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.4",
"@vitest/ui": "3.2.4",
"happy-dom": "*",
"jsdom": "*"
},
@@ -11696,19 +11813,13 @@
"@edge-runtime/vm": {
"optional": true
},
"@opentelemetry/api": {
"@types/debug": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
@@ -12094,9 +12205,9 @@
}
},
"node_modules/zustand": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
"integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
+7 -7
View File
@@ -1,8 +1,8 @@
{
"name": "qitech-control-electron",
"productName": "QiTech Control",
"version": "2.4.1",
"description": "Next Generation Machine Interface ",
"version": "1.0.0",
"description": "",
"main": ".vite/build/main.js",
"private": true,
"scripts": {
@@ -36,7 +36,7 @@
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.4",
@@ -53,7 +53,7 @@
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^4.0.18"
"vitest": "^3.0.6"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
@@ -88,12 +88,12 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^16.5.4",
"react-i18next": "^15.4.1",
"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.3",
"socket.io-client": "^4.8.1",
"socket.io-msgpack-parser": "^3.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
@@ -102,6 +102,6 @@
"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"
"zustand": "^5.0.8"
}
}
@@ -14,7 +14,7 @@ import {
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
} from "../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
@@ -1,53 +0,0 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { useTestMotor } from "./useTestMotor";
export function TestMotorControlPage() {
const { state, setMotorOn, setVelocity } = useTestMotor();
// Fallback in case state is still null
const safeState = state ?? { motor_enabled: false, motor_velocity: 0 };
return (
<Page>
<ControlGrid columns={2}>
{/* Basic control */}
<ControlCard title="Motor Status">
{/* On/Off switch */}
<Label label="Power State">
<SelectionGroupBoolean
value={safeState.motor_enabled}
// Icon mapping for True/False
optionTrue={{ children: "Enabled", icon: "lu:Play" }}
optionFalse={{ children: "Disabled", icon: "lu:CirclePause" }}
onChange={(val) => setMotorOn(val)}
/>
</Label>
</ControlCard>
{/* Velocity */}
<ControlCard title="Settings">
{/* Velocity input with unit */}
<Label label="Target Velocity">
<EditValue
title="Velocity"
value={safeState.motor_velocity}
unit="rpm"
min={0}
max={1000} // Limit
step={1}
onChange={(val) => setVelocity(val)}
// Displays the value (as integer)
renderValue={(v) => v.toFixed(0)}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,22 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { testMotorSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMotorPage() {
// Access is allowed here because this function is called later
const { serial } = testMotorSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmotor/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -1,73 +0,0 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
// ========== Event Schema (Must match StateEvent) ==========
export const stateEventDataSchema = z.object({
motor_enabled: z.boolean(),
motor_velocity: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMotorNamespaceStore = {
state: StateEvent | null;
};
export const createTestMotorNamespaceStore =
(): StoreApi<TestMotorNamespaceStore> =>
create<TestMotorNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMotorMessageHandler(
chstore: StoreApi<TestMotorNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMotorNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (state: TestMotorNamespaceStore) => TestMotorNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useTestMotorNamespaceImplementation =
createNamespaceHookImplementation<TestMotorNamespaceStore>({
createStore: createTestMotorNamespaceStore,
createEventHandler: testMotorMessageHandler,
});
export function useTestMotorNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMotorNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMotorNamespaceImplementation(namespaceId);
}
@@ -1,96 +0,0 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMotorSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMotorNamespace, StateEvent } from "./testMotorNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { TestMotor } from "@/machines/properties";
import { z } from "zod";
export function useTestMotor() {
const { serial: serialString } = testMotorSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: TestMotor.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMotorNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
type: z.string(),
payload: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
// --- ACTIONS ---
const setMotorOn = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.motor_enabled = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
// Must exactly match the 'Mutation' enum!
data: { type: "SetMotorOn", payload: on },
}),
);
};
const setVelocity = (velocity: number) => {
updateStateOptimistically(
(current) => {
current.motor_velocity = velocity;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { type: "SetMotorVelocity", payload: velocity },
}),
);
};
return {
state: stateOptimistic.value,
setMotorOn,
setVelocity,
};
}
@@ -0,0 +1,117 @@
import React from "react";
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { usePellet1 as usePellet1 } from "./usePelletizer";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
import { roundToDecimals } from "@/lib/decimal";
import { Badge } from "@/components/ui/badge";
export function Pellet1ConfigPage() {
const {
//state
state,
defaultState,
//live values
frequency,
temperature,
voltage,
current,
// mutation functions
SetRunning,
SetDirection,
SetFrequencyTarget,
SetAccelerationLevel,
SetDecelerationLevel,
} = usePellet1();
// Extract values from consolidated state
const running = state?.inverter_state?.running ?? false;
const direction = state?.inverter_state?.direction ?? true;
const frequency_target = state?.inverter_state?.frequency_target ?? 0;
const acceleration_level = state?.inverter_state?.acceleration_level ?? 0;
const deceleration_level = state?.inverter_state?.deceleration_level ?? 0;
// Shared timeframe state (default 5 minutes)
const [timeframe, setTimeframe] = React.useState<number>(5 * 60 * 1000);
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Advanced Inverter Configuration">
<SelectionGroupBoolean
value={running}
optionTrue={{ children: "On" }}
optionFalse={{ children: "Off" }}
onChange={(v) => {
SetRunning(v)
}}
/>
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={direction}
optionTrue={{ children: "Forward" }}
optionFalse={{ children: "Backward" }}
onChange={(v) => {
SetDirection(v)
}}
/>
</Label>
<Label label="Set Target Frequency">
<EditValue
title="Set Target Frequency"
value={frequency_target}
unit="Hz"
step={0.1}
min={0}
max={50}
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
SetFrequencyTarget(val);
}}
defaultValue={5}
/>
</Label>
<Label label="Set Acceleration Level">
<EditValue
title="Set Acceleration Level"
value={acceleration_level}
unit={undefined}
step={1}
min={1}
max={15}
renderValue={(value) => value.toFixed(0)}
onChange={(val) => SetAccelerationLevel(val)}
defaultValue={7}
/>
</Label>
<Label label="Set Deceleration Level">
<EditValue
title="Set Deceleration Level"
value={deceleration_level}
unit={undefined}
step={1}
min={1}
max={15}
renderValue={(value) => value.toFixed(0)}
onChange={(val) => SetDecelerationLevel(val)}
defaultValue={7}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,123 @@
import React from "react";
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { usePellet1 as usePellet1 } from "./usePelletizer";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
import { roundToDecimals } from "@/lib/decimal";
import { Badge } from "@/components/ui/badge";
export function Pellet1ControlPage() {
const {
//state
state,
defaultState,
//live values
frequency,
temperature,
voltage,
current,
// mutation functions
SetRunning,
SetDirection,
SetFrequencyTarget,
SetAccelerationLevel,
SetDecelerationLevel,
} = usePellet1();
// Extract values from consolidated state
const running = state?.inverter_state?.running ?? false;
const direction = state?.inverter_state?.direction ?? true;
const frequency_target = state?.inverter_state?.frequency_target ?? 0;
const acceleration_level = state?.inverter_state?.acceleration_level ?? 0;
const deceleration_level = state?.inverter_state?.deceleration_level ?? 0;
// Shared timeframe state (default 5 minutes)
const [timeframe, setTimeframe] = React.useState<number>(5 * 60 * 1000);
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Motor Configuration">
<SelectionGroupBoolean
value={running}
optionTrue={{ children: "On" }}
optionFalse={{ children: "Off" }}
onChange={(v) => {
SetRunning(v)
}}
/>
<Label label="Speed">
<EditValue
title="Speed"
value={frequency_target * 2}
unit="%"
step={0.5}
min={0}
max={100}
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
SetFrequencyTarget(val / 2);
}}
defaultValue={5}
/>
</Label>
</ControlCard>
<ControlCard title="Inverter Status">
{
/*
<Badge
className={`text-md ${true ? "bg-white-600 border-green-600 text-green-600" : "bg-red-600 font-bold text-white"} mx-auto h-12 w-[100%] border-3 text-lg`}
>
{ true ? "Running" : "PULSE OVERCURRENT"}
</Badge>
*/
}
<TimeSeriesValueNumeric
label="Frequency"
unit="Hz"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={ frequency }
/>
<TimeSeriesValueNumeric
label="Temperature"
unit="C"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={ temperature }
/>
<TimeSeriesValueNumeric
label="Voltage"
unit="V"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={ voltage }
/>
<TimeSeriesValueNumeric
label="Current"
unit="A"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={ current }
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,168 @@
import { Page } from "@/components/Page";
import {
AutoSyncedBigGraph,
SyncedFloatingControlPanel,
useGraphSync,
type GraphConfig,
} from "@/components/graph";
import React from "react";
import { usePellet1 } from "./usePelletizer";
export function Pellet1GraphsPage() {
const { diameter, x_diameter, y_diameter, roundness, state } = usePellet1();
const syncHook = useGraphSync("diameter-roundness-group");
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
// Convert roundness from ratio (0-1) to percentage (0-100)
const roundnessPercent = React.useMemo(() => {
if (!roundness) return null;
const transformValue = (
val: { value: number; timestamp: number } | null,
) => {
if (!val) return null;
return { value: val.value * 100, timestamp: val.timestamp };
};
const transformSeries = (series: any) => {
if (!series || !series.values) return series;
return {
...series,
values: series.values.map(transformValue),
};
};
return {
current: roundness.current
? transformValue(roundness.current)
: null,
long: transformSeries(roundness.long),
short: transformSeries(roundness.short),
};
}, [roundness]);
const diameterColor = "#3b82f6"; // blue
const xDiameterColor = "#ef4444"; // red
const yDiameterColor = "#22c55e"; // green
const roundnessColor = "#eab308"; // yellow
// config for all graphs
const baseGraphConfig: GraphConfig = {
title: "",
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes Standard
colors: {
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Diameter-Graph (Diameter, X-Diameter, Y-Diameter)
const diameterGraphData = [
{
newData: diameter,
color: diameterColor,
title: "Diameter",
lines: [
// Tolerance for diameter
{
type: "threshold" as const,
value: targetDiameter + higherTolerance,
label: "Upper Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold" as const,
value: targetDiameter - lowerTolerance,
label: "Lower Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "target" as const,
value: targetDiameter,
label: "Target",
color: diameterColor,
},
],
},
...(isTwoAxis && x_diameter
? [
{
newData: x_diameter,
color: xDiameterColor,
title: "X-Diameter",
},
]
: []),
...(isTwoAxis && y_diameter
? [
{
newData: y_diameter,
color: yDiameterColor,
title: "Y-Diameter",
},
]
: []),
];
// Roundness-Graph
const roundnessGraphData = {
newData: roundnessPercent,
color: roundnessColor,
title: "Roundness (%)",
};
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
{/* Graph 1: Diameter */}
<AutoSyncedBigGraph
syncHook={syncHook}
newData={diameterGraphData}
unit="mm"
renderValue={(value) => value.toFixed(3)}
config={{
...baseGraphConfig,
title: "Diameter (mm)",
exportFilename: "diameter_data",
colors: {
...baseGraphConfig.colors,
primary: diameterColor,
},
}}
graphId="diameter-graph"
/>
{/* Graph 2: Roundness */}
{isTwoAxis && roundnessPercent && (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={roundnessGraphData}
unit="%"
renderValue={(value) => value.toFixed(2)}
config={{
...baseGraphConfig,
title: "Roundness (%)",
exportFilename: "roundness_data",
colors: {
...baseGraphConfig.colors,
primary: roundnessColor,
},
}}
graphId="roundness-graph"
/>
)}
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
@@ -0,0 +1,12 @@
import { Page } from "@/components/Page";
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
import React from "react";
import laserManualContent from "@/assets/markdown/laser/manual.md?raw";
export function Pellet1ManualPage() {
return (
<Page>
<MarkdownWithToc markdownContent={laserManualContent} />
</Page>
);
}
@@ -0,0 +1,50 @@
import { Topbar } from "@/components/Topbar";
import { pelletizer1SerialRoute } from "@/routes/routes";
import React from "react";
export function Pellet1Page() {
const { serial } = pelletizer1SerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/pelletizer1/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
{
link: "config",
activeLink: "config",
title: "Config",
icon: "lu:ChartSpline",
},
// {
// link: "graphs",
// activeLink: "graphs",
// title: "Graphs",
// icon: "lu:ChartSpline",
// },
// {
// link: "settings",
// activeLink: "settings",
// title: "Config",
// icon: "lu:ChartSpline",
// },
// {
// link: "manual",
// activeLink: "manual",
// title: "Manual",
// icon: "lu:BookOpen",
// },
// {
// link: "presets",
// activeLink: "presets",
// title: "Presets",
// icon: "lu:Save",
// },
]}
/>
);
}
@@ -0,0 +1,77 @@
import React from "react";
import { z } from "zod";
import { Preset } from "@/lib/preset/preset";
import { laser1 } from "@/machines/properties";
import { PresetPreviewEntries } from "@/components/preset/PresetPreviewTable";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { usePellet1 } from "./usePelletizer";
const laser1PresetDataSchema = z
.object({
targetDiameter: z.number(),
lowerTolerance: z.number(),
higherTolerance: z.number(),
})
.partial();
type Laser1 = z.infer<typeof laser1PresetDataSchema>;
const schemas = new Map([[1, laser1PresetDataSchema]]);
const previewEntries: PresetPreviewEntries<Laser1> = [
{
name: "Target Diameter",
unit: "mm",
renderValue: (data: Laser1) => data?.targetDiameter?.toFixed(3),
},
{
name: "Lower Tolerance",
unit: "mm",
renderValue: (data: Laser1) => data?.lowerTolerance?.toFixed(3),
},
{
name: "Higher Tolerance",
unit: "mm",
renderValue: (data: Laser1) => data?.higherTolerance?.toFixed(3),
},
];
export function Pellet1PresetsPage() {
const {
setTargetDiameter,
setLowerTolerance,
setHigherTolerance,
defaultState,
state,
} = usePellet1();
const applyPreset = (preset: Preset<Laser1>) => {
const targetDiameter = preset?.data?.targetDiameter ?? 1.75;
const lowerTolerance = preset?.data?.lowerTolerance ?? 0.05;
const higherTolerance = preset?.data?.higherTolerance ?? 0.05;
setTargetDiameter(targetDiameter);
setLowerTolerance(lowerTolerance);
setHigherTolerance(higherTolerance);
};
const toPresetData = (s: typeof state): Laser1 => ({
targetDiameter: s?.laser_state.target_diameter,
lowerTolerance: s?.laser_state.lower_tolerance,
higherTolerance: s?.laser_state.higher_tolerance,
});
return (
<PresetsPage
machine_identification={laser1.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
schemaVersion={1}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={toPresetData(defaultState)}
/>
);
}
@@ -0,0 +1,219 @@
/**
* @file pellet1Namespace.ts
* @description TypeScript implementation of Pellet1 namespace with Zod schema validation.
*/
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
TimeSeries,
TimeSeriesValue,
} from "@/lib/timeseries";
// ========== Event Schema Definitions ==========
/**
* Live values from Pellet (30 FPS)
*/
export const liveValuesEventDataSchema = z.object({
inverter_values: z.object({
frequency: z.number(),
temperature: z.number(),
voltage: z.number(),
current: z.number(),
})
});
/**
* State event from Pellet (on state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
inverter_state: z.object({
running: z.boolean(),
direction: z.boolean(),
frequency_target: z.number(),
acceleration_level: z.number(),
deceleration_level: z.number(),
}),
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
// ========== Type Inferences ==========
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
export type StateEvent = z.infer<typeof stateEventSchema>;
export type Pellet1NamespaceStore = {
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
frequency: TimeSeries;
temperature: TimeSeries;
voltage: TimeSeries;
current: TimeSeries;
};
// Constants for time durations
const HALF_SECOND = 500;
const { initialTimeSeries: frequency, insert: addFrequency } =
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
const { initialTimeSeries: temperature, insert: addTemperature } =
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
const { initialTimeSeries: voltage, insert: addVoltage } =
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
const { initialTimeSeries: current, insert: addCurrent } =
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
/**
* Factory function to create a new Pellet1 namespace store
* @returns A new Zustand store instance for Pellet1 namespace
*/
export const createPellet1NamespaceStore =
(): StoreApi<Pellet1NamespaceStore> =>
create<Pellet1NamespaceStore>(() => {
return {
state: null,
defaultState: null,
frequency,
temperature,
voltage,
current,
};
});
/**
* Creates a message handler for Pellet1 namespace events with validation and appropriate caching strategies
* @param store The store to update when messages are received
* @param throttledUpdater Throttled updater for batching updates at 30 FPS
* @returns A message handler function
*/
export function pellet1MessageHandler(
store: StoreApi<Pellet1NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Pellet1NamespaceStore>,
): EventHandler {
return (event: Event<any>) =>
{
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (updater: (state: Pellet1NamespaceStore) => Pellet1NamespaceStore) =>
{
throttledUpdater.updateWith(updater);
};
try
{
// Apply appropriate caching strategy based on event type
if (eventName === "StateEvent")
{
const stateEvent = stateEventSchema.parse(event);
console.log(stateEvent);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
}
// Live values events (keep for 1 hour)
else if (eventName === "LiveValuesEvent")
{
const liveValuesEvent = liveValuesEventSchema.parse(event);
const frequencyValue: TimeSeriesValue = {
value: liveValuesEvent.data.inverter_values.frequency,
timestamp: event.ts,
};
updateStore((state) => ({ ...state, frequency: addFrequency(state.frequency, frequencyValue) }));
const temperatureValue: TimeSeriesValue = {
value: liveValuesEvent.data.inverter_values.temperature,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
temperature: addTemperature(state.temperature, temperatureValue),
}));
const voltageValue: TimeSeriesValue = {
value: liveValuesEvent.data.inverter_values.voltage,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
voltage: addVoltage(state.voltage, voltageValue),
}));
const currentValue: TimeSeriesValue = {
value: liveValuesEvent.data.inverter_values.current,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
current: addCurrent(state.current, currentValue),
}));
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
console.error(
`Unexpected error processing ${eventName} event:`,
error,
);
throw error;
}
};
}
/**
* Create the Pellet1 namespace implementation
*/
const usePellet1NamespaceImplementation =
createNamespaceHookImplementation<Pellet1NamespaceStore>({
createStore: createPellet1NamespaceStore,
createEventHandler: pellet1MessageHandler,
});
export function usePellet1Namespace(
machine_identification_unique: MachineIdentificationUnique,
): Pellet1NamespaceStore {
// Generate namespace ID from validated machine ID
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
// Use the implementation with validated namespace ID
return usePellet1NamespaceImplementation(namespaceId);
}
@@ -0,0 +1,8 @@
import { z } from "zod";
export const liveValuesEventDataSchema = z.object({
frequency: z.number(),
temperature: z.number(),
voltage: z.number(),
current: z.number(),
});
@@ -0,0 +1,15 @@
import { z } from "zod";
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
inverter_state: z.object({
running_state: z.number(),
frequency_target: z.number(),
acceleration_level: z.number(),
deceleration_level: z.number(),
error_code: z.number().nullable(),
system_status: z.number(),
}),
});
@@ -0,0 +1,185 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { MachineIdentificationUnique } from "@/machines/types";
import { pelletizer } from "@/machines/properties";
import { pelletizer1SerialRoute } from "@/routes/routes";
import { z } from "zod";
import { usePellet1Namespace, StateEvent } from "./namespace";
import { useEffect, useMemo } from "react";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
function usePellet(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, frequency, temperature, voltage, current } =
usePellet1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => { if (state) { stateOptimistic.setReal(state); } }, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
};
// Mutation schemas
const schemaRunning = z.object({ SetRunning: z.boolean() });
const { request: requestRunning } = useMachineMutation(schemaRunning);
const schemaDirection = z.object({ SetDirection: z.boolean() });
const { request: requestDirection } = useMachineMutation(schemaDirection);
const schemaFrequencyTarget = z.object({ SetFrequencyTarget: z.number() });
const { request: requestFrequencyTarget } = useMachineMutation(schemaFrequencyTarget);
const schemaAccelerationLevel = z.object({ SetAccelerationLevel: z.number() });
const { request: requestAccelerationLevel } = useMachineMutation(schemaAccelerationLevel);
const schemaDecelerationLevel = z.object({ SetDecelerationLevel: z.number() });
const { request: requestDecelerationLevel } = useMachineMutation(schemaDecelerationLevel);
const SetRunning = (running: boolean) => {
updateStateOptimistically(
(current) => {
current.data.inverter_state.running = running;
},
() =>
requestRunning({
machine_identification_unique,
data: {
SetRunning: running,
},
}),
);
};
const SetDirection = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.inverter_state.direction = forward;
},
() =>
requestDirection({
machine_identification_unique,
data: {
SetDirection: forward,
},
}),
);
};
const SetFrequencyTarget = (frequency_target: number) =>
{
updateStateOptimistically(
(current) => {
current.data.inverter_state.frequency_target = frequency_target;
},
() =>
requestFrequencyTarget({
machine_identification_unique,
data: {
SetFrequencyTarget: frequency_target,
},
}),
);
};
const SetAccelerationLevel = (acceleration_level: number) => {
updateStateOptimistically(
(current) => {
current.data.inverter_state.acceleration_level = acceleration_level;
},
() =>
requestAccelerationLevel({
machine_identification_unique,
data: {
SetAccelerationLevel: acceleration_level,
},
}),
);
};
const SetDecelerationLevel = (deceleration_level: number) => {
updateStateOptimistically(
(current) => {
current.data.inverter_state.deceleration_level = deceleration_level;
},
() =>
requestDecelerationLevel({
machine_identification_unique,
data: {
SetDecelerationLevel: deceleration_level,
},
}),
);
};
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Live values (TimeSeries)
frequency,
temperature,
voltage,
current,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
// Action functions
SetRunning,
SetDirection,
SetFrequencyTarget,
SetAccelerationLevel,
SetDecelerationLevel,
};
}
export function usePellet1()
{
const { serial: serialString } = pelletizer1SerialRoute.useParams();
// Memoize the machine identification to keep it stable between renders
const machineIdentification: MachineIdentificationUnique = useMemo(() =>
{
const serial = parseInt(serialString); // Use 0 as fallback if NaN
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: {
vendor: 0,
machine: 0,
},
serial: 0,
};
}
return {
machine_identification: pelletizer.machine_identification,
serial,
};
}, [serialString]); // Only recreate when serialString changes
const pellet = usePellet(machineIdentification);
return { ...pellet };
}
+13 -36
View File
@@ -302,6 +302,18 @@ export const laser1: MachineProperties = {
device_roles: [],
};
export const pelletizer: MachineProperties = {
name: "Pelletizer",
version: "V1",
slug: "pelletizer1",
icon: "lu:CircleDotDashed",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x000b,
},
device_roles: [],
};
export const mock1: MachineProperties = {
name: "Mock",
version: "V1",
@@ -544,41 +556,6 @@ export const ip20TestMachine: MachineProperties = {
],
};
export const TestMotor: MachineProperties = {
name: "TestMotor",
version: "V1",
slug: "testmotor",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0011,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 0x2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "Stepper Motor",
allowed_devices: [
{
vendor_id: 0x2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
],
};
export const wagoAiTestMachine: MachineProperties = {
name: "Wago AI Test",
version: "V1",
@@ -608,6 +585,7 @@ export const machineProperties: MachineProperties[] = [
extruder3,
extruder2,
laser1,
pelletizer,
mock1,
buffer1,
wagoPower1,
@@ -617,7 +595,6 @@ export const machineProperties: MachineProperties[] = [
wagoAiTestMachine,
digitalInputTestMachine,
ip20TestMachine,
TestMotor,
];
export const getMachineProperties = (
@@ -75,7 +75,7 @@ function wagoPower1MessageHandler(
};
try {
if (eventName === "State") {
if (eventName === "StateEvent") {
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => ({
@@ -86,7 +86,7 @@ function wagoPower1MessageHandler(
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValues") {
} else if (eventName === "LiveValuesEvent") {
console.log(event);
const liveValues = liveValuesEventSchema.parse(event);
const { voltage, current } = liveValues.data;
+45 -16
View File
@@ -43,6 +43,12 @@ import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { Pellet1Page } from "@/machines/pelletizer/pelletizer1/Page";
import { Pellet1ControlPage } from "@/machines/pelletizer/pelletizer1/ControlPage";
import { Pellet1GraphsPage } from "@/machines/pelletizer/pelletizer1/Graph";
import { Pellet1ConfigPage } from "@/machines/pelletizer/pelletizer1/ConfigPage";
import { Pellet1PresetsPage } from "@/machines/pelletizer/pelletizer1/PresetsPage";
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/mock/mock1/Mock1Manual";
@@ -67,8 +73,6 @@ import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestm
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/motor_test_machine/TestMotorControlPage";
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
@@ -144,18 +148,6 @@ export const ip20TestMachineControlRoute = createRoute({
component: () => <IP20TestMachineControlPage />,
});
export const testMotorSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmotor/$serial",
component: () => <TestMotorPage />,
});
export const testMotorControlRoute = createRoute({
getParentRoute: () => testMotorSerialRoute,
path: "control",
component: () => <TestMotorControlPage />,
});
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
path: "_sidebar",
@@ -299,6 +291,38 @@ export const laser1PresetsRoute = createRoute({
component: () => <Laser1PresetsPage />,
});
// --- pelletizer
export const pelletizer1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "pelletizer1/$serial",
component: () => <Pellet1Page />,
});
export const pelletizer1ControlRoute = createRoute({
getParentRoute: () => pelletizer1SerialRoute,
path: "control",
component: () => <Pellet1ControlPage />,
});
export const pelletizer1ConfigRoute = createRoute({
getParentRoute: () => pelletizer1SerialRoute,
path: "config",
component: () => <Pellet1ConfigPage />,
});
export const pelletizer1GraphsRoute = createRoute({
getParentRoute: () => pelletizer1SerialRoute,
path: "graphs",
component: () => <Pellet1GraphsPage />,
});
export const pelletizer1PresetsRoute = createRoute({
getParentRoute: () => pelletizer1SerialRoute,
path: "presets",
component: () => <Pellet1PresetsPage />,
});
// --- mock
export const mock1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "mock1/$serial",
@@ -485,6 +509,13 @@ export const rootTree = RootRoute.addChildren([
laser1PresetsRoute,
]),
pelletizer1SerialRoute.addChildren([
pelletizer1ControlRoute,
pelletizer1GraphsRoute,
pelletizer1ConfigRoute,
pelletizer1PresetsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
analogInputTestMachineSerialRoute.addChildren([
@@ -499,8 +530,6 @@ export const rootTree = RootRoute.addChildren([
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
testMotorSerialRoute.addChildren([testMotorControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
aquapath1GraphRoute,
+1 -1
View File
@@ -8,7 +8,7 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
units = { path = "../units" }
bitvec = { version = "1.0.1", features = ["alloc"] }
+5
View File
@@ -112,6 +112,11 @@ impl EL4002 {
.write_channel_config(subdevice, 0x8010)
.await?;
self.configuration
.pdo_assignment
.txpdo_assignment()
.write_config(subdevice)
.await?;
self.configuration
.pdo_assignment
.rxpdo_assignment()
+1 -1
View File
@@ -180,6 +180,7 @@ impl Configuration for EL5152Configuration {
) -> Result<(), anyhow::Error> {
// Configure channel 1
self.channel1.write_channel_config(device, 0x8000).await?;
// Configure channel 2
self.channel2.write_channel_config(device, 0x8010).await?;
// Write PDO assignments
@@ -303,7 +304,6 @@ impl EL5152ChannelConfiguration {
device
.sdo_write(base_index, 0x11, self.frequency_window)
.await?;
device
.sdo_write(base_index, 0x13, self.frequency_scaling)
.await?;
@@ -351,7 +351,6 @@ pub enum EL7031_0030AnalogInputPort {
pub const EL7031_0030_VENDOR_ID: u32 = 0x2;
pub const EL7031_0030_PRODUCT_ID: u32 = 0x1b773052;
pub const EL7031_0030_REVISION_A: u32 = 0x10001E;
pub const EL7031_0030_IDENTITY_A: SubDeviceIdentityTuple = (
EL7031_0030_VENDOR_ID,
EL7031_0030_PRODUCT_ID,
+1 -2
View File
@@ -56,9 +56,8 @@ use std::{any::Any, fmt::Debug, sync::Arc};
use wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
use wago_modules::ip20_ec_di8_do8::{IP20_EC_DI8_DO8_IDENTITY, IP20EcDi8Do8};
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Module {
pub name: String,
pub slot: u16,
pub belongs_to_addr: u16,
pub has_tx: bool,
+52 -117
View File
@@ -2,9 +2,6 @@ use super::{
EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed, NewEthercatDevice,
SubDeviceIdentityTuple,
};
use crate::devices::wago_modules::wago_750_430::{
WAGO_750_430_MODULE_IDENT, WAGO_750_430_PRODUCT_ID,
};
use crate::devices::wago_modules::*;
use crate::{
devices::{
@@ -23,25 +20,24 @@ use crate::{
use anyhow::Error;
use smol::lock::RwLock;
use std::sync::Arc;
const MODULE_COUNT_INDEX: (u16, u8) = (0xf050, 0x00);
const TX_MAPPING_INDEX: (u16, u8) = (0x1c13, 0x00);
const RX_MAPPING_INDEX: (u16, u8) = (0x1c12, 0x00);
#[derive(Clone, Debug)]
struct ModulePdoMapping {
pub offset: usize,
pub module_i: u32,
}
// For both the rx and tx The Wago Coupler has 4 bytes, which we dont care about and skip
/// Wago750_354 bus coupler
/*
The "Modules" simply write at an offset into rx and read at an offset in tx
*/
pub struct Wago750_354 {
is_used: bool,
pub slots: [Option<Module>; 64],
pub slot_devices: [Option<Arc<RwLock<dyn DynamicEthercatDevice>>>; 64],
pub dev_count: usize,
pub module_count: usize,
rx_pdo_mappings: Vec<ModulePdoMapping>,
tx_pdo_mappings: Vec<ModulePdoMapping>,
rx_offsets: Vec<usize>,
tx_offsets: Vec<usize>,
tx_size: usize,
rx_size: usize,
}
@@ -109,7 +105,7 @@ impl EthercatDevice for Wago750_354 {
}
fn set_module(&mut self, module: Module) {
self.slots[self.module_count] = Some(module.clone());
self.slots[self.module_count] = Some(module);
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
@@ -138,8 +134,8 @@ impl NewEthercatDevice for Wago750_354 {
dev_count: 0,
tx_size: 0,
rx_size: 0,
rx_pdo_mappings: vec![],
tx_pdo_mappings: vec![],
rx_offsets: vec![],
tx_offsets: vec![],
}
}
}
@@ -151,76 +147,39 @@ impl std::fmt::Debug for Wago750_354 {
}
impl Wago750_354 {
pub fn calculate_module_index(pdo_mapping: u32, is_tx: bool) -> u32 {
let start_index = match is_tx {
true => 0x6000 as u32,
false => 0x7000 as u32,
};
let index_in_hex = ((pdo_mapping & 0xFFFF0000) >> 16) - start_index;
if index_in_hex < 16 {
return 0;
} else {
return index_in_hex / 16;
}
}
pub async fn get_pdo_offsets<'a>(
&mut self,
device: &EthercrabSubDevicePreoperational<'a>,
get_tx: bool,
) -> Result<(), Error> {
let mut vec: Vec<ModulePdoMapping> = vec![];
let mut vec: Vec<usize> = vec![];
let mut bit_offset = 0;
let mut module_i;
let start_subindex = 0x2;
let start_subindex = 0x1;
let index = match get_tx {
true => (TX_MAPPING_INDEX.0, TX_MAPPING_INDEX.1),
false => (RX_MAPPING_INDEX.0, RX_MAPPING_INDEX.1),
};
let count = device.sdo_read::<u8>(index.0, index.1).await?;
let count_mappings = device.sdo_read::<u8>(index.0, index.1).await?;
let pdo_index = device.sdo_read::<u16>(index.0, 1).await?;
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
for i in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + i).await?;
let bit_length = (pdo_mapping & 0xFF) as u8;
bit_offset += bit_length as usize;
}
let mut mappings_without_coupler: Vec<u32> = vec![];
for i in start_subindex..count_mappings {
let pdo_index = device.sdo_read(index.0, i).await?;
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
for j in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + j).await?;
mappings_without_coupler.push(pdo_mapping);
for i in 0..count {
vec.push(bit_offset);
let pdo_index = device.sdo_read(index.0, start_subindex + i).await?;
if pdo_index != 0 {
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
// Iterate over every PDO Mapped entry and add it to the cumulative bit offset
for j in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + j).await?;
// We only need / Want the bit len, which we extract with a bitmask extracting the lsb
let bit_length = (pdo_mapping & 0xFF) as u8;
bit_offset += bit_length as usize;
}
}
}
mappings_without_coupler.sort();
for pdo_mapping in mappings_without_coupler {
module_i = Wago750_354::calculate_module_index(pdo_mapping, get_tx);
let bit_length = (pdo_mapping & 0xFF) as u8;
if module_i < 64 {
vec.push(ModulePdoMapping {
offset: bit_offset,
module_i,
});
}
bit_offset += bit_length as usize;
}
vec.sort_by_key(|e| (e.module_i, e.offset));
// deduplicate by module_i, so we only have the offset to the start of inputs/outputs
vec.dedup_by(|a, b| a.module_i == b.module_i);
if get_tx {
self.tx_pdo_mappings = vec;
self.tx_offsets = vec;
} else {
self.rx_pdo_mappings = vec;
self.rx_offsets = vec;
}
Ok(())
}
@@ -263,7 +222,6 @@ impl Wago750_354 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -271,37 +229,26 @@ impl Wago750_354 {
WAGO_750_1506_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-1506".to_string();
}
WAGO_750_455_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
module.name = "750-455".to_string();
}
WAGO_750_501_PRODUCT_ID => {
module.has_tx = false;
module.has_rx = true;
module.name = "750-501".to_string();
}
WAGO_750_530_PRODUCT_ID => {
module.has_tx = false;
module.has_rx = true;
module.name = "750-530".to_string();
}
WAGO_750_652_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
module.name = "750-652".to_string();
}
WAGO_750_402_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
module.name = "750-402".to_string();
}
WAGO_750_430_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
module.name = "750-430".to_string();
}
_ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}",
@@ -319,42 +266,17 @@ impl Wago750_354 {
if self.dev_count != 0 {
return;
}
let mut tx_index = 1;
let mut rx_index = 1;
smol::block_on(async {
let _ = self.get_pdo_offsets(device, true).await;
let _ = self.get_pdo_offsets(device, false).await;
});
for module in &mut self.slots {
match module {
Some(m) => {
let tx_pdo_mapping = self
.tx_pdo_mappings
.iter()
.find(|map| map.module_i == m.slot.into());
if m.has_tx {
m.tx_offset = match tx_pdo_mapping {
Some(map) => map.offset,
None => 0,
}
}
println!("{:?}\n\n{:?}", self.tx_offsets, self.rx_offsets);
let rx_pdo_mapping = self
.rx_pdo_mappings
.iter()
.find(|map| map.module_i == m.slot.into());
if m.has_rx {
m.rx_offset = match rx_pdo_mapping {
Some(map) => map.offset,
None => 0,
}
}
}
None => break,
}
}
for module in &self.slots {
for module in self.slots {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
@@ -380,10 +302,6 @@ impl Wago750_354 {
WAGO_750_402_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
}
WAGO_750_430_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_430::Wago750_430::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -394,10 +312,27 @@ impl Wago750_354 {
return;
}
};
let tx_pdo_offset = self.tx_offsets.get(tx_index as usize);
if m.has_tx {
tx_index += 1;
}
let rx_pdo_offset = self.rx_offsets.get(rx_index as usize);
if m.has_rx {
rx_index += 1;
}
let mut dev_guard = dev.write_blocking();
//println!("For {:?} setting tx: {} rx: {}",m.name,m.tx_offset,m.rx_offset);
dev_guard.set_tx_offset(m.tx_offset);
dev_guard.set_rx_offset(m.rx_offset);
match tx_pdo_offset {
Some(offset) => dev_guard.set_tx_offset(*offset),
None => (),
}
match rx_pdo_offset {
Some(offset) => dev_guard.set_rx_offset(*offset),
None => (),
}
drop(dev_guard);
self.slot_devices[self.dev_count] = Some(dev);
self.dev_count += 1;
@@ -214,7 +214,7 @@ impl EthercatDevice for IP20EcDi8Do8 {
}
fn set_module(&mut self, module: Module) {
self.slots[self.module_count] = Some(module.clone());
self.slots[self.module_count] = Some(module);
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
@@ -378,7 +378,6 @@ impl IP20EcDi8Do8 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -416,7 +415,7 @@ impl IP20EcDi8Do8 {
let _ = self.get_pdo_offsets(device, false).await;
});
for module in &self.slots {
for module in self.slots {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
@@ -1,7 +1,6 @@
pub mod ip20_ec_di8_do8;
pub mod wago_750_1506;
pub mod wago_750_402;
pub mod wago_750_430;
pub mod wago_750_455;
pub mod wago_750_501;
pub mod wago_750_530;
@@ -1,211 +0,0 @@
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
SubDeviceProductTuple,
};
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput};
#[derive(Clone)]
pub struct Wago750_430 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub txpdo: Wago750_430TxPdo,
module: Option<Module>,
}
#[derive(Debug, Clone)]
pub enum Wago750_430Port {
Port1,
Port2,
Port3,
Port4,
Port5,
Port6,
Port7,
Port8,
}
impl From<Wago750_430Port> for usize {
fn from(value: Wago750_430Port) -> Self {
match value {
Wago750_430Port::Port1 => 0,
Wago750_430Port::Port2 => 1,
Wago750_430Port::Port3 => 2,
Wago750_430Port::Port4 => 3,
Wago750_430Port::Port5 => 4,
Wago750_430Port::Port6 => 5,
Wago750_430Port::Port7 => 6,
Wago750_430Port::Port8 => 7,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_430TxPdo {
pub port1: bool,
pub port2: bool,
pub port3: bool,
pub port4: bool,
pub port5: bool,
pub port6: bool,
pub port7: bool,
pub port8: bool,
}
impl DigitalInputDevice<Wago750_430Port> for Wago750_430 {
fn get_input(
&self,
port: Wago750_430Port,
) -> Result<crate::io::digital_input::DigitalInputInput, anyhow::Error> {
match port {
Wago750_430Port::Port1 => Ok(DigitalInputInput {
value: self.txpdo.port1,
}),
Wago750_430Port::Port2 => Ok(DigitalInputInput {
value: self.txpdo.port2,
}),
Wago750_430Port::Port3 => Ok(DigitalInputInput {
value: self.txpdo.port3,
}),
Wago750_430Port::Port4 => Ok(DigitalInputInput {
value: self.txpdo.port4,
}),
Wago750_430Port::Port5 => Ok(DigitalInputInput {
value: self.txpdo.port5,
}),
Wago750_430Port::Port6 => Ok(DigitalInputInput {
value: self.txpdo.port6,
}),
Wago750_430Port::Port7 => Ok(DigitalInputInput {
value: self.txpdo.port7,
}),
Wago750_430Port::Port8 => Ok(DigitalInputInput {
value: self.txpdo.port8,
}),
}
}
}
impl EthercatDeviceUsed for Wago750_430 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_430 {}
impl EthercatDynamicPDO for Wago750_430 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
}
fn get_rx_offset(&self) -> usize {
self.rx_bit_offset
}
fn set_tx_offset(&mut self, offset: usize) {
self.tx_bit_offset = offset
}
fn set_rx_offset(&mut self, offset: usize) {
self.rx_bit_offset = offset
}
}
impl EthercatDevice for Wago750_430 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
self.txpdo.port1 = input[0];
self.txpdo.port2 = input[1];
self.txpdo.port3 = input[2];
self.txpdo.port4 = input[3];
self.txpdo.port5 = input[4];
self.txpdo.port6 = input[5];
self.txpdo.port7 = input[6];
self.txpdo.port8 = input[7];
Ok(())
}
fn input_len(&self) -> usize {
8
}
fn output(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_len(&self) -> usize {
0
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_checked(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn get_module(&self) -> Option<crate::devices::Module> {
self.module.clone()
}
fn set_module(&mut self, module: crate::devices::Module) {
self.tx_bit_offset = module.tx_offset;
self.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl EthercatDeviceProcessing for Wago750_430 {}
impl NewEthercatDevice for Wago750_430 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
txpdo: Wago750_430TxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_430 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_430")
}
}
pub const WAGO_750_430_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_430_PRODUCT_ID: u32 = 2147483777;
pub const WAGO_750_430_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_430_VENDOR_ID, WAGO_750_430_PRODUCT_ID);
@@ -148,7 +148,8 @@ impl EthercatDevice for Wago750_652 {
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.rx_bit_offset;
let base = 32;
let mut ol0_i = 5; // Ol0 starts at bit 5 and goes to bit 7 (inclusive)
let mut ol1_i = 8;
@@ -173,10 +174,12 @@ impl EthercatDevice for Wago750_652 {
);
ol1_i += 1;
}
println!("{}", self.rx_pdo.control.output_length);
let data_start = base + 16;
for (i, &byte) in self.rx_pdo.out_buffer.iter().take(22).enumerate() {
output[(data_start + i * 8)..(data_start + (i + 1) * 8)].store_le(byte);
}
//println!("{:?}",self.rx_pdo.out_buffer);
Ok(())
}
+13
View File
@@ -0,0 +1,13 @@
# Git Hooks
This directory contains Git hooks for the project.
## Installation
To install the hooks, run the following command from the repository root:
```bash
# Copy the pre-commit hook to your local .git/hooks directory
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
#
# Pre-commit hook to generate Rust documentation
#
# This hook runs the rustdoc.sh script to update documentation
# before each commit.
echo "Running rustdoc generation..."
# Get the repository root
REPO_ROOT="$(git rev-parse --show-toplevel)"
# iteralte all directories thst contain a Cargo.toml file
for dir in $(find "$REPO_ROOT" -name "Cargo.toml" -exec dirname {} \;); do
echo "Generating documentation in $dir"
# cargotoml conatains workspace members, skip this directory
# HAS_MEMBERS=$(grep -c '\[workspace\]' "$dir/Cargo.toml")
# if [ "$HAS_MEMBERS" -gt 0 ]; then
# echo "Skipping workspace directory $dir"
# continue
# fi
# get package name from Cargo.toml using cargo metadata
PACKAGE_NAME=$(cargo metadata --format-version 1 --no-deps --manifest-path "$dir/Cargo.toml" | jq -r '.packages[0].name')
if [ -z "$PACKAGE_NAME" ]; then
echo "Could not determine package name for $dir"
continue
fi
# go into the directory
cd "$dir" || continue
# generate the documentation using rustdoc-md
cargo install rustdoc-md --git https://github.com/qitechgmbh/rustdoc-md --rev 6dc34f9c9d878799d9c1b4f2d2bcbb1f2757ede7
RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS="-Z unstable-options --output-format json" cargo doc --no-deps
rustdoc-md --path "${REPO_ROOT}/target/doc/${PACKAGE_NAME}.json" --output ./rustdoc.md
# stage the generated documentation file
git add "rustdoc.md"
# go back to the repository root
cd "$REPO_ROOT" || exit
done
git rm --cached rustdoc.md 2>/dev/null
rm rustdoc.md
exit 0
+10 -3
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
control_core_derive = { path = "../control-core-derive" }
@@ -15,13 +15,13 @@ anyhow = "1.0.100"
bitvec = "1.0.1"
lazy_static = "1.5.0"
euclid = "0.22.13"
euclid = "0.22.11"
# concurrency
smol = "2.0.2"
# web
serde_json = "1.0.149"
serde_json = "1.0.143"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
# serial
@@ -31,6 +31,13 @@ serialport = "4.7.3"
tracing = { version = "0.1.44", features = ["attributes"] }
tonic = { version = "0.13.1", optional = true }
# modbus interfaces
modbus = { git = "https://github.com/just-some-entity/modbus-rs.git" }
# enum stuff
strum = "0.26"
strum_macros = "0.26"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8.7"
+3 -7
View File
@@ -1,6 +1,7 @@
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use super::{
@@ -91,12 +92,7 @@ impl MachineNewTrait for AquaPathV1 {
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
..Default::default()
};
let subdevice =
get_ethercat_device::<EL5152>(hardware, params, 4, [EL5152_IDENTITY_A].to_vec())
.await?
.1;
let subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
el5152
.write()
.await
+4 -4
View File
@@ -159,7 +159,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_1,
Heating::default(),
Duration::from_millis(500),
900.0,
700.0,
1.0,
);
@@ -174,7 +174,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_2,
Heating::default(),
Duration::from_millis(500),
900.0,
700.0,
1.0,
);
@@ -189,7 +189,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_3,
Heating::default(),
Duration::from_millis(500),
900.0,
700.0,
1.0,
);
@@ -204,7 +204,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_4,
Heating::default(),
Duration::from_millis(500),
150.0,
200.0,
0.95,
);
+2 -3
View File
@@ -26,9 +26,9 @@ pub mod extruder1;
pub mod extruder2;
pub mod ip20_test_machine;
pub mod laser;
pub mod pelletizer;
pub mod machine_identification;
pub mod mock;
pub mod motor_test_machine;
pub mod registry;
pub mod serial;
pub mod test_machine;
@@ -40,6 +40,7 @@ pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_PELLETIZER: u16 = 0x000B;
pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
@@ -51,8 +52,6 @@ pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
use serde_json::Value;
use smol::lock::RwLock;
+2
View File
@@ -54,6 +54,7 @@ impl MachineIdentification {
x if x == MACHINE_WINDER_V1 => "winder_v1".to_string(),
x if x == MACHINE_EXTRUDER_V1 => "extruder_v1".to_string(),
x if x == MACHINE_LASER_V1 => "laser_v1".to_string(),
x if x == MACHINE_PELLETIZER => "pelletizer_v1".to_string(),
x if x == MACHINE_MOCK => "mock".to_string(),
x if x == MACHINE_AQUAPATH_V1 => "aquapath_v1".to_string(),
x if x == MACHINE_BUFFER_V1 => "buffer_v1".to_string(),
@@ -181,6 +182,7 @@ use crate::MACHINE_BUFFER_V1;
use crate::MACHINE_EXTRUDER_V1;
use crate::MACHINE_EXTRUDER_V2;
use crate::MACHINE_LASER_V1;
use crate::MACHINE_PELLETIZER;
use crate::MACHINE_MOCK;
use crate::MACHINE_WAGO_POWER_V1;
use crate::MACHINE_WINDER_V1;
-40
View File
@@ -1,40 +0,0 @@
use super::MotorTestMachine;
use crate::{MachineAct, MachineMessage};
use std::time::Instant;
impl MachineAct for MotorTestMachine {
fn act(&mut self, _now_ts: Instant) {
// println!("[{}::act] Running act", module_path!());
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
self.motor_driver.set_enabled(self.motor_state.enabled);
if self.motor_state.enabled {
let steps_per_rev = 200.0; // Adjust to match motor
let steps_per_second = (self.motor_state.target_velocity as f64) * steps_per_rev / 60.0;
let _ = self.motor_driver.set_speed(steps_per_second);
} else {
let _ = self.motor_driver.set_speed(0.0);
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(ns) => {
self.namespace.namespace = Some(ns);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => {
self.namespace.namespace = None;
}
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _ = self.api_mutate(value);
}
_ => {}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
use super::MotorTestMachine;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub motor_enabled: bool,
pub motor_velocity: i32,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum BeckhoffEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(tag = "type", content = "payload")]
enum Mutation {
SetMotorOn(bool),
SetMotorVelocity(i32),
SetMotorOff(bool),
}
#[derive(Debug, Clone)]
pub struct BeckhoffNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<BeckhoffEvents> for BeckhoffNamespace {
#[instrument(skip_all)]
fn emit(&mut self, events: BeckhoffEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<BeckhoffEvents> for BeckhoffEvents {
fn event_value(&self) -> GenericEvent {
match self {
BeckhoffEvents::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for MotorTestMachine {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetMotorOn(t) => {
if t {
self.turn_motor_on();
} else {
self.turn_motor_off();
}
}
Mutation::SetMotorVelocity(vel) => {
self.motor_state.target_velocity = vel;
self.emit_state();
}
_ => {}
}
Ok(())
}
}
-64
View File
@@ -1,64 +0,0 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use crate::{MOTOR_TEST_MACHINE, VENDOR_QITECH};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use smol::channel::{Receiver, Sender};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug, Clone, PartialEq)]
pub struct MotorState {
pub enabled: bool,
pub target_velocity: i32,
}
#[derive(Debug)]
pub struct MotorTestMachine {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: api::BeckhoffNamespace,
pub motor_driver: StepperVelocityEL70x1,
pub motor_state: MotorState,
}
impl Machine for MotorTestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl MotorTestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MOTOR_TEST_MACHINE,
};
pub fn emit_state(&mut self) {
let event = api::StateEvent {
motor_enabled: self.motor_state.enabled,
motor_velocity: self.motor_state.target_velocity,
};
self.namespace
.emit(api::BeckhoffEvents::State(event.build()));
}
pub fn turn_motor_on(&mut self) {
self.motor_state.enabled = true;
self.emit_state();
}
pub fn turn_motor_off(&mut self) {
self.motor_state.enabled = false;
self.emit_state();
}
}
-91
View File
@@ -1,91 +0,0 @@
use super::{MotorState, MotorTestMachine, api::BeckhoffNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::ek1100::{EK1100, EK1100_IDENTITY_A};
use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
use ethercat_hal::devices::el7031_0030::{
EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030StepperPort,
};
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
impl MachineNewTrait for MotorTestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
println!("[{}::new] Creating new MotorTestMachine", module_path!());
let device_identification = params.device_group.iter().cloned().collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => return Err(anyhow::anyhow!("Hardware is not EtherCAT")),
};
smol::block_on(async {
// Role 0: EK1100 (Koppler)
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
// Role 1: EL7031 (Stepper Motor)
let el7031 = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
1,
vec![EL7031_0030_IDENTITY_A],
)
.await?;
let el7031_config = EL7031_0030Configuration {
stm_features: ethercat_hal::devices::el7031_0030::coe::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 1500,
..Default::default()
},
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_config)
.await?;
device.0
};
let motor_driver =
StepperVelocityEL70x1::new(el7031.clone(), EL7031_0030StepperPort::STM1);
let (sender, receiver) = smol::channel::unbounded();
Ok(Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: BeckhoffNamespace {
namespace: params.namespace.clone(),
},
motor_driver,
motor_state: MotorState {
enabled: true,
target_velocity: 100,
},
})
})
}
}
+140
View File
@@ -0,0 +1,140 @@
use std::time::{Duration, Instant};
use serde::de::VariantAccess;
use units::frequency::hertz;
use crate::MachineAct;
use crate::MachineMessage;
use crate::MachineValues;
use super::Pelletizer;
impl MachineAct for Pelletizer
{
fn act(&mut self, now: Instant)
{
if let Ok(msg) = self.api_receiver.try_recv()
{
self.act_machine_message(msg);
};
let should_emit =
now.duration_since(self.last_measurement_emit)
> Duration::from_secs_f64(1.0 / 12.0);
let mut mutated: bool = false;
let mut inverter_snapshot_id: u64 = 0;
{
let mut inverter = smol::block_on(async {
self.inverter.write().await
});
if let Some(value) = self.mutation_request.running.take()
{
tracing::warn!("Setting running to: {}", value);
inverter.set_running(value);
mutated = true;
}
if let Some(value) = self.mutation_request.direction.take()
{
tracing::warn!("Setting direction to: {}", value);
inverter.set_direction(value);
mutated = true;
}
if let Some(value) = self.mutation_request.frequency.take()
{
tracing::warn!("Setting to: {}", value);
inverter.set_frequency_target((value * 10.0) as u16);
mutated = true;
}
if let Some(value) = self.mutation_request.accleration_level.take()
{
tracing::warn!("Setting to: {}", value);
inverter.set_acceleration_level(value);
mutated = true;
}
if let Some(value) = self.mutation_request.decleration_level.take()
{
tracing::warn!("Setting to: {}", value);
inverter.set_deceleration_level(value);
mutated = true;
}
if should_emit {
inverter.refresh_status();
}
if mutated { }
inverter.update();
inverter_snapshot_id = inverter.config.snapshot_id;
} // drop lock
if should_emit
{
if self.inverter_snapshot_id != inverter_snapshot_id
{
self.emit_state();
self.inverter_snapshot_id = inverter_snapshot_id;
}
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage)
{
match msg
{
MachineMessage::SubscribeNamespace(namespace) =>
{
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => match &mut self.namespace.namespace
{
Some(namespace) =>
{
tracing::info!("UnsubscribeNamespace");
namespace.socket_queue_tx.close();
namespace.sockets.clear();
namespace.events.clear();
}
None => todo!(),
},
MachineMessage::HttpApiJsonRequest(value) =>
{
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) => {}
MachineMessage::RequestValues(sender) =>
{
tracing::error!("REQUESTED VALUES");
let state = serde_json::to_value(self.create_state_event()).expect("Failed to serialize state");
let live_values = serde_json::to_value(self.create_live_values_event()).expect("Failed to serialize live values");
sender.send_blocking(MachineValues{ state, live_values }).expect("Failed to send values");
sender.close();
}
}
}
}
+158
View File
@@ -0,0 +1,158 @@
use crate::{MachineApi, MachineMessage};
use super::Pelletizer;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone)]
pub struct LiveValuesEvent
{
pub inverter_values: InverterLiveValues
}
#[derive(Serialize, Debug, Clone)]
pub struct InverterLiveValues
{
pub frequency: f64,
pub temperature: f64,
pub voltage: f64,
pub current: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, PartialEq, BuildEvent)]
pub struct StateEvent {
pub is_default_state: bool,
pub inverter_state: InverterState,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct InverterState
{
pub running: bool,
pub direction: bool,
pub frequency_target: f64,
pub acceleration_level: u16,
pub deceleration_level: u16,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct RotationState
{
pub forward: bool,
}
pub enum PelletMachineEvents {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
#[derive(Debug)]
pub struct PelletMachineNamespace {
pub namespace: Option<Namespace>,
}
#[derive(Debug, Deserialize, Serialize)]
/// Mutation for controlling the Pellet machine
enum Mutation
{
SetRunning(bool),
SetDirection(bool),
SetFrequencyTarget(f64),
SetAccelerationLevel(u16),
SetDecelerationLevel(u16),
}
impl MachineApi for Pelletizer {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error>
{
tracing::error!("Received: {:?}", &request_body);
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation
{
Mutation::SetRunning(running) =>
{
self.mutation_request.running = Some(running);
}
Mutation::SetDirection(forward) =>
{
self.mutation_request.direction = Some(forward);
}
Mutation::SetFrequencyTarget(frequency) =>
{
self.mutation_request.frequency = Some(frequency);
}
Mutation::SetAccelerationLevel(level) =>
{
self.mutation_request.accleration_level = Some(level);
}
Mutation::SetDecelerationLevel(level) =>
{
self.mutation_request.decleration_level = Some(level);
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
//TODO; rename NamespaceCacheingLogic to NamespaceCachingLogic
impl NamespaceCacheingLogic<PelletMachineEvents> for PelletMachineNamespace {
#[instrument(skip_all)]
fn emit(&mut self, events: PelletMachineEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace { ns.emit(event, &buffer_fn) }
}
}
impl CacheableEvents<Self> for PelletMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
}
+73
View File
@@ -0,0 +1,73 @@
use crate::pelletizer::Pelletizer;
use crate::pelletizer::api::{InverterLiveValues, InverterState, LiveValuesEvent, PelletMachineEvents, StateEvent};
use control_core::socketio::event::BuildEvent;
use control_core::socketio::namespace::NamespaceCacheingLogic;
impl Pelletizer
{
pub fn emit_state(&mut self)
{
let event = self.create_state_event().build();
self.namespace.emit(PelletMachineEvents::State(event));
self.emitted_default_state = true;
}
pub fn emit_live_values(&mut self)
{
let event = self.create_live_values_event().build();
self.namespace.emit(PelletMachineEvents::LiveValues(event));
}
pub fn create_state_event(&self) -> StateEvent
{
let inverter = smol::block_on(async {
self.inverter.read().await
});
tracing::error!("EMIT STATE: {:?}", inverter.config);
StateEvent
{
is_default_state: !self.emitted_default_state,
inverter_state: InverterState {
running: inverter.config.running,
direction: inverter.config.direction,
frequency_target: (inverter.config.frequency_target as f64) / 10.0,
acceleration_level: inverter.config.acceleration_level,
deceleration_level: inverter.config.deceleration_level,
},
}
}
pub fn create_live_values_event(&self) -> LiveValuesEvent
{
let inverter = smol::block_on(async {
self.inverter.read().await
});
if let Some(status) = inverter.status
{
return LiveValuesEvent {
inverter_values: InverterLiveValues {
voltage: status.voltage.value,
current: status.current.value,
temperature: status.temperature.value,
frequency: status.frequency.value,
}
};
}
LiveValuesEvent {
inverter_values: InverterLiveValues {
voltage: 0.0,
current: 0.0,
temperature: 0.0,
frequency: 0.0,
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
//use serde::{Deserialize, Serialize};
use crate::{
MACHINE_PELLETIZER, MachineMessage, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
use smol::{
channel::{Receiver, Sender},
lock::RwLock,
};
use socketioxide::extract::SocketRef;
use crate::{AsyncThreadMessage, serial::devices::us_3202510::US3202510};
use std::{sync::Arc, time::Instant};
pub mod act;
pub mod api;
pub mod new;
pub mod emit;
use crate::Machine;
use api::{PelletMachineNamespace};
#[derive(Debug)]
pub struct Pelletizer
{
// stuff for every machine
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
emitted_default_state: bool,
// socketio
namespace: PelletMachineNamespace,
last_measurement_emit: Instant,
// machine specific
inverter: Arc<RwLock<US3202510>>,
inverter_snapshot_id: u64,
mutation_request: MutationRequests
}
#[derive(Debug)]
pub struct MutationRequests
{
running: Option<bool>,
direction: Option<bool>,
frequency: Option<f64>,
accleration_level: Option<u16>,
decleration_level: Option<u16>,
}
impl Machine for Pelletizer
{
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique
{
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>>
{
self.main_sender.clone()
}
}
impl PelletMachineNamespace
{
pub async fn disconnect_all(&self)
{
for socket in self.connected_sockets().await
{
let _ = socket.disconnect();
}
}
async fn connected_sockets(&self) -> Vec<SocketRef>
{
if self.namespace.is_none()
{ vec![] }
else
{ self.namespace.clone().unwrap().sockets.clone() }
}
}
impl Drop for Pelletizer
{
fn drop(&mut self)
{
tracing::info!(
"[Pelletizer::{:?}] Dropping machine and disconnecting clients...",
self.machine_identification_unique
);
smol::block_on(self.namespace.disconnect_all());
}
}
impl Pelletizer
{
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_PELLETIZER,
};
pub fn update(&mut self)
{
//let laser_data = smol::block_on(async { self.inverter.read().await.get_data().await });
}
}
+61
View File
@@ -0,0 +1,61 @@
use std::time::Instant;
use crate::pelletizer::MutationRequests;
use crate::serial::devices::us_3202510::US3202510;
use crate::serial::{registry::SERIAL_DEVICE_REGISTRY};
use crate::{MachineNewHardware, MachineNewTrait};
use super::{Pelletizer, api::PelletMachineNamespace};
use anyhow::Error;
impl MachineNewTrait for Pelletizer
{
fn new(params: &crate::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized,
{
let hardware_serial = match params.hardware
{
MachineNewHardware::Serial(serial) => *serial,
_ => return Err(Error::msg("Invalid hardware type for Pelletizer")),
};
let inverter = match smol::block_on(
SERIAL_DEVICE_REGISTRY.downcast_arc_rwlock::<US3202510>(hardware_serial.device.clone()),
) {
Ok(inverter) => inverter,
Err(_) => return Err(Error::msg("Failed to downcast to US3202510")),
};
let (sender, receiver) = smol::channel::unbounded();
let pelletizer = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: PelletMachineNamespace {
namespace: params.namespace.clone(),
},
emitted_default_state: false,
last_measurement_emit: Instant::now(),
inverter,
inverter_snapshot_id: 0,
mutation_request: MutationRequests {
running: None,
direction: None,
frequency: None,
accleration_level: None,
decleration_level: None,
}
};
Ok(pelletizer)
}
}
+4 -4
View File
@@ -17,14 +17,13 @@ use crate::extruder1::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
use crate::{
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine,
winder2::Winder2,
pelletizer::Pelletizer, winder2::Winder2,
};
use crate::test_machine::TestMachine;
use lazy_static::lazy_static;
use crate::motor_test_machine::MotorTestMachine;
use anyhow::Error;
use std::{any::TypeId, collections::HashMap};
@@ -119,6 +118,9 @@ lazy_static! {
#[cfg(not(feature = "mock-machine"))]
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<Pelletizer>(Pelletizer::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
@@ -133,8 +135,6 @@ lazy_static! {
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
mc.register::<MotorTestMachine>(MotorTestMachine::MACHINE_IDENTIFICATION);
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
mc
+1
View File
@@ -1,6 +1,7 @@
#[cfg(feature = "mock-machine")]
pub mod extruder_mock;
pub mod laser;
pub mod us_3202510;
#[cfg(feature = "mock-machine")]
pub mod mock;
#[cfg(feature = "mock-machine")]
@@ -0,0 +1,289 @@
use modbus::{RequestResult, rtu::{DispatchError}};
// external deps
use serde::{Deserialize, Serialize};
// use modbus::ExceptionCode
// internal deps
pub use request::Request;
use units::{Frequency, electric_current::ampere, electric_potential::volt, frequency::hertz, thermodynamic_temperature::{degree_celsius, kelvin}};
// modules
mod register;
mod request;
mod serial_device;
mod transport;
use crate::serial::devices::us_3202510::{register::{HoldingRegister, InputRegister}, transport::{CustomTransport, EntryContext}};
type ModbusInterface = modbus::rtu::Interface<CustomTransport, EntryContext, 25>;
#[derive(Debug)]
pub struct US3202510
{
pub path: String,
pub config: Config,
pub status: Option<Status>,
failed_attempts: u8,
interface: ModbusInterface,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Config
{
pub snapshot_id: u64,
pub running: bool,
pub direction: bool,
pub frequency_target: u16, // 0 - 99hz
pub frequency_min: u16,
pub frequency_max: u16,
pub acceleration_level: u16, // 1 - 15
pub deceleration_level: u16, // 1 - 15
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Status
{
pub frequency: units::Frequency, // 1 - 99hz
pub voltage: units::ElectricPotential,
pub current: units::ElectricCurrent,
pub temperature: units::ThermodynamicTemperature,
pub operation_status: OperationStatus,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction
{
#[default]
Stopped,
Forward,
Reverse,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OperationStatus
{
#[default]
Idle,
Running,
Fault,
}
impl US3202510
{
pub fn update(&mut self)
{
if self.config.snapshot_id == 0
{
self.refresh_config();
self.config.snapshot_id += 1;
}
match self.interface.await_result()
{
Ok(result) =>
{
self.handle_result(result);
},
Err(e) =>
{
match e
{
modbus::rtu::ReceiveError::NoPendingRequest => {},
e =>
{
tracing::error!("Error reciving result: {:?}", e);
}
}
},
}
if !self.interface.has_pending_request()
{
match self.interface.dispatch_next_request()
{
Ok(_) =>
{
tracing::error!("Success sending request: {:?}", self.interface);
},
Err(e) =>
{
match e
{
DispatchError::RequestPending => {},
DispatchError::QueueEmpty => {},
DispatchError::Transport(err) =>
{
tracing::error!("Error sending request: {}", err);
},
DispatchError::BridgeDropped =>
{
tracing::error!("Bridge dropped");
},
}
},
}
}
}
fn queue_request(&mut self, request: Request)
{
let data = request.to_interface_request();
match self.interface.queue_request(data.type_id, data.payload, data.priority)
{
Ok(_) => {},
Err(x) =>
{
match x
{
modbus::QueueItemError::QueueFull => { tracing::error!("QueueFull!"); },
modbus::QueueItemError::DuplicateItem => { tracing::error!("DuplicateItem!"); },
}
},
}
}
pub fn refresh_status(&mut self)
{
self.queue_request(Request::RefreshStatus);
}
pub fn refresh_config(&mut self)
{
self.queue_request(Request::RefreshState);
}
pub fn set_running(&mut self, running: bool)
{
if self.config.running == running { return; }
tracing::error!("Setting running to: {}", running);
match running
{
true =>
{
let value: u16 = match self.config.direction
{
true => 1,
false => 3,
};
self.queue_request(Request::SetRotationState(value));
},
false => { self.queue_request(Request::SetRotationState(2)); },
}
self.config.running = running;
}
pub fn set_direction(&mut self, direction: bool)
{
if self.config.direction != direction && self.config.running
{
let value: u16 = if direction { 1 } else { 3 };
tracing::error!("Setting direction to: {}", value);
self.queue_request(Request::SetRotationState(value));
}
self.config.direction = direction;
}
pub fn set_frequency_target(&mut self, frequency: u16)
{
self.config.frequency_target = frequency.min(990);
self.queue_request(Request::SetFrequencyTarget(self.config.frequency_target));
}
pub fn set_acceleration_level(&mut self, acceleration_level: u16)
{
self.config.acceleration_level = acceleration_level.clamp(1, 15);
self.queue_request(Request::SetAccelerationLevel(self.config.acceleration_level));
}
pub fn set_deceleration_level(&mut self, deceleration_level: u16)
{
self.config.deceleration_level = deceleration_level.clamp(1, 15);
self.queue_request(Request::SetDecelerationLevel(self.config.deceleration_level));
}
fn handle_result(&mut self, response: RequestResult)
{
match response
{
RequestResult::ReadHoldingRegisters(data) =>
{
tracing::error!("BRANCH: HIT: {:?}", data);
let frequency_target: u16 =
*data.result.get(0)
.unwrap_or(&0);
let _run_command: u16 =
*data.result.get(1)
.unwrap_or(&0);
let acceleration_level: u16 =
*data.result.get(2)
.unwrap_or(&0);
let deceleration_level: u16 =
*data.result.get(3)
.unwrap_or(&0);
self.config.running = _run_command != 2;
self.config.frequency_target = frequency_target;
self.config.acceleration_level = acceleration_level;
self.config.deceleration_level = deceleration_level;
self.config.snapshot_id += 1;
},
RequestResult::ReadInputRegisters(data) =>
{
let frequency: u16 = *data.result.get(InputRegister::CurrentFrequency as usize).unwrap_or(&0);
let voltage: u16 = *data.result.get(InputRegister::BusVoltage as usize).unwrap_or(&0);
let current: u16 = *data.result.get(InputRegister::LineCurrent as usize).unwrap_or(&0);
let temperature: u16 = *data.result.get(InputRegister::DriveTemperature as usize).unwrap_or(&0);
let operation_status: u16 = *data.result.get(InputRegister::SystemStatus as usize).unwrap_or(&0);
self.status = Some(Status {
frequency: units::Frequency::new::<hertz>((frequency as f64) / 10.0),
voltage: units::ElectricPotential::new::<volt>((voltage as f64) / 10.0),
current: units::ElectricCurrent::new::<ampere>((current as f64) / 10.0),
temperature: units::ThermodynamicTemperature::new::<kelvin>((temperature as f64)),
operation_status: if operation_status == 0 { OperationStatus::Idle } else if operation_status == 1 {OperationStatus::Running } else { OperationStatus::Fault },
});
},
RequestResult::PresetHoldingRegister(request_result_data) =>
{
tracing::error!("PresetHoldingRegister: WORKED");
self.config.snapshot_id += 1;
},
RequestResult::Exception(request_result_data) =>
{
tracing::error!("Received exception: {:?}", request_result_data.result);
},
}
}
}
#[cfg(test)]
mod tests
{
#[test]
fn test_requests() {}
}
@@ -0,0 +1,83 @@
use strum_macros::{EnumCount};
#[derive(Debug, Clone, Copy, EnumCount)]
pub enum HoldingRegister
{
/// Register 0x0002
SetFrequency = 0x2,
/// Register 0x0003
#[allow(dead_code)]
RunCommand = 0x3,
/// Register 0x0004
#[allow(dead_code)]
AccelerationTime = 0x4,
/// Register 0x0005
#[allow(dead_code)]
DecelerationTime = 0x5,
}
#[derive(Debug, Clone, Copy, EnumCount)]
pub enum InputRegister
{
/// Register 0x0008
#[allow(dead_code)]
BusVoltage,
/// Register 0x0009
#[allow(dead_code)]
LineCurrent,
/// Register 0x000A
#[allow(dead_code)]
DriveTemperature,
/// Register 0x000B
#[allow(dead_code)]
SystemStatus,
/// Register 0x000C
#[allow(dead_code)]
ErrorCode,
/// Register 0x000D
#[allow(dead_code)]
CurrentFrequency,
}
impl HoldingRegister
{
pub const OFFSET: u16 = 0x2;
pub const fn address(self) -> u16
{
match self
{
Self::SetFrequency => Self::OFFSET, // 0x0002
Self::RunCommand => Self::OFFSET + 1, // 0x0003
Self::AccelerationTime => Self::OFFSET + 2, // 0x0004
Self::DecelerationTime => Self::OFFSET + 3, // 0x0005
}
}
}
impl InputRegister
{
pub const OFFSET: u16 = 0x8;
#[allow(dead_code)]
pub const fn address(self) -> u16
{
match self
{
Self::BusVoltage => Self::OFFSET, // 0x0008
Self::LineCurrent => Self::OFFSET + 1, // 0x0009
Self::DriveTemperature => Self::OFFSET + 2, // 0x000A
Self::SystemStatus => Self::OFFSET + 3, // 0x000B
Self::ErrorCode => Self::OFFSET + 4, // 0x000C
Self::CurrentFrequency => Self::OFFSET + 5, // 0x000D
}
}
}
@@ -0,0 +1,117 @@
use modbus::request::WriteRegister;
use strum::EnumCount;
use strum_macros::{EnumCount};
use crate::serial::devices::us_3202510::register::HoldingRegister;
use crate::serial::devices::us_3202510::register::InputRegister;
use modbus::Request as InterfaceRequest;
use modbus::request::ReadRegisters;
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum Request
{
RefreshStatus,
RefreshState,
SetRotationState(u16),
SetFrequencyTarget(u16),
SetAccelerationLevel(u16),
SetDecelerationLevel(u16),
}
#[derive(Debug)]
pub struct RequestData
{
pub type_id: u32,
pub priority: u32,
pub payload: InterfaceRequest,
}
impl Request
{
pub fn to_interface_request(&self) -> RequestData
{
match self
{
Request::RefreshStatus =>
{
let payload =
InterfaceRequest::ReadInputRegisters(
ReadRegisters
{
start_address: InputRegister::OFFSET,
quantity: InputRegister::COUNT as u16,
}
);
RequestData { type_id: 0, priority: 10, payload }
}
Request::RefreshState =>
{
let payload =
InterfaceRequest::ReadHoldingRegisters(
ReadRegisters
{
start_address: HoldingRegister::OFFSET,
quantity: HoldingRegister::COUNT as u16,
}
);
RequestData { type_id: 1, priority: 20, payload }
}
Request::SetRotationState(value) =>
{
let payload =
InterfaceRequest::PresetHoldingRegister(
WriteRegister
{
address: HoldingRegister::RunCommand as u16,
value: *value,
}
);
RequestData { type_id: 2, priority: 100, payload }
},
Request::SetFrequencyTarget(value) =>
{
let payload =
InterfaceRequest::PresetHoldingRegister(
WriteRegister
{
address: HoldingRegister::SetFrequency as u16,
value: *value,
}
);
RequestData { type_id: 3, priority: 50, payload }
}
Request::SetAccelerationLevel(value) =>
{
let payload =
InterfaceRequest::PresetHoldingRegister(
WriteRegister
{
address: HoldingRegister::AccelerationTime as u16,
value: *value,
}
);
RequestData { type_id: 4, priority: 30, payload }
},
Request::SetDecelerationLevel(value) =>
{
let payload =
InterfaceRequest::PresetHoldingRegister(
WriteRegister
{
address: HoldingRegister::DecelerationTime as u16,
value: *value,
}
);
RequestData { type_id: 5, priority: 30, payload }
},
}
}
}
@@ -0,0 +1,114 @@
use anyhow::anyhow;
use modbus::{Request, request::ReadRegisters};
use smol::lock::RwLock;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
};
use units::{ConstZero, Frequency};
use super::US3202510;
use crate::{
MACHINE_PELLETIZER,
SerialDevice,
SerialDeviceNew,
SerialDeviceNewParams,
VENDOR_QITECH,
machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification, DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique
}, serial::devices::us_3202510::{Config, ModbusInterface, request, transport::CustomTransport}
};
use serialport::{DataBits, FlowControl, Parity, StopBits};
use std::{sync::Arc, time::Duration};
impl SerialDevice for US3202510 {}
impl SerialDeviceNew for US3202510
{
fn new_serial(
params: &SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
{
let hash = hash_djb2(params.path.as_bytes());
let serial = byte_folding_u16(&hash.to_le_bytes());
let device_identification = DeviceIdentification {
device_machine_identification: Some(DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_PELLETIZER,
},
serial,
},
role: 0,
}),
device_hardware_identification: DeviceHardwareIdentification::Serial(
DeviceHardwareIdentificationSerial {
path: params.path.clone(),
},
),
};
// let interface_config = modbus_rtu::Config {
// slave_id: 1,
// path: params.path.clone(),
// data_bits: DataBits::Eight,
// parity: Parity::None,
// stop_bits: StopBits::One,
// flow_control: FlowControl::None,
// timeout: Duration::from_millis(1000),
// baudrate: 9600,
// machine_operation_delay: Duration::from_millis(100),
// };
let port =
serialport::new(params.path.clone(), 9600)
.data_bits(DataBits::Eight)
.parity(Parity::None)
.flow_control(FlowControl::None)
.baud_rate(9600)
.stop_bits(StopBits::One)
.timeout(Duration::from_millis(500))
.open()
.expect("");
let transport = CustomTransport::new(port, 1);
let interface = ModbusInterface::new(transport);
// let request: [u8; 8] = [
// 0x01, // slave id
// 0x04, // Read Input Registers
// 0x00, 0x08, // start address
// 0x00, 0x06, // quantity
// 0xB1, 0xCA, // CRC (lo, hi)
// ];
let _self = Arc::new(RwLock::new(Self {
path: params.path.clone(),
config: Config {
running: false,
direction: true,
frequency_target: 0,
frequency_max: 50,
frequency_min: 0,
acceleration_level: 7,
deceleration_level: 7,
snapshot_id: 0,
},
status: None,
failed_attempts: 0,
interface,
}));
Ok((device_identification, _self))
}
}
@@ -0,0 +1,178 @@
use std::io;
use std::time::{Duration, Instant};
use modbus::WithContext;
use modbus::rtu::{FrameParseError, Transport as TransportTrait};
use modbus::rtu::{DispatchError, ReceiveError};
use modbus::rtu::Frame;
#[derive(Debug)]
pub struct CustomTransport
{
port: std::sync::Mutex<Box<dyn serialport::SerialPort>>,
slave_id: u8,
}
// #[derive(Debug, Clone, PartialEq)]
// pub struct EntryContext
// {
// type_id: u32
// }
pub type EntryContext = u32;
impl CustomTransport
{
pub fn new(port: Box<dyn serialport::SerialPort>, slave_id: u8) -> Self
{
let p = std::sync::Mutex::new(port);
Self { port: p ,slave_id }
}
}
// implement InterfaceTransport for your RTU frames
impl TransportTrait<EntryContext> for CustomTransport
{
type Error = io::Error;
fn slave_id(&self) -> u8 { self.slave_id }
fn try_send(
&mut self,
item: WithContext<Frame, EntryContext>
) -> Result<(), DispatchError<Self::Error>>
{
let mut port = self.port.lock().unwrap();
match port.write_all(item.payload.bytes())
{
Ok(_) => {},
Err(e) =>
{
tracing::error!("Error: try_send: {:?}", e);
return Err(DispatchError::BridgeDropped);
},
}
match port.flush()
{
Ok(_) => {},
Err(e) =>
{
tracing::error!("Error: try_send: {:?}", e);
return Err(DispatchError::BridgeDropped);
},
}
Ok(())
}
fn try_recv(
&mut self
) -> Result<Option<Frame>, ReceiveError<Self::Error>>
{
let mut port = self.port.lock().unwrap();
let mut buf = [0u8; 256];
match port.read(&mut buf)
{
Ok(0) => Ok(None),
Ok(n) =>
{
match Frame::from_bytes(&buf[..n])
{
Ok(frame) => Ok(Some(frame)),
Err(e) =>
{
Err(ReceiveError::ParseError(e))
},
}
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(ReceiveError::Transport(e)),
}
}
fn send(
&mut self,
item: WithContext<Frame, EntryContext>
) -> Result<(), DispatchError<Self::Error>>
{
let mut port = self.port.lock().unwrap();
match port.write_all(item.payload.bytes())
{
Ok(_) => {},
Err(e) =>
{
tracing::error!("Error: try_send: {:?}", e);
},
}
match port.flush()
{
Ok(_) => {},
Err(e) =>
{
tracing::error!("Error: try_send: {:?}", e);
},
}
Ok(())
}
fn recv(&mut self) -> Result<Frame, ReceiveError<Self::Error>>
{
let mut port = self.port.lock().unwrap();
let mut buf = [0u8; 256];
let mut pos = 0;
let mut now: Instant = Instant::now();
let timeout = Duration::from_secs(1);
loop
{
match port.read(&mut buf[pos..])
{
Ok(0) =>
{
return Err(ReceiveError::ParseError(FrameParseError::TooSmall(0)));
}
Ok(n) =>
{
pos += n;
if let Ok(frame) = Frame::from_bytes(&buf[..pos])
{
return Ok(frame);
}
now = Instant::now(); // reset timeout timer
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock =>
{
if now.elapsed() >= timeout
{
if pos > 0
{
match Frame::from_bytes(&buf[..pos])
{
Ok(frame) => { return Ok(frame); },
Err(e) => { return Err(ReceiveError::ParseError(e)); },
}
}
return Err(ReceiveError::Timeout);
}
std::thread::sleep(Duration::from_millis(150));
}
Err(e) => return Err(ReceiveError::Transport(e)),
}
}
}
}
+6
View File
@@ -3,6 +3,7 @@ use lazy_static::lazy_static;
use crate::{
SerialDevice, SerialDeviceIdentification, machine_identification::DeviceIdentification,
serial::devices::laser::Laser,
serial::devices::us_3202510::US3202510,
};
#[cfg(feature = "mock-machine")]
@@ -109,6 +110,11 @@ lazy_static! {
product_id: 0x6001,
});
sdr.register::<US3202510>(SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
});
// Register MockSerialDevice when mock-machine feature is enabled
#[cfg(feature = "mock-machine")]
sdr.register::<MockSerialDevice>(SerialDeviceIdentification {
+1 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-rnaAsSkoTRoBgLCTF8ZFpi7uUQ+8W+u3qoMxsptX6NU=";
npmDepsHash = "sha256-wzVjsgcZsViPOyQKlKTb5gJIeY416KyCSiheSXSRZoc=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
+4 -5
View File
@@ -9,7 +9,7 @@ workspace = true
[dependencies]
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
@@ -27,10 +27,10 @@ tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
regex = "1.11.3"
# web
serde_json = "1.0.149"
serde_json = "1.0.143"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
tower-http = { version = "0.6.6", features = ["cors", "trace", "fs"] }
axum = { version = "0.8.8", features = ["macros"] }
axum = { version = "0.8.6", features = ["macros"] }
# serial
serialport = "4.7.3"
@@ -38,7 +38,7 @@ serialport = "4.7.3"
#tracing
tracing = { version = "0.1.44", features = ["attributes"] }
tracing-journald = { version = "0.3.1", optional = true }
tracing-subscriber = { version = "0.3.22", features = [
tracing-subscriber = { version = "0.3.20", features = [
"env-filter",
"chrono",
"registry",
@@ -47,7 +47,6 @@ utils = { version = "0.1.0", path="../utils" }
spin_sleep = "1.3.3"
dhat = { version = "0.3.3", optional = true }
ctrlc = { version = "3.3", optional = true }
ta = "0.5.0"
[dev-dependencies]
+2 -6
View File
@@ -7,7 +7,6 @@ use anyhow::{Result, bail};
use control_core::socketio::event::GenericEvent;
use ethercat_hal::devices::EthercatDevice;
use ethercrab::SubDeviceRef;
use ethercrab::subdevice_group::HasDc;
use ethercrab::{MainDevice, SubDeviceGroup, subdevice_group::Op};
use machines::machine_identification::{DeviceIdentification, MachineIdentificationUnique};
use machines::serial::registry::SERIAL_DEVICE_REGISTRY;
@@ -103,25 +102,22 @@ pub struct EthercatSetup {
/// All Ethercat devices
/// Generic interface for all devices
/// Needed to interface with the devices on an Ethercat level
pub group: SubDeviceGroup<MAX_SUBDEVICES, PDI_LEN, Op, HasDc>,
pub group: SubDeviceGroup<MAX_SUBDEVICES, PDI_LEN, Op>,
/// The Ethercat main device
/// Needed to interface with the devices
pub maindevice: MainDevice<'static>,
pub all_operational: bool,
}
impl EthercatSetup {
pub fn new(
devices: Vec<(DeviceIdentification, Arc<RwLock<dyn EthercatDevice>>)>,
group: SubDeviceGroup<MAX_SUBDEVICES, PDI_LEN, Op, HasDc>,
group: SubDeviceGroup<MAX_SUBDEVICES, PDI_LEN, Op>,
maindevice: MainDevice<'static>,
all_operational: bool,
) -> Self {
Self {
devices,
group,
maindevice,
all_operational,
}
}
}
+26 -2
View File
@@ -1,7 +1,12 @@
use crate::metrics::io::set_ethercat_iface;
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::Timer;
use std::time::Duration;
use std::{sync::Arc, time::Duration};
use crate::{
app_state::SharedState,
ethercat::{ethercat_discovery_info::send_ethercat_found, setup::setup_loop},
metrics::io::set_ethercat_iface,
};
pub async fn find_ethercat_interface() -> String {
loop {
@@ -18,3 +23,22 @@ pub async fn find_ethercat_interface() -> String {
}
}
}
pub async fn start_interface_discovery(app_state: Arc<SharedState>) {
let interface = find_ethercat_interface().await;
tracing::info!("Calling setup_loop");
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
Err(e) => {
tracing::error!(
"[{}::main] Failed to initialize EtherCAT network \n{:?}",
module_path!(),
e
);
}
}
send_ethercat_found(app_state, &interface).await;
}
+83 -179
View File
@@ -18,20 +18,10 @@ use ethercat_hal::devices::wago_750_354::{
use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::{
IP20_EC_DI8_DO8_PRODUCT_ID, IP20_EC_DI8_DO8_VENDOR_ID, IP20EcDi8Do8,
};
use ethercrab::error::Error;
use ethercrab::subdevice_group::{DcConfiguration, HasDc, PreOpPdi, SafeOp};
use smol::stream::StreamExt;
use ta::Next;
use ta::indicators::ExponentialMovingAverage;
#[cfg(not(feature = "development-build"))]
use crate::utils::stop_dnsmasq;
use crate::utils::{start_dnsmasq, stop_dnsmasq};
use ethercrab::std::ethercat_now;
use ethercrab::{
DcSync, MainDevice, MainDeviceConfig, PduStorage, RegisterAddress, RetryBehaviour,
SubDeviceGroup, Timeouts,
};
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use machines::machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
DeviceIdentificationIdentified, MachineIdentificationUnique, read_device_identifications,
@@ -40,7 +30,6 @@ use machines::registry::{MACHINE_REGISTRY, MachineRegistry};
use machines::{Machine, MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams};
use smol::channel::Sender;
use socketioxide::extract::SocketRef;
use std::time::Instant;
use std::{sync::Arc, time::Duration};
/// Structure to hold the result of grouping devices by identification
@@ -180,25 +169,23 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
pub async fn setup_loop(
interface: &str,
cycle_target: Duration,
app_state: Arc<SharedState>,
) -> Result<EthercatSetup, anyhow::Error> {
tracing::info!("Starting Ethercat PDU loop");
#[cfg(not(feature = "development-build"))]
{
let res = stop_dnsmasq();
match res {
Ok(_) => tracing::info!("Stopped dnsmasq"),
Err(e) => tracing::error!("Failed to stop dnsmasq: {:?}", e),
};
// Small Timeout to ensure interfaces get released
smol::Timer::after(Duration::from_millis(1500)).await;
}
let res = stop_dnsmasq();
match res {
Ok(_) => (),
Err(e) => tracing::error!("Failed to stop dnsmasq: {:?}", e),
};
// Small Timeout to ensure interfaces get released
smol::Timer::after(Duration::from_millis(1500)).await;
// Setup ethercrab tx/rx task
let pdu_storage = Box::leak(Box::new(PduStorage::<MAX_FRAMES, MAX_PDU_DATA>::new()));
let (tx, rx, pdu) = pdu_storage.try_split().expect("can only split once");
let interface = interface.to_string();
let mut has_dc = false;
std::thread::Builder::new()
.name("EthercatTxRxThread".to_owned())
@@ -242,7 +229,7 @@ pub async fn setup_loop(
pdu,
Timeouts {
// Default 5000ms
state_transition: Duration::from_millis(10000),
state_transition: Duration::from_millis(5000),
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
@@ -291,40 +278,6 @@ pub async fn setup_loop(
))?,
};
for mut subdevice in group_preop.iter_mut(&maindevice) {
if subdevice.name() == "EL4002" {
// Sync mode 01 = SM Synchronous, says it does dc but actually doesnt, thx?
subdevice
.sdo_write(0x1c32, 1, 1u16)
.await
.expect("Set sync mode");
}
// Configure SYNC0 AND SYNC1 for EL4102
else if subdevice.name() == "EL5152" {
// Sync mode 02 = SYNC0
subdevice
.sdo_write(0x1c32, 1, 2u16)
.await
.expect("Set sync mode");
subdevice
.sdo_write(0x1c32, 0x02, cycle_target.as_nanos() as u32)
.await
.expect("Set cycle time");
subdevice.set_dc_sync(DcSync::Sync01 {
// EL4102 ESI specifies SYNC1 with an offset of 100k ns
sync1_period: Duration::from_nanos(100_000),
});
} else {
match subdevice.dc_support() {
ethercrab::DcSupport::None => (),
ethercrab::DcSupport::RefOnly => (),
ethercrab::DcSupport::Bits64 => subdevice.set_dc_sync(DcSync::Sync0),
ethercrab::DcSupport::Bits32 => subdevice.set_dc_sync(DcSync::Sync0),
}
}
}
// create devices
let devices =
devices_from_subdevices::<MAX_SUBDEVICES, PDI_LEN>(&mut group_preop, &maindevice)?;
@@ -345,7 +298,6 @@ pub async fn setup_loop(
},
)
.collect::<Vec<_>>();
let devices = device_identifications
.into_iter()
.zip(devices)
@@ -404,6 +356,7 @@ pub async fn setup_loop(
*/
match (coupler.identity().vendor_id, coupler.identity().product_id) {
(WAGO_750_354_VENDOR_ID, WAGO_750_354_PRODUCT_ID) => {
has_dc = true;
let r = Wago750_354::initialize_modules(coupler).await?;
for module in r {
if coupler.configured_address() == module.belongs_to_addr {
@@ -411,7 +364,7 @@ pub async fn setup_loop(
Some(meta) => {
let meta_data = EtherCatDeviceMetaData {
configured_address: module.slot,
name:module.name,
name: "wago module".to_owned(),
vendor_id:module.vendor_id,
product_id: module.product_id,
revision: 0x2,
@@ -427,6 +380,7 @@ pub async fn setup_loop(
}
}
(IP20_EC_DI8_DO8_VENDOR_ID, IP20_EC_DI8_DO8_PRODUCT_ID) => {
has_dc = true;
let r = IP20EcDi8Do8::initialize_modules(coupler).await?;
for module in r {
if coupler.configured_address() == module.belongs_to_addr {
@@ -434,7 +388,7 @@ pub async fn setup_loop(
Some(meta) => {
let meta_data = EtherCatDeviceMetaData {
configured_address: module.slot,
name:module.name,
name: "IP20 EC DI8 DO8 module".to_owned(),
vendor_id:module.vendor_id,
product_id: module.product_id,
revision: 0x1,
@@ -453,11 +407,19 @@ pub async fn setup_loop(
};
drop(ethercat_meta_devices);
for subdevice in subdevices.iter() {
// Hack so El5152 goes into OP
if subdevice.name() == "EL5152" {
has_dc = true;
}
}
// remove subdevice from devices tuple
let devices = devices
.iter()
.map(|(device_identification, device, _)| (device_identification.clone(), device.clone()))
.collect::<Vec<_>>();
// Notify client via socketio
set_ethercat_devices::<MAX_SUBDEVICES, PDI_LEN>(
&identified_device_identifications,
@@ -471,130 +433,72 @@ pub async fn setup_loop(
)
.await?;
// Avoid potential Sdo write Contention on the bus, by just waiting a bit
// Timeout is not enough ... or rather a monstrous timeout would be required for all of the sdo writes we do ...
// TODO: Keep small timeout here and add Complete Access configuration for the stepper terminals and complex trerminals like (el5152)
smol::Timer::after(Duration::from_millis(4000)).await;
let mut now = Instant::now();
let start = Instant::now();
let mut averages = Vec::new();
for _ in 0..group_preop.len() {
averages.push(ExponentialMovingAverage::new(64).unwrap());
}
let mut tick_interval = smol::Timer::interval(cycle_target);
tracing::info!("Moving into PRE-OP with PDI");
let group = group_preop.into_pre_op_pdi(&maindevice).await?;
tracing::info!("Done. PDI available. Waiting for SubDevices to align");
loop {
group
.tx_rx_sync_system_time(&maindevice)
.await
.expect("TX/RX");
if now.elapsed() >= Duration::from_millis(25) {
now = Instant::now();
let mut max_deviation = 0;
for (s1, ema) in group.iter(&maindevice).zip(averages.iter_mut()) {
let diff = match s1
.register_read::<u32>(RegisterAddress::DcSystemTimeDifference)
.await
{
Ok(value) =>
// The returned value is NOT in two's compliment, rather the upper bit specifies
// whether the number in the remaining bits is odd or even, so we convert the
// value to `i32` using that logic here.
{
let flag = 0b1u32 << 31;
if value >= flag {
// Strip off negative flag bit and negate value as normal
-((value & !flag) as i32)
} else {
value as i32
}
}
Err(Error::WorkingCounter { .. }) => 0,
Err(e) => {
return Err(anyhow::anyhow!("Failed to read DC system time: {:?}", e));
}
};
let ema_next = ema.next(diff as f64);
max_deviation = max_deviation.max(ema_next.abs() as u32);
}
if max_deviation < 100 {
tracing::info!("Clocks settled after {} ms", start.elapsed().as_millis());
break;
}
let group_safe = match group_preop.into_safe_op(&maindevice).await {
Ok(group_op) => {
tracing::info!("Group in Safe-OP state");
group_op
}
tick_interval.next().await;
}
// SubDevice clocks are aligned. We can turn DC on now.
let group = group
.configure_dc_sync(
&maindevice,
DcConfiguration {
// Start SYNC0 100ms in the future
start_delay: Duration::from_millis(100),
// SYNC0 period should be the same as the process data loop in most cases
sync0_period: cycle_target,
// Send process data half way through cycle
sync0_shift: cycle_target / 2,
},
)
.await?;
// State machine to handle transition to SafeOp with process data
enum GroupState {
PreOp(SubDeviceGroup<MAX_SUBDEVICES, PDI_LEN, PreOpPdi, HasDc>),
SafeOp(SubDeviceGroup<MAX_SUBDEVICES, PDI_LEN, SafeOp, HasDc>),
}
let mut group_container = Some(GroupState::PreOp(group));
let mut tick = 0;
let group = loop {
let now = Instant::now();
match group_container.take().unwrap() {
GroupState::PreOp(group) => {
let res = group.tx_rx_dc(&maindevice).await.expect("TX/RX");
if tick > 300 {
let group = group.request_into_safe_op(&maindevice).await?;
group_container = Some(GroupState::SafeOp(group));
tracing::info!("Requested SAFE-OP");
} else {
group_container = Some(GroupState::PreOp(group));
}
smol::Timer::at(now + res.extra.next_cycle_wait).await;
}
GroupState::SafeOp(group) => {
let res = group.tx_rx_dc(&maindevice).await.expect("TX/RX");
if res.all_safe_op() {
tracing::info!("SAFE-OP");
break group;
} else {
group_container = Some(GroupState::SafeOp(group));
}
smol::Timer::at(now + res.extra.next_cycle_wait).await;
}
}
tick += 1;
Err(err) => Err(anyhow::anyhow!(
"[{}::setup_loop] Failed to put group in Safe-OP state: {:?}",
module_path!(),
err
))?,
};
let group_op = group
.request_into_op(&maindevice)
.await
.expect("SAFE-OP -> OP");
tracing::info!("Started Transition to OP");
// TODO Make a more extensive init for the case of DC-Sync
// Maybe we need multiple groups? like one DC group and one non dc sync group?
// For now we just check if we use wago coupler or IP20
if has_dc {
for _ in 1..1000 {
let res = group_safe.tx_rx_sync_system_time(&maindevice).await;
match res {
Ok(_) => (),
Err(e) => tracing::error!(
"[{}::setup_loop] Failed to sync dc time: {:?}",
e,
module_path!()
),
}
}
}
// Put group in operational state
let group_op = match group_safe.into_op(&maindevice).await {
Ok(group_op) => {
tracing::info!("Group in OP state");
group_op
}
Err(err) => Err(anyhow::anyhow!(
"[{}::setup_loop] Failed to put group in OP state: {:?}",
module_path!(),
err
))?,
};
{
// Notify client via socketio
let app_state_clone = app_state.clone();
let main_namespace = &mut app_state_clone
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatDevicesEventBuilder()
.build(app_state_clone.clone())
.await;
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
}
let res = start_dnsmasq();
match res {
Ok(o) => o,
Err(e) => tracing::error!("Failed to start dnsmasq: {:?}", e),
};
Ok(EthercatSetup {
devices,
group: group_op,
maindevice,
all_operational: false,
})
}
+43 -67
View File
@@ -4,8 +4,6 @@ use bitvec::prelude::*;
use control_core::realtime::set_core_affinity;
#[cfg(not(feature = "development-build"))]
use control_core::realtime::set_realtime_priority;
use ethercrab::TxRxResponse;
use ethercrab::subdevice_group::CycleInfo;
use machines::Machine;
use machines::machine_identification::write_machine_device_identification;
use smol::channel::Receiver;
@@ -90,6 +88,7 @@ pub fn start_loop_thread(
match msg {
HotThreadMessage::NoMsg => {}
HotThreadMessage::AddEtherCatSetup(ethercat_setup) => {
println!("EthercatSetup: {:?}", ethercat_setup.devices);
rt_loop_inputs.ethercat_setup = Some(Box::new(ethercat_setup));
}
HotThreadMessage::WriteMachineDeviceInfo(info_request) => {
@@ -114,6 +113,7 @@ pub fn start_loop_thread(
.retain(|m| m.get_machine_identification_unique() != unique_id);
}
HotThreadMessage::AddMachines(machine_vec) => {
tracing::info!("received machines{:?}", machine_vec);
for new_machine in machine_vec {
let id = new_machine.get_machine_identification_unique();
if !rt_loop_inputs
@@ -159,79 +159,55 @@ pub fn start_loop_thread(
}
pub async fn copy_ethercat_inputs(
ethercat_setup: Option<&mut EthercatSetup>,
ethercat_setup: Option<&EthercatSetup>,
) -> Result<(), anyhow::Error> {
// only if we have an ethercat setup
// - tx/rx cycle
// - copy inputs to devices
if let Some(ethercat_setup) = ethercat_setup {
if ethercat_setup.all_operational {
ethercat_setup
.group
.tx_rx_dc(&ethercat_setup.maindevice)
.await?;
ethercat_setup
.group
.tx_rx(&ethercat_setup.maindevice)
.await?;
// copy inputs to devices
for (i, subdevice) in ethercat_setup
.group
.iter(&ethercat_setup.maindevice)
.enumerate()
{
// retrieve inputs
let input = subdevice.inputs_raw();
let input_bits = input.view_bits::<Lsb0>();
// copy inputs to devices
for (i, subdevice) in ethercat_setup
.group
.iter(&ethercat_setup.maindevice)
.enumerate()
{
// retrieve inputs
let input = subdevice.inputs_raw();
let input_bits = input.view_bits::<Lsb0>();
// get device
let mut device = ethercat_setup.devices[i].1.as_ref().write().await;
// get device
let mut device = ethercat_setup.devices[i].1.as_ref().write().await;
// check if the device is used
if !device.is_used() {
// if the device is not used, we skip it
continue;
}
// put inputs into device
device.input_checked(input_bits).map_err(|e| {
anyhow::anyhow!(
"[{}::loop_once] SubDevice with index {} failed to copy inputs\n{:?}",
module_path!(),
i,
e
)
})?;
// post process inputs
device.input_post_process().map_err(|e| {
anyhow::anyhow!(
"[{}::loop_once] SubDevice with index {} failed to copy post_process\n{:?}",
module_path!(),
i,
e
)
})?;
}
return Ok(());
} else {
loop {
let now = Instant::now();
let response @ TxRxResponse {
working_counter: _wkc,
extra:
CycleInfo {
next_cycle_wait, ..
},
..
} = ethercat_setup
.group
.tx_rx_dc(&ethercat_setup.maindevice)
.await
.expect("TX/RX");
if response.all_op() {
ethercat_setup.all_operational = true;
break;
}
smol::Timer::at(now + next_cycle_wait).await;
// check if the device is used
if !device.is_used() {
// if the device is not used, we skip it
continue;
}
// put inputs into device
device.input_checked(input_bits).map_err(|e| {
anyhow::anyhow!(
"[{}::loop_once] SubDevice with index {} failed to copy inputs\n{:?}",
module_path!(),
i,
e
)
})?;
// post process inputs
device.input_post_process().map_err(|e| {
anyhow::anyhow!(
"[{}::loop_once] SubDevice with index {} failed to copy post_process\n{:?}",
module_path!(),
i,
e
)
})?;
}
}
Ok(())
@@ -300,7 +276,7 @@ pub fn loop_once<'maindevice>(inputs: &mut RtLoopInputs<'_>) -> Result<(), anyho
.unwrap()
.cycle_start();
let res = smol::block_on(copy_ethercat_inputs(inputs.ethercat_setup.as_deref_mut()));
let res = smol::block_on(copy_ethercat_inputs(inputs.ethercat_setup.as_deref()));
match res {
Ok(_) => (),
Err(e) => {
+30 -54
View File
@@ -2,24 +2,11 @@ use crate::{
metrics::collector::{RuntimeMetricsConfig, spawn_runtime_metrics_sampler},
socketio::main_namespace::machines_event::MachineObj,
};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use machines::{
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
SerialDeviceNewParams,
laser::LaserMachine,
machine_identification::{
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial, MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew, SerialDeviceNewParams, laser::LaserMachine, machine_identification::{
DeviceIdentification, DeviceIdentificationIdentified, MachineIdentificationUnique,
},
registry::{MACHINE_REGISTRY, MachineRegistry},
serial::{devices::laser::Laser, init::SerialDetection},
winder2::api::GenericEvent,
}, pelletizer::Pelletizer, registry::{MACHINE_REGISTRY, MachineRegistry}, serial::{devices::{laser::Laser, us_3202510::US3202510}, init::SerialDetection}, winder2::api::GenericEvent
};
use socketio::{
main_namespace::{MainNamespaceEvents, ethercat_devices_event::EthercatDevicesEventBuilder},
queue::start_socketio_queue,
};
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
use utils::start_dnsmasq;
@@ -39,12 +26,6 @@ use smol::{
use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
#[cfg(feature = "development-build")]
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(2000);
#[cfg(not(feature = "development-build"))]
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(700);
#[cfg(feature = "mock-machine")]
use mock_init::init_mock;
@@ -54,6 +35,7 @@ use crate::{
setup::setup_loop,
},
modbus_tcp::start_modbus_tcp_discovery,
socketio::queue::socketio_queue_worker,
};
#[cfg(feature = "mock-machine")]
@@ -135,8 +117,10 @@ pub async fn add_serial_device(
pub async fn start_serial_discovery(app_state: Arc<SharedState>) {
loop {
let devices = SerialDetection::detect_devices();
// This allows detection of disconnected devices
handle_serial_device_hotplug(app_state.clone(), devices).await;
smol::Timer::after(Duration::from_secs(1)).await;
}
}
@@ -148,35 +132,14 @@ pub async fn start_interface_discovery(
let interface = find_ethercat_interface().await;
tracing::info!("Inferface found {}, setting up EtherCAT loop", interface);
set_ethercat_iface(interface.clone());
let res = setup_loop(&interface, CYCLE_TARGET_TIME, app_state.clone()).await;
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(setup) => {
let _ = sender.send(HotThreadMessage::AddEtherCatSetup(setup)).await;
tracing::info!("Successfully initialized EtherCAT devices");
// Notify client via socketio
let app_state_clone = app_state.clone();
let main_namespace = &mut app_state_clone
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatDevicesEventBuilder()
.build(app_state_clone.clone())
.await;
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
#[cfg(not(feature = "development-build"))]
{
let res = start_dnsmasq();
match res {
Ok(o) => o,
Err(e) => tracing::error!("Failed to start dnsmasq: {:?}", e),
};
}
}
Err(e) => {
tracing::error!(
"[{}::main] Failed to initialize EtherCAT network \n{:?}",
@@ -184,7 +147,6 @@ pub async fn start_interface_discovery(
e
);
// if this doesnt work, unlucky
#[cfg(not(feature = "development-build"))]
let _ = start_dnsmasq();
}
}
@@ -195,19 +157,20 @@ pub async fn handle_serial_device_hotplug(
app_state: Arc<SharedState>,
map: HashMap<String, UsbPortInfo>,
) {
let laser_ident = SerialDeviceIdentification {
// IDENTIFIER OF THE USB DEVICE, NOT THE ACTUAL DEVICE
let usb_interface_id = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = SerialDetection::get_path_by_id(laser_ident, map);
let path = SerialDetection::get_path_by_id(usb_interface_id, map);
let mut unique_ident: Option<MachineIdentificationUnique> = None;
{
let machines = app_state.current_machines_meta.lock().await;
for machine in machines.iter() {
if machine.machine_identification_unique.machine_identification
== LaserMachine::MACHINE_IDENTIFICATION
for machine in machines.iter()
{
if machine.machine_identification_unique.machine_identification == Pelletizer::MACHINE_IDENTIFICATION
{
unique_ident = Some(machine.machine_identification_unique.clone());
break;
@@ -216,11 +179,12 @@ pub async fn handle_serial_device_hotplug(
}
// Machine isnt connected, so add it back
if laser.is_some() && unique_ident.is_none() {
if path.is_some() && unique_ident.is_none() {
let serial_params = SerialDeviceNewParams {
path: laser.unwrap(),
path: path.unwrap(),
};
match Laser::new_serial(&serial_params) {
match US3202510::new_serial(&serial_params) {
Ok((device_identification, serial_device)) => {
add_serial_device(
app_state.clone(),
@@ -233,7 +197,7 @@ pub async fn handle_serial_device_hotplug(
}
_ => (),
};
} else if laser.is_none() && unique_ident.is_some() {
} else if path.is_none() && unique_ident.is_some() {
let unique_ident = unique_ident.unwrap();
app_state
.clone()
@@ -324,6 +288,15 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
tracing::warn!("Async handler task finished");
}
pub async fn start_socketio_queue(app_state: Arc<SharedState>) {
let app_state = app_state.as_ref();
loop {
let res = socketio_queue_worker(app_state).await;
tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
tracing::error!("Restarting SocketIO...");
}
}
#[cfg(feature = "development-build")]
fn setup_ctrlc_handler() -> Arc<AtomicBool> {
let running = Arc::new(AtomicBool::new(true));
@@ -353,6 +326,8 @@ fn main() {
#[cfg(feature = "development-build")]
let running = setup_ctrlc_handler();
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(700);
// for the "hot thread"
let (sender, receiver) = smol::channel::unbounded();
let (main_sender, main_receiver) = smol::channel::unbounded();
@@ -376,6 +351,7 @@ fn main() {
#[cfg(not(feature = "mock-machine"))]
smol::spawn(start_interface_discovery(app_state.clone(), sender)).detach();
smol::spawn(start_modbus_tcp_discovery(app_state.clone())).detach();
smol::block_on(async {
+37 -13
View File
@@ -1,6 +1,6 @@
use crate::app_state::SharedState;
use std::sync::Arc;
use tracing::{error, info, instrument, trace};
use std::{sync::Arc, time::Instant};
use tracing::{debug, info, instrument, trace};
/// Send a single event with retry logic
#[instrument(skip_all)]
@@ -12,7 +12,7 @@ async fn send_event_with_retry(
loop {
// check if socket is still connected
if !socket.connected() {
info!(
trace!(
socket_id = ?socket.id,
event = %event.name,
"Socket disconnected, skipping event"
@@ -32,7 +32,7 @@ async fn send_event_with_retry(
}
Err(e) => match e {
socketioxide::SendError::Serialize(serialize_error) => {
info!(
trace!(
socket_id = ?socket.id,
event = %event.name,
error = %serialize_error,
@@ -45,7 +45,7 @@ async fn send_event_with_retry(
continue; // Retry sending the event
}
socketioxide::SocketError::Closed => {
info!(
trace!(
socket_id = ?socket.id,
event = %event.name,
"Socket closed, skipping event"
@@ -58,16 +58,40 @@ async fn send_event_with_retry(
}
}
pub async fn socketio_queue_worker(app_state: &SharedState) {
tracing::info!("SocketIO global queue listener started");
let mut event_count = 0;
let mut batch_start = Instant::now();
while let Ok((socket, event)) = app_state.socketio_setup.socket_queue_rx.recv().await {
event_count += 1;
send_event_with_retry(&socket, &event).await;
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::socketio_queue_worker] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
info!("SocketIO global queue listener stopped");
}
pub async fn start_socketio_queue(app_state: Arc<SharedState>) {
let app_state = app_state.as_ref();
loop {
let res = app_state.socketio_setup.socket_queue_rx.recv().await;
match res {
Ok((socket, event)) => send_event_with_retry(&socket, &event).await,
Err(e) => {
error!("SocketIO global queue listener stopped: {:?}", e);
break;
}
}
let res = socketio_queue_worker(app_state).await;
tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
tracing::error!("Restarting SocketIO...");
}
}