Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8acbb8b082 | ||
|
|
4a7682f3a6 | ||
|
|
491fe78572 | ||
|
|
b07b4a03e1 | ||
|
|
3f1af9bf9d | ||
|
|
4b08be6bed | ||
|
|
0572c1030f |
No files matched your search
@@ -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
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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```'
|
||||
})
|
||||
@@ -7,5 +7,4 @@ build.sh
|
||||
shell.nix
|
||||
node_modules/
|
||||
runtime_metrics.csv
|
||||
compile_metrics.csv
|
||||
.idea/
|
||||
compile_metrics.csv
|
||||
@@ -1,9 +1,3 @@
|
||||
# `2.14.1`
|
||||
_26.01.2026_
|
||||
|
||||
## Laser
|
||||
-[#1079](https://github.com/qitechgmbh/control/pull/1079) Fixed Overflowing of buffer for Graphs causing crashes
|
||||
|
||||
# `2.14.0`
|
||||
_22.01.2026_
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -979,7 +980,7 @@ dependencies = [
|
||||
"embedded-io-async",
|
||||
"ethercrab-wire",
|
||||
"futures-lite",
|
||||
"heapless",
|
||||
"heapless 0.8.0",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -997,16 +998,18 @@ 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",
|
||||
"heapless 0.8.0",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -1234,12 +1237,28 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
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 +1267,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 +1354,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 +1367,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.6.2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -1387,9 +1405,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 +1481,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 +1503,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 +1566,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 +1592,14 @@ dependencies = [
|
||||
"ethercrab",
|
||||
"euclid",
|
||||
"lazy_static",
|
||||
"modbus",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialport",
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"strum",
|
||||
"strum_macros 0.26.4",
|
||||
"textplots",
|
||||
"tonic",
|
||||
"tracing",
|
||||
@@ -1657,15 +1678,26 @@ 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#e71a27aaf4356402d38dca6224e8c2b66183dc1e"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"heapless 0.9.2",
|
||||
"libc",
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nb"
|
||||
version = "0.1.3"
|
||||
@@ -1818,17 +1850,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 +1873,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 +1883,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 +1893,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 +1906,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 +2005,7 @@ dependencies = [
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix 1.1.2",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -2004,9 +2030,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 +2095,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 +2151,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 +2195,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 +2214,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 +2352,6 @@ dependencies = [
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"spin_sleep",
|
||||
"ta",
|
||||
"textplots",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
@@ -2379,10 +2401,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 +2474,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 +2581,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 +2640,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 +2658,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 +2712,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 +2738,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 +2761,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 +2820,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 +2839,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 +2910,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 +2992,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 +3087,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 +3109,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 +3119,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 +3132,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 +3448,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 +3463,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 +3483,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"
|
||||
@@ -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://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://youtu.be/55egCAkQgyM)
|
||||
*Click here to watch a full explainer Video of our Software.*
|
||||
|
||||
## Videos
|
||||
# Repo Structure
|
||||
|
||||
| Software Demo | Full Explainer |
|
||||
|---------------|----------------|
|
||||
| [](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Click to watch a demo of the control software in action* | [](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://www.youtube.com/live/UlbnSVIhfLI?si=ZTotC5B7gd87tUim)
|
||||
*Video: Live stream of software dev meeting*
|
||||
@@ -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"
|
||||
|
||||
@@ -18,11 +18,11 @@ pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
// tracing::info!(
|
||||
// "set_interface_managed for {} managed was set to: {}",
|
||||
// interface,
|
||||
// managed_str
|
||||
// );
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
@@ -52,7 +52,7 @@ fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
//tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
@@ -119,7 +119,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
//tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
|
||||
@@ -52,11 +52,11 @@ pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
// tracing::info!(
|
||||
// "set_interface_managed for {} managed was set to: {}",
|
||||
// interface,
|
||||
// managed_str
|
||||
// );
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
@@ -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.
|
||||
|
||||

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

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

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

|
||||
|
||||
---
|
||||
|
||||
## 6. Documentation
|
||||
|
||||
Use the official documentation of the EL7031-0030 for more information:
|
||||
[Beckhoff EL7031-0030 Documentation](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)
|
||||
|
||||
---
|
||||
|
||||
## 7. Software Architecture
|
||||
|
||||
### Backend (Rust)
|
||||
Located in [machines/src/motor_test_machine/](/machines/src/motor_test_machine/).
|
||||
|
||||
1. **`mod.rs`**: Defines the `MotorTestMachine` struct and holds the state (driver wrapper, enabled state, target velocity).
|
||||
2. **`api.rs`**: Handles incoming JSON commands from the frontend (Enable/Disable, Set Velocity) via WebSockets/SocketIO.
|
||||
3. **`act.rs`**: The real-time control loop. It updates the `StepperVelocityEL70x1` driver wrapper in every cycle based on the current state.
|
||||
4. **`new.rs`**: Initializes the hardware.
|
||||
|
||||
### Frontend (TypeScript/React)
|
||||
Located in [electron/src/machines/motor_test_machine/](/electron/src/machines/motor_test_machine/) and [electron/src/routes/routes.tsx](/electron/src/routes/routes.tsx).
|
||||
|
||||
1. **`useTestMotor.ts`**: Custom hook managing the optimistic state and communication with the backend.
|
||||
2. **`TestMotorControlPage.tsx`**: The UI using QiTech UI components (`ControlCard`, `EditValue`, `SelectionGroupBoolean`) to match the look and feel of the Winder2.
|
||||
3. **Routing**: Integrated into `routes.tsx` using TanStack Router, ensuring the machine appears in the sidebar and navigation works correctly.
|
||||
@@ -0,0 +1,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 |
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,9 @@ export function useMachines(): UseMachine[] {
|
||||
const machinePreset = getMachineProperties(
|
||||
machine.machine_identification_unique.machine_identification,
|
||||
);
|
||||
|
||||
console.log("Machine!: {}", machine);
|
||||
|
||||
const vendorPreset = getVendorProperties(
|
||||
machinePreset!.machine_identification.vendor,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "@/client/socketioStore";
|
||||
} from "../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
createTimeSeries,
|
||||
@@ -66,13 +66,19 @@ export type Laser1NamespaceStore = {
|
||||
roundness: TimeSeries;
|
||||
};
|
||||
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries();
|
||||
// Constants for time durations
|
||||
const HALF_SECOND = 500;
|
||||
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries({
|
||||
sampleIntervalLong: HALF_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
|
||||
createTimeSeries();
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } =
|
||||
createTimeSeries();
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
|
||||
{ sampleIntervalLong: HALF_SECOND },
|
||||
);
|
||||
|
||||
/**
|
||||
* Factory function to create a new Laser1 namespace store
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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", features = ["linux-rtu-transport"] }
|
||||
|
||||
# enum stuff
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
textplots = "0.8.7"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
@@ -182,6 +183,7 @@ use crate::MACHINE_EXTRUDER_V1;
|
||||
use crate::MACHINE_EXTRUDER_V2;
|
||||
use crate::MACHINE_LASER_V1;
|
||||
use crate::MACHINE_MOCK;
|
||||
use crate::MACHINE_PELLETIZER;
|
||||
use crate::MACHINE_WAGO_POWER_V1;
|
||||
use crate::MACHINE_WINDER_V1;
|
||||
use crate::TEST_MACHINE;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 ¶ms.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,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
use crate::extruder1::ExtruderV2;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{
|
||||
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine,
|
||||
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine, pelletizer::Pelletizer,
|
||||
winder2::Winder2,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ 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,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, backends::LinuxTransport}};
|
||||
// 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, rtu::backends::LinuxTransport};
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -12,9 +17,28 @@ pub async fn find_ethercat_interface() -> String {
|
||||
return interface;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("No working interface found: {}. Retrying...", e);
|
||||
// tracing::warn!("No working interface found: {}. Retrying...", e);
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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(ðercat_setup.maindevice)
|
||||
.await?;
|
||||
ethercat_setup
|
||||
.group
|
||||
.tx_rx(ðercat_setup.maindevice)
|
||||
.await?;
|
||||
|
||||
// copy inputs to devices
|
||||
for (i, subdevice) in ethercat_setup
|
||||
.group
|
||||
.iter(ðercat_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(ðercat_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(ðercat_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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
}
|
||||