Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e1b74a583 | ||
|
|
ee93bb150d | ||
|
|
208fe5734f | ||
|
|
2abb6b2ce0 | ||
|
|
6f986d3c45 |
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,15 +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_
|
||||
|
||||
## General
|
||||
- [#1040](https://github.com/qitechgmbh/control/pull/1040) Added API for reading Machine Data
|
||||
|
||||
# `2.13.0`
|
||||
_07.01.2026_
|
||||
|
||||
@@ -17,6 +5,7 @@ _07.01.2026_
|
||||
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
|
||||
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
|
||||
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
|
||||
- []
|
||||
|
||||
## Extruder V3
|
||||
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
|
||||
|
||||
@@ -204,7 +204,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -221,7 +221,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -232,9 +232,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
@@ -291,7 +291,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -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",
|
||||
]
|
||||
@@ -539,7 +539,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -682,7 +682,7 @@ dependencies = [
|
||||
"deluxe-macros",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -695,7 +695,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -710,7 +710,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -721,7 +721,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -962,13 +962,14 @@ dependencies = [
|
||||
"deluxe",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ethercrab"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb13becfa15713adb09b27fdb74468b827cdc78aa99aea638a89c1b0db3ce94"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"atomic-waker",
|
||||
@@ -997,7 +998,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ethercrab-wire"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d15828cb020fb44c92ca67e1ee3c122c9621c2e00d383d066e488f0619023ba2"
|
||||
dependencies = [
|
||||
"ethercrab-wire-derive",
|
||||
"heapless",
|
||||
@@ -1006,19 +1008,20 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ethercrab-wire-derive"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/v-morlock/ethercrab?branch=main#d297d13389d85a4edb4e69cc98f9dd7f12626cac"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e790b8011211ed70babfcacbd4277b91ee21402ad5ba6a2c293db02e2f12919"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "euclid"
|
||||
version = "0.22.13"
|
||||
version = "0.22.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63"
|
||||
checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
@@ -1875,7 +1878,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1905,7 +1908,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2218,7 +2221,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2248,20 +2251,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2329,7 +2332,6 @@ dependencies = [
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"spin_sleep",
|
||||
"ta",
|
||||
"textplots",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
@@ -2571,9 +2573,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2586,12 +2588,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"
|
||||
@@ -2625,7 +2621,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2685,7 +2681,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2847,7 +2843,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2884,9 +2880,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"matchers",
|
||||
@@ -3078,7 +3074,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3143,7 +3139,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3154,7 +3150,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3430,11 +3426,5 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
@@ -2,76 +2,167 @@
|
||||
|
||||
# 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/>*Watch a demo of the control software in action* | [](https://youtu.be/55egCAkQgyM)<br/>*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/)
|
||||
|
||||
## 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
|
||||
|
||||
# Machines
|
||||
|
||||
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
|
||||
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
|
||||
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
|
||||
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
|
||||
|
||||
# Minimal Hardware Examples
|
||||
|
||||
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).
|
||||
|
||||
Available examples:
|
||||
- **LED Control with EL2004** - Digital output control (simplest setup)
|
||||
- **Analog Input with EL3021** - Reading analog current measurements
|
||||
|
||||
|
||||
## Hardware Examples
|
||||
# More Docs
|
||||
|
||||
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.
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
|
||||
**[View all hardware examples and tutorials on the wiki](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)**
|
||||
- [x] Example Winder V2
|
||||
|
||||
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)
|
||||
- [ ] Electron
|
||||
|
||||
## QiTech Machines
|
||||
- Folder Structure
|
||||
- Routing with TanStack Router
|
||||
- Design with Tailwind & Shadcn
|
||||
- ...
|
||||
|
||||
QiTech Control powers 10+ production machines for filament extrusion, winding, measurement, and material processing.
|
||||
- [ ] Interfacing with Electron/Server
|
||||
|
||||
**[View complete machine catalog on the wiki](https://github.com/qitechgmbh/control/wiki/QiTech-Machines)**
|
||||
- [ ] SocketIO
|
||||
- Machine Namespace
|
||||
- Main Namespace
|
||||
- [ ] REST
|
||||
- Machine Mutations
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
|
||||
## Contributing
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
- Maindevice
|
||||
- Group
|
||||
- Extracting Device Identifications
|
||||
- Identifying Groups
|
||||
- Validating Machines
|
||||
- Run Control Loop
|
||||
- [x] [Control Loop](./docs/control-loop.md)
|
||||
- [x] [Machine/Device Identification](./docs/identification.md)
|
||||
- [ ] Machines
|
||||
- When to create a new Machine?
|
||||
- Versioning
|
||||
- Code sharing
|
||||
- Creating/Validating a Machine
|
||||
- Validation
|
||||
- Configuration
|
||||
- [ ] Machine Implementation Guide
|
||||
- Link: How to create a Device
|
||||
- Link: How to create an Actor
|
||||
- Link: How to create a Machine
|
||||
- API (SocketIO + REST)
|
||||
- Creation/Validation Logic
|
||||
- Optional/Mandatory Devices
|
||||
- Validate Devices
|
||||
- Business Logic
|
||||
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
|
||||
- Forward `act` in winder.
|
||||
|
||||
This is an open-source project. Contributions are welcome! Please see the [wiki](https://github.com/qitechgmbh/control/wiki) for development guidelines.
|
||||
- [ ] Control Core
|
||||
|
||||
- [x] [Actors](./docs/actors.md)
|
||||
- [ ] SocketIO
|
||||
- Namespaces & Caching
|
||||
- Joining leaving namespaces
|
||||
- NamespaceId
|
||||
- Caching
|
||||
- Serverside Caching
|
||||
- Clientside Caching
|
||||
- [ ] REST
|
||||
|
||||
- [x] Ethercat HAL
|
||||
|
||||
## Additional Videos
|
||||
- [x] [Devices](./docs/devices.md)
|
||||
- [x] [Configuration (CoE)](./docs/coe.md)
|
||||
- [x] [IO](./docs/io.md)
|
||||
- [x] [PDO](./docs/pdo.md)
|
||||
|
||||
| Episode Highlights | |
|
||||
|--------------------|--------------------|
|
||||
| [](https://www.youtube.com/watch?v=U2wNN6sF4to&t=163s)<br/>**Day in the Software Team**<br/>*Behind the scenes with Robin and how the team ships control software.* | [](https://www.youtube.com/watch?v=R5WGAY3WRWA&t=1s)<br/>**Software Stack Day**<br/>*A fast tour through the tools, stack, and daily workflow.* |
|
||||
| [](https://www.youtube.com/watch?v=xyaHwxXguw4&t=32s)<br/>**First Client**<br/>*A milestone week building the first real client project.* | [](https://www.youtube.com/watch?v=moI7z2PVdF4)<br/>**Steelworks Week**<br/>*A deep-dive week on a demanding steelworks order.* |
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) file for details.
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 6.6 MiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 5.9 MiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 5.9 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 985 KiB |
@@ -89,86 +89,5 @@ To get started with actual hardware, check out these step-by-step tutorials:
|
||||
|
||||
- **[LED Control with EL2004](./minimal-example-el2004.md)** - Digital output control, the simplest possible hardware setup
|
||||
- **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements
|
||||
- **[Stepper Motor Control with EL7031-0030](./minimal-example-el7031-motor.md)** - Complete motor control integration with velocity control
|
||||
|
||||
These examples provide complete hardware wiring diagrams and software setup instructions.
|
||||
|
||||
|
||||
# More Docs
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
|
||||
- [x] Example Winder V2
|
||||
|
||||
- [ ] Electron
|
||||
|
||||
- Folder Structure
|
||||
- Routing with TanStack Router
|
||||
- Design with Tailwind & Shadcn
|
||||
- ...
|
||||
|
||||
- [ ] Interfacing with Electron/Server
|
||||
|
||||
- [ ] SocketIO
|
||||
- Machine Namespace
|
||||
- Main Namespace
|
||||
- [ ] REST
|
||||
- Machine Mutations
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
- Maindevice
|
||||
- Group
|
||||
- Extracting Device Identifications
|
||||
- Identifying Groups
|
||||
- Validating Machines
|
||||
- Run Control Loop
|
||||
- [x] [Control Loop](./docs/control-loop.md)
|
||||
- [x] [Machine/Device Identification](./docs/identification.md)
|
||||
- [ ] Machines
|
||||
- When to create a new Machine?
|
||||
- Versioning
|
||||
- Code sharing
|
||||
- Creating/Validating a Machine
|
||||
- Validation
|
||||
- Configuration
|
||||
- [ ] Machine Implementation Guide
|
||||
- Link: How to create a Device
|
||||
- Link: How to create an Actor
|
||||
- Link: How to create a Machine
|
||||
- API (SocketIO + REST)
|
||||
- Creation/Validation Logic
|
||||
- Optional/Mandatory Devices
|
||||
- Validate Devices
|
||||
- Business Logic
|
||||
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
|
||||
- Forward `act` in winder.
|
||||
|
||||
- [ ] Control Core
|
||||
|
||||
- [x] [Actors](./docs/actors.md)
|
||||
- [ ] SocketIO
|
||||
- Namespaces & Caching
|
||||
- Joining leaving namespaces
|
||||
- NamespaceId
|
||||
- Caching
|
||||
- Serverside Caching
|
||||
- Clientside Caching
|
||||
- [ ] REST
|
||||
|
||||
- [x] Ethercat HAL
|
||||
|
||||
- [x] [Devices](./docs/devices.md)
|
||||
- [x] [Configuration (CoE)](./docs/coe.md)
|
||||
- [x] [IO](./docs/io.md)
|
||||
- [x] [PDO](./docs/pdo.md)
|
||||
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
@@ -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.
|
||||
@@ -1,240 +0,0 @@
|
||||
# Minimal Example — LED Control on the WAGO750-402
|
||||
A complete hardware + software walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
The WAGO 750-402 Display Example is a minimal demonstation showing how to display the digital inputs on a WAGO 750-402 EtherCAT terminal using the QiTech machine framework.
|
||||
It represents the simplest possible hardware interaction in the system:
|
||||
Displaying the Digital Inputs using the QiTech Control Dashboard.
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- WAGO **2787-2144 EtherCAT**
|
||||
- WAGO **750-354 EtherCAT Coupler**
|
||||
- WAGO **750-602 EtherCAT Power Terminal**
|
||||
- WAGO **750-402 EtherCAT Terminal** (4.channle digital input)
|
||||
- WAGO **750-600 EtherCAT Endmodule**
|
||||
- WAGO **852-1322 Ethernet-Switch**
|
||||
- **24V DC Lab power supply**(AC/DC adapter)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Software
|
||||
*(Installation steps in Section 4)*
|
||||
|
||||
- Rust tollchain
|
||||
- Node-js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL *(include inside repo)*
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Schematic
|
||||
|
||||

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

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

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

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

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

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

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

|
||||
|
||||
## 6. References
|
||||
This guide incorporates information from official WAGO documentation.
|
||||
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
|
||||
|
||||
Referenced Manuals
|
||||
|
||||
[WAGO 750-402 Documentation](https://www.wago.com/de/io-systeme/4-kanal-digitaleingang/p/750-402#downloads)
|
||||
|
||||
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/feldbuskoppler-ethercat/p/750-354#details)
|
||||
|
||||
[WAGO 750-602 Documentation](https://www.wago.com/de/io-systeme/potentialeinspeisung/p/750-602#details)
|
||||
|
||||
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/endmodul/p/750-600#details)
|
||||
@@ -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"
|
||||
},
|
||||
@@ -10287,9 +10326,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz",
|
||||
"integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz",
|
||||
"integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -10297,9 +10336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/seroval-plugins": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.0.tgz",
|
||||
"integrity": "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.4.0.tgz",
|
||||
"integrity": "sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -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",
|
||||
@@ -10525,15 +10581,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js": {
|
||||
"version": "1.9.11",
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz",
|
||||
"integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==",
|
||||
"version": "1.9.10",
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
|
||||
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.5.0",
|
||||
"seroval-plugins": "~1.5.0"
|
||||
"seroval": "~1.3.0",
|
||||
"seroval-plugins": "~1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js/node_modules/seroval": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js/node_modules/seroval-plugins": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz",
|
||||
"integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"seroval": "^1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
@@ -10739,6 +10816,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 +10976,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 +11028,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 +11691,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 +11782,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 +11834,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 +12226,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"
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function Aquapath1Page() {
|
||||
link: "graph",
|
||||
title: "Graph",
|
||||
icon: "lu:ChartSpline",
|
||||
activeLink: "graph",
|
||||
activeLink: "graphs",
|
||||
},
|
||||
{
|
||||
link: "settings",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { Label } from "@/control/Label";
|
||||
import { useDigitalInputTestMachine } from "./useDigitalInputTestMachine";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { toastError } from "@/components/Toast";
|
||||
|
||||
export function DigitalInputTestMachineControlPage() {
|
||||
const { state } = useDigitalInputTestMachine();
|
||||
|
||||
const safeState = state ?? { led_on: [false, false, false, false] };
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlCard title="Machine LEDs">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{safeState.led_on.map((led, index) => (
|
||||
<Label key={index} label={`LED ${index + 1}`}>
|
||||
<Badge variant={led ? "outline" : "destructive"}>
|
||||
{led ? "On" : "OFF"}
|
||||
</Badge>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { digitalInputTestMachineSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function DigitalInputTestMachinePage() {
|
||||
const { serial } = digitalInputTestMachineSerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/digitalInputTestMachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +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 ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
led_on: z.array(z.boolean()).length(4),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type DigitalTestMachineNamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
};
|
||||
|
||||
export const createTestMachineNamespaceStore =
|
||||
(): StoreApi<DigitalTestMachineNamespaceStore> =>
|
||||
create<DigitalTestMachineNamespaceStore>(() => ({
|
||||
state: null,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
export function digitalInputTestMachineMessageHandler(
|
||||
store: StoreApi<DigitalTestMachineNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<DigitalTestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const updateStore = (
|
||||
updater: (
|
||||
state: DigitalTestMachineNamespaceStore,
|
||||
) => DigitalTestMachineNamespaceStore,
|
||||
) => 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 useDigitalInputTestMachineNamespaceImplementation =
|
||||
createNamespaceHookImplementation<DigitalTestMachineNamespaceStore>({
|
||||
createStore: createTestMachineNamespaceStore,
|
||||
createEventHandler: digitalInputTestMachineMessageHandler,
|
||||
});
|
||||
|
||||
export function useDigitalInputTestMachineNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): DigitalTestMachineNamespaceStore {
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
return useDigitalInputTestMachineNamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Toast, toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { digitalInputTestMachineSerialRoute } from "@/routes/routes";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
useDigitalInputTestMachineNamespace,
|
||||
StateEvent,
|
||||
} from "./digitalInputTestMachineNamespace";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { digitalInputTestMachine } from "@/machines/properties";
|
||||
import { z } from "zod";
|
||||
export function useDigitalInputTestMachine() {
|
||||
const { serial: serialString } =
|
||||
digitalInputTestMachineSerialRoute.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: digitalInputTestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Namespace state from backend
|
||||
const { state } = useDigitalInputTestMachineNamespace(machineIdentification);
|
||||
|
||||
// Optimistic state
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
useEffect(() => {
|
||||
if (state) stateOptimistic.setReal(state);
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -390,11 +390,6 @@ export const aquapath1: MachineProperties = {
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x110000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x120000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -467,41 +462,6 @@ export const testmachine: MachineProperties = {
|
||||
},
|
||||
],
|
||||
};
|
||||
export const digitalInputTestMachine: MachineProperties = {
|
||||
name: "Digital Input Machine",
|
||||
version: "V1",
|
||||
slug: "digitalInputTestMachine",
|
||||
icon: "lu:Disc3",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0040,
|
||||
},
|
||||
device_roles: [],
|
||||
};
|
||||
|
||||
export const wago8chDioTestMachine: MachineProperties = {
|
||||
name: "WAGO 8ch DIO Test",
|
||||
version: "V1",
|
||||
slug: "wago8chdiotestmachine",
|
||||
icon: "lu:Disc3",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0041,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "Wago 750-354 Bus Coupler",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 0x21,
|
||||
product_id: 0x07500354,
|
||||
revision: 0x2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const wagoPower1: MachineProperties = {
|
||||
name: "WAGO Power",
|
||||
version: "V1",
|
||||
@@ -573,64 +533,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 wagoDoTestMachine: MachineProperties = {
|
||||
name: "Wago DO Test",
|
||||
version: "V1",
|
||||
slug: "wagodotestmachine",
|
||||
icon: "lu:ToggleRight",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x000e,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "Wago 750-354 Bus Coupler",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 0x21,
|
||||
product_id: 0x07500354,
|
||||
revision: 0x2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const wagoAiTestMachine: MachineProperties = {
|
||||
name: "Wago AI Test",
|
||||
version: "V1",
|
||||
@@ -667,11 +569,7 @@ export const machineProperties: MachineProperties[] = [
|
||||
testmachine,
|
||||
analogInputTestMachine,
|
||||
wagoAiTestMachine,
|
||||
digitalInputTestMachine,
|
||||
wago8chDioTestMachine,
|
||||
ip20TestMachine,
|
||||
wagoDoTestMachine,
|
||||
TestMotor,
|
||||
];
|
||||
|
||||
export const getMachineProperties = (
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { wago8chDioTestMachineRoute } from "@/routes/routes";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { wago8chDioTestMachine } from "@/machines/properties";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
StateEvent,
|
||||
useWago8chDioTestMachineNamespace,
|
||||
} from "./wago8chDioTestMachineNameSpace";
|
||||
export function useWago8chDioTestMachine() {
|
||||
const { serial: serialString } = wago8chDioTestMachineRoute.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: wago8chDioTestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Namespace state from backend
|
||||
const { state } = useWago8chDioTestMachineNamespace(machineIdentification);
|
||||
|
||||
// Optimistic state
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
useEffect(() => {
|
||||
if (state) stateOptimistic.setReal(state);
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
// Generic mutation sender
|
||||
const { request: sendMutation } = useMachineMutate(
|
||||
z.object({
|
||||
action: z.string(),
|
||||
value: z.any(),
|
||||
}),
|
||||
);
|
||||
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest?: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState)
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
serverRequest?.();
|
||||
};
|
||||
|
||||
const setOutputAtIndex = (index: number, value: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.digital_output[index] = value;
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetOutput", value: { index, value } },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value,
|
||||
setOutputAtIndex: setOutputAtIndex,
|
||||
};
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Label } from "@/control/Label";
|
||||
import { useWago8chDioTestMachine } from "./useWago8chDioTestMachine";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export function Wago8chDioTestMachineControlRoute() {
|
||||
const { state, setOutputAtIndex } = useWago8chDioTestMachine();
|
||||
|
||||
const safeState = state ?? {
|
||||
digital_input: [false, false, false, false, false, false, false, false],
|
||||
digital_output: [false, false, false, false, false, false, false, false],
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
{/* Digital Inputs */}
|
||||
<ControlCard title="Digital Inputs">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{safeState.digital_input.map((input, index) => (
|
||||
<Label key={index} label={`Input ${index + 1}`}>
|
||||
<Badge variant={input ? "outline" : "destructive"}>
|
||||
{input ? "ON" : "OFF"}
|
||||
</Badge>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
{/* Digital Outputs */}
|
||||
<ControlCard title="Digital Outputs">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{safeState.digital_output.map((output, index) => (
|
||||
<Label key={index} label={`Output ${index + 1}`}>
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={output ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-red-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => setOutputAtIndex(index, value === "On")}
|
||||
/>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +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 ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
digital_input: z.array(z.boolean()).length(8),
|
||||
digital_output: z.array(z.boolean()).length(8),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type Wago8chDioTestMachineNamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
};
|
||||
|
||||
export const createWago8chDioTestMachineNamespaceStore =
|
||||
(): StoreApi<Wago8chDioTestMachineNamespaceStore> =>
|
||||
create<Wago8chDioTestMachineNamespaceStore>(() => ({
|
||||
state: null,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
export function wago8chDioTestMachineMessageHndler(
|
||||
store: StoreApi<Wago8chDioTestMachineNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<Wago8chDioTestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const updateStore = (
|
||||
updater: (
|
||||
state: Wago8chDioTestMachineNamespaceStore,
|
||||
) => Wago8chDioTestMachineNamespaceStore,
|
||||
) => 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 useWago8chDioTestMachineImplementation =
|
||||
createNamespaceHookImplementation<Wago8chDioTestMachineNamespaceStore>({
|
||||
createStore: createWago8chDioTestMachineNamespaceStore,
|
||||
createEventHandler: wago8chDioTestMachineMessageHndler,
|
||||
});
|
||||
|
||||
export function useWago8chDioTestMachineNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): Wago8chDioTestMachineNamespaceStore {
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
return useWago8chDioTestMachineImplementation(namespaceId);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { wago8chDioTestMachineRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function Wago8chDioTestMachinePage() {
|
||||
const { serial } = wago8chDioTestMachineRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/wago8chdiotestmachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Label } from "@/control/Label";
|
||||
import { useWagoDoTestMachine } from "./useWagoDoTestMachine";
|
||||
|
||||
export function WagoDoTestMachineControlPage() {
|
||||
const { state, setLed, setAllLeds } = useWagoDoTestMachine();
|
||||
|
||||
const safeState = state ?? {
|
||||
led_on: Array(8).fill(false),
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
{/* Digital Output Controls */}
|
||||
<ControlCard title="Digital Outputs (Do1-Do8)">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{safeState.led_on.map((output, index) => (
|
||||
<Label key={index} label={`Do${index + 1}`}>
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={output ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="flex flex-col gap-3"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-red-600",
|
||||
className: "flex-1",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "flex-1",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => setLed(index, value === "On")}
|
||||
/>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
{/* Master Output Control */}
|
||||
<ControlCard title="Master Output Control">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={safeState.led_on.every(Boolean) ? "On" : "Off"}
|
||||
orientation="horizontal"
|
||||
options={{
|
||||
Off: { children: "Turn All Off" },
|
||||
On: { children: "Turn All On" },
|
||||
}}
|
||||
onChange={(value) => setAllLeds(value === "On")}
|
||||
/>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +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 ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
led_on: z.array(z.boolean()).length(8),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type WagoDoTestMachineNamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
};
|
||||
|
||||
export const createWagoDoTestMachineNamespaceStore =
|
||||
(): StoreApi<WagoDoTestMachineNamespaceStore> =>
|
||||
create<WagoDoTestMachineNamespaceStore>(() => ({
|
||||
state: null,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
export function wagoDoTestMachineMessageHandler(
|
||||
store: StoreApi<WagoDoTestMachineNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<WagoDoTestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const updateStore = (
|
||||
updater: (
|
||||
state: WagoDoTestMachineNamespaceStore,
|
||||
) => WagoDoTestMachineNamespaceStore,
|
||||
) => 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 useWagoDoTestMachineNamespaceImplementation =
|
||||
createNamespaceHookImplementation<WagoDoTestMachineNamespaceStore>({
|
||||
createStore: createWagoDoTestMachineNamespaceStore,
|
||||
createEventHandler: wagoDoTestMachineMessageHandler,
|
||||
});
|
||||
|
||||
export function useWagoDoTestMachineNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): WagoDoTestMachineNamespaceStore {
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
return useWagoDoTestMachineNamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { wagoDoTestMachineSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function WagoDoTestMachinePage() {
|
||||
const { serial } = wagoDoTestMachineSerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/wagodotestmachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { wagoDoTestMachineSerialRoute } from "@/routes/routes";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
useWagoDoTestMachineNamespace,
|
||||
StateEvent,
|
||||
} from "./WagoDoTestMachineNamespace";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { wagoDoTestMachine } from "@/machines/properties";
|
||||
import { z } from "zod";
|
||||
|
||||
export function useWagoDoTestMachine() {
|
||||
const { serial: serialString } = wagoDoTestMachineSerialRoute.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: wagoDoTestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Namespace state from backend
|
||||
const { state } = useWagoDoTestMachineNamespace(machineIdentification);
|
||||
|
||||
// Optimistic state
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
useEffect(() => {
|
||||
if (state) stateOptimistic.setReal(state);
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
// Generic mutation sender
|
||||
const { request: sendMutation } = useMachineMutate(
|
||||
z.object({
|
||||
action: z.string(),
|
||||
value: z.any(),
|
||||
}),
|
||||
);
|
||||
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest?: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState)
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
serverRequest?.();
|
||||
};
|
||||
|
||||
const setLed = (index: number, on: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.led_on[index] = on;
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetLed", value: { index, on } },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setAllLeds = (on: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.led_on = Array(8).fill(on);
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetAllLeds", value: { on } },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value,
|
||||
setLed,
|
||||
setAllLeds,
|
||||
};
|
||||
}
|
||||
@@ -61,24 +61,14 @@ import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/Analog
|
||||
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
|
||||
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
|
||||
import { WagoAiTestMachineControl } from "@/machines/wagoaitestmachine/WagoAiTestMachineControlPage";
|
||||
|
||||
import { DigitalInputTestMachinePage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachinePage";
|
||||
import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
|
||||
|
||||
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";
|
||||
|
||||
import { WagoPower1Page } from "@/machines/wago_power/wago_power1/WagoPower1Page";
|
||||
import { WagoPower1ControlPage } from "@/machines/wago_power/wago_power1/WagoPower1ControlPage";
|
||||
import { WagoDoTestMachinePage } from "@/machines/wagodotestmachine/WagoDoTestMachinePage";
|
||||
import { WagoDoTestMachineControlPage } from "@/machines/wagodotestmachine/WagoDoTestMachineControlPage";
|
||||
import { Wago8chDioTestMachinePage } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachinePage";
|
||||
import { Wago8chDioTestMachineControlRoute } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachineControlPage";
|
||||
|
||||
// make a route tree like this
|
||||
// _mainNavigation/machines/winder2/$serial/control
|
||||
@@ -93,18 +83,6 @@ export const testMachineSerialRoute = createRoute({
|
||||
component: () => <TestMachinePage />,
|
||||
});
|
||||
|
||||
export const wago8chDioTestMachineRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "wago8chdiotestmachine/$serial",
|
||||
component: () => <Wago8chDioTestMachinePage />,
|
||||
});
|
||||
|
||||
export const wago8chDioTestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => wago8chDioTestMachineRoute,
|
||||
path: "control",
|
||||
component: () => <Wago8chDioTestMachineControlRoute />,
|
||||
});
|
||||
|
||||
// Leaf route: control page
|
||||
export const testMachineControlRoute = createRoute({
|
||||
getParentRoute: () => testMachineSerialRoute,
|
||||
@@ -136,18 +114,6 @@ export const wagoAiTestMachineControlRoute = createRoute({
|
||||
component: () => <WagoAiTestMachineControl />,
|
||||
});
|
||||
|
||||
export const digitalInputTestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "digitalInputTestMachine/$serial",
|
||||
component: () => <DigitalInputTestMachinePage />,
|
||||
});
|
||||
|
||||
export const digitalInputTestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => digitalInputTestMachineSerialRoute,
|
||||
path: "control",
|
||||
component: () => <DigitalInputTestMachineControlPage />,
|
||||
});
|
||||
|
||||
export const ip20TestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "ip20testmachine/$serial",
|
||||
@@ -160,18 +126,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",
|
||||
@@ -399,18 +353,6 @@ export const wagoPower1ControlRoute = createRoute({
|
||||
component: () => <WagoPower1ControlPage />,
|
||||
});
|
||||
|
||||
export const wagoDoTestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "wagodotestmachine/$serial",
|
||||
component: () => <WagoDoTestMachinePage />,
|
||||
});
|
||||
|
||||
export const wagoDoTestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => wagoDoTestMachineSerialRoute,
|
||||
path: "control",
|
||||
component: () => <WagoDoTestMachineControlPage />,
|
||||
});
|
||||
|
||||
export const setupRoute = createRoute({
|
||||
getParentRoute: () => sidebarRoute,
|
||||
path: "setup",
|
||||
@@ -512,27 +454,16 @@ export const rootTree = RootRoute.addChildren([
|
||||
laser1GraphsRoute,
|
||||
laser1PresetsRoute,
|
||||
]),
|
||||
|
||||
testMachineSerialRoute.addChildren([testMachineControlRoute]),
|
||||
|
||||
analogInputTestMachineSerialRoute.addChildren([
|
||||
analogInputTestMachineControlRoute,
|
||||
]),
|
||||
|
||||
wago8chDioTestMachineRoute.addChildren([
|
||||
wago8chDioTestMachineControlRoute,
|
||||
]),
|
||||
|
||||
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
|
||||
|
||||
digitalInputTestMachineSerialRoute.addChildren([
|
||||
digitalInputTestMachineControlRoute,
|
||||
]),
|
||||
|
||||
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
|
||||
|
||||
testMotorSerialRoute.addChildren([testMotorControlRoute]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
aquapath1ControlRoute,
|
||||
aquapath1GraphRoute,
|
||||
@@ -573,8 +504,6 @@ export const rootTree = RootRoute.addChildren([
|
||||
wagoPower1SerialRoute.addChildren([wagoPower1ControlRoute]),
|
||||
|
||||
buffer1SerialRoute.addChildren([buffer1ControlRoute]),
|
||||
|
||||
wagoDoTestMachineSerialRoute.addChildren([wagoDoTestMachineControlRoute]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -15,13 +15,7 @@ import {
|
||||
machineProperties,
|
||||
VENDOR_QITECH,
|
||||
} from "@/machines/properties";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -156,15 +150,6 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const updateNumpadPosition = useCallback(() => {
|
||||
if (!numpadOpen || !dialogRef.current) return;
|
||||
const rect = dialogRef.current.getBoundingClientRect();
|
||||
setNumpadPosition({
|
||||
left: rect.right + 20,
|
||||
top: rect.top + rect.height / 2,
|
||||
});
|
||||
}, [numpadOpen]);
|
||||
|
||||
const machinePreset = useMemo(() => {
|
||||
if (!values.machine) return;
|
||||
return getMachineProperties({
|
||||
@@ -202,7 +187,12 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
|
||||
// Position numpad once when it opens
|
||||
useEffect(() => {
|
||||
updateNumpadPosition();
|
||||
if (!numpadOpen || !dialogRef.current) return;
|
||||
const rect = dialogRef.current.getBoundingClientRect();
|
||||
setNumpadPosition({
|
||||
left: rect.right + 20,
|
||||
top: rect.top + rect.height / 2,
|
||||
});
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Close numpad when clicking outside input/numpad
|
||||
@@ -236,14 +226,6 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
}
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Update numpad position whenever the window resizes
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", updateNumpadPosition);
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateNumpadPosition);
|
||||
};
|
||||
}, [updateNumpadPosition]);
|
||||
|
||||
// Numpad handlers for serial input
|
||||
const numpadHandlers = useMemo(() => {
|
||||
const ensureFocus = () => {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,15 +2,11 @@ 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::{
|
||||
DynamicEthercatDevice, Module,
|
||||
wago_modules::{
|
||||
wago_750_402::{WAGO_750_402_MODULE_IDENT, WAGO_750_402_PRODUCT_ID},
|
||||
wago_750_455::{WAGO_750_455_MODULE_IDENT, WAGO_750_455_PRODUCT_ID},
|
||||
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_PRODUCT_ID},
|
||||
wago_750_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
|
||||
@@ -23,25 +19,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 +104,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 +133,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 +146,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 +221,6 @@ impl Wago750_354 {
|
||||
has_rx: false,
|
||||
tx_offset: 0,
|
||||
rx_offset: 0,
|
||||
name: "".to_string(),
|
||||
};
|
||||
|
||||
match ident_iom {
|
||||
@@ -271,37 +228,22 @@ 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 +261,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
|
||||
@@ -377,13 +294,6 @@ impl Wago750_354 {
|
||||
WAGO_750_652_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
|
||||
}
|
||||
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 +304,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
|
||||
@@ -431,9 +430,6 @@ impl IP20EcDi8Do8 {
|
||||
wago_750_652::WAGO_750_652_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
|
||||
}
|
||||
wago_750_402::WAGO_750_402_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
|
||||
}
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -38,27 +38,27 @@ impl From<Wago750_1506OutputPort> for usize {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Wago750_1506InputPort {
|
||||
DI1,
|
||||
DI2,
|
||||
DI3,
|
||||
DI4,
|
||||
DI5,
|
||||
DI6,
|
||||
DI7,
|
||||
DI8,
|
||||
DO1,
|
||||
DO2,
|
||||
DO3,
|
||||
DO4,
|
||||
DO5,
|
||||
DO6,
|
||||
DO7,
|
||||
DO8,
|
||||
}
|
||||
|
||||
impl From<Wago750_1506InputPort> for usize {
|
||||
fn from(value: Wago750_1506InputPort) -> Self {
|
||||
match value {
|
||||
Wago750_1506InputPort::DI1 => 0,
|
||||
Wago750_1506InputPort::DI2 => 1,
|
||||
Wago750_1506InputPort::DI3 => 2,
|
||||
Wago750_1506InputPort::DI4 => 3,
|
||||
Wago750_1506InputPort::DI5 => 4,
|
||||
Wago750_1506InputPort::DI6 => 5,
|
||||
Wago750_1506InputPort::DI7 => 6,
|
||||
Wago750_1506InputPort::DI8 => 7,
|
||||
Wago750_1506InputPort::DO1 => 0,
|
||||
Wago750_1506InputPort::DO2 => 1,
|
||||
Wago750_1506InputPort::DO3 => 2,
|
||||
Wago750_1506InputPort::DO4 => 3,
|
||||
Wago750_1506InputPort::DO5 => 4,
|
||||
Wago750_1506InputPort::DO6 => 5,
|
||||
Wago750_1506InputPort::DO7 => 6,
|
||||
Wago750_1506InputPort::DO8 => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,14 +91,14 @@ impl DigitalInputDevice<Wago750_1506InputPort> for Wago750_1506 {
|
||||
fn get_input(&self, port: Wago750_1506InputPort) -> Result<DigitalInputInput, anyhow::Error> {
|
||||
Ok(DigitalInputInput {
|
||||
value: match port {
|
||||
Wago750_1506InputPort::DI1 => self.tx_pdo.port1,
|
||||
Wago750_1506InputPort::DI2 => self.tx_pdo.port2,
|
||||
Wago750_1506InputPort::DI3 => self.tx_pdo.port3,
|
||||
Wago750_1506InputPort::DI4 => self.tx_pdo.port4,
|
||||
Wago750_1506InputPort::DI5 => self.tx_pdo.port5,
|
||||
Wago750_1506InputPort::DI6 => self.tx_pdo.port6,
|
||||
Wago750_1506InputPort::DI7 => self.tx_pdo.port7,
|
||||
Wago750_1506InputPort::DI8 => self.tx_pdo.port8,
|
||||
Wago750_1506InputPort::DO1 => self.tx_pdo.port1,
|
||||
Wago750_1506InputPort::DO2 => self.tx_pdo.port2,
|
||||
Wago750_1506InputPort::DO3 => self.tx_pdo.port3,
|
||||
Wago750_1506InputPort::DO4 => self.tx_pdo.port4,
|
||||
Wago750_1506InputPort::DO5 => self.tx_pdo.port5,
|
||||
Wago750_1506InputPort::DO6 => self.tx_pdo.port6,
|
||||
Wago750_1506InputPort::DO7 => self.tx_pdo.port7,
|
||||
Wago750_1506InputPort::DO8 => self.tx_pdo.port8,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -189,28 +189,28 @@ impl EthercatDevice for Wago750_1506 {
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let base = self.tx_bit_offset;
|
||||
|
||||
let idx1 = base + Into::<usize>::into(Wago750_1506InputPort::DI1);
|
||||
let idx1 = base + Into::<usize>::into(Wago750_1506InputPort::DO1);
|
||||
self.tx_pdo.port1 = *input.get(idx1).expect("Bit 1 out of bounds");
|
||||
|
||||
let idx2 = base + Into::<usize>::into(Wago750_1506InputPort::DI2);
|
||||
let idx2 = base + Into::<usize>::into(Wago750_1506InputPort::DO2);
|
||||
self.tx_pdo.port2 = *input.get(idx2).expect("Bit 2 out of bounds");
|
||||
|
||||
let idx3 = base + Into::<usize>::into(Wago750_1506InputPort::DI3);
|
||||
let idx3 = base + Into::<usize>::into(Wago750_1506InputPort::DO3);
|
||||
self.tx_pdo.port3 = *input.get(idx3).expect("Bit 3 out of bounds");
|
||||
|
||||
let idx4 = base + Into::<usize>::into(Wago750_1506InputPort::DI4);
|
||||
let idx4 = base + Into::<usize>::into(Wago750_1506InputPort::DO4);
|
||||
self.tx_pdo.port4 = *input.get(idx4).expect("Bit 4 out of bounds");
|
||||
|
||||
let idx5 = base + Into::<usize>::into(Wago750_1506InputPort::DI5);
|
||||
let idx5 = base + Into::<usize>::into(Wago750_1506InputPort::DO5);
|
||||
self.tx_pdo.port5 = *input.get(idx5).expect("Bit 5 out of bounds");
|
||||
|
||||
let idx6 = base + Into::<usize>::into(Wago750_1506InputPort::DI6);
|
||||
let idx6 = base + Into::<usize>::into(Wago750_1506InputPort::DO6);
|
||||
self.tx_pdo.port6 = *input.get(idx6).expect("Bit 6 out of bounds");
|
||||
|
||||
let idx7 = base + Into::<usize>::into(Wago750_1506InputPort::DI7);
|
||||
let idx7 = base + Into::<usize>::into(Wago750_1506InputPort::DO7);
|
||||
self.tx_pdo.port7 = *input.get(idx7).expect("Bit 7 out of bounds");
|
||||
|
||||
let idx8 = base + Into::<usize>::into(Wago750_1506InputPort::DI8);
|
||||
let idx8 = base + Into::<usize>::into(Wago750_1506InputPort::DO8);
|
||||
self.tx_pdo.port8 = *input.get(idx8).expect("Bit 8 out of bounds");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
use crate::{
|
||||
devices::{
|
||||
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
|
||||
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
|
||||
},
|
||||
io::digital_input::{DigitalInputDevice, DigitalInputInput},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Wago750_402InputPort {
|
||||
DI1,
|
||||
DI2,
|
||||
DI3,
|
||||
DI4,
|
||||
}
|
||||
|
||||
impl From<Wago750_402InputPort> for usize {
|
||||
fn from(value: Wago750_402InputPort) -> Self {
|
||||
match value {
|
||||
Wago750_402InputPort::DI1 => 0,
|
||||
Wago750_402InputPort::DI2 => 1,
|
||||
Wago750_402InputPort::DI3 => 2,
|
||||
Wago750_402InputPort::DI4 => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Wago750_402TxPdo {
|
||||
port1: bool,
|
||||
port2: bool,
|
||||
port3: bool,
|
||||
port4: bool,
|
||||
}
|
||||
|
||||
impl DigitalInputDevice<Wago750_402InputPort> for Wago750_402 {
|
||||
fn get_input(&self, port: Wago750_402InputPort) -> Result<DigitalInputInput, anyhow::Error> {
|
||||
Ok(DigitalInputInput {
|
||||
value: match port {
|
||||
Wago750_402InputPort::DI1 => self.tx_pdo.port1,
|
||||
Wago750_402InputPort::DI2 => self.tx_pdo.port2,
|
||||
Wago750_402InputPort::DI3 => self.tx_pdo.port3,
|
||||
Wago750_402InputPort::DI4 => self.tx_pdo.port4,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wago750_402 {
|
||||
is_used: bool,
|
||||
tx_bit_offset: usize,
|
||||
// Should always be on but not when calling a constructor
|
||||
module: Option<Module>,
|
||||
tx_pdo: Wago750_402TxPdo,
|
||||
}
|
||||
|
||||
impl DynamicEthercatDevice for Wago750_402 {}
|
||||
|
||||
impl EthercatDynamicPDO for Wago750_402 {
|
||||
fn get_tx_offset(&self) -> usize {
|
||||
self.tx_bit_offset
|
||||
}
|
||||
|
||||
fn get_rx_offset(&self) -> usize {
|
||||
0 //this device has no rx
|
||||
}
|
||||
|
||||
fn set_tx_offset(&mut self, offset: usize) {
|
||||
self.tx_bit_offset = offset
|
||||
}
|
||||
|
||||
fn set_rx_offset(&mut self, _offset: usize) {
|
||||
// it does nothing because the device has no rx
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceUsed for Wago750_402 {
|
||||
fn is_used(&self) -> bool {
|
||||
self.is_used
|
||||
}
|
||||
|
||||
fn set_used(&mut self, used: bool) {
|
||||
self.is_used = used;
|
||||
}
|
||||
}
|
||||
|
||||
impl Wago750_402 {}
|
||||
|
||||
impl EthercatDevice for Wago750_402 {
|
||||
// input_len 48 offset: 40
|
||||
fn input(
|
||||
&mut self,
|
||||
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let base = self.tx_bit_offset;
|
||||
|
||||
let idx1 = base + Into::<usize>::into(Wago750_402InputPort::DI1);
|
||||
self.tx_pdo.port1 = *input.get(idx1).expect("Bit 1 out of bounds");
|
||||
|
||||
let idx2 = base + Into::<usize>::into(Wago750_402InputPort::DI2);
|
||||
self.tx_pdo.port2 = *input.get(idx2).expect("Bit 2 out of bounds");
|
||||
|
||||
let idx3 = base + Into::<usize>::into(Wago750_402InputPort::DI3);
|
||||
self.tx_pdo.port3 = *input.get(idx3).expect("Bit 3 out of bounds");
|
||||
|
||||
let idx4 = base + Into::<usize>::into(Wago750_402InputPort::DI4);
|
||||
self.tx_pdo.port4 = *input.get(idx4).expect("Bit 4 out of bounds");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_len(&self) -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn output(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// println!("output: {} {}",self.rx_bit_offset,_output);
|
||||
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<Module> {
|
||||
self.module.clone()
|
||||
}
|
||||
|
||||
fn set_module(&mut self, module: Module) {
|
||||
self.tx_bit_offset = module.tx_offset;
|
||||
self.module = Some(module);
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceProcessing for Wago750_402 {
|
||||
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl NewEthercatDevice for Wago750_402 {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
is_used: false,
|
||||
tx_bit_offset: 0,
|
||||
module: None,
|
||||
tx_pdo: Wago750_402TxPdo::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Wago750_402 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Wago750_402")
|
||||
}
|
||||
}
|
||||
|
||||
pub const WAGO_750_402_VENDOR_ID: u32 = 0x00000021;
|
||||
pub const WAGO_750_402_PRODUCT_ID: u32 = 2147483713;
|
||||
pub const WAGO_750_402_MODULE_IDENT: SubDeviceProductTuple =
|
||||
(WAGO_750_402_VENDOR_ID, WAGO_750_402_PRODUCT_ID);
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,74 +2,3 @@ pub mod counter_wrapper_u16_i128;
|
||||
pub mod el70xx_velocity_converter;
|
||||
pub mod ethercrab_types;
|
||||
pub mod signing_converter_u16;
|
||||
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
// This is the Codeword for disabling EEPROM writes on sdo writes
|
||||
const BECKHOFF_EEPROM_LOCK_CODEWORD: u32 = 0x12345678;
|
||||
// In this index Codewords can be written for Beckhoff Terminals
|
||||
const BECKHOFF_CODEWORD_INDEX: u16 = 0xF008;
|
||||
|
||||
// This code locks the eeprom and protects it from writes
|
||||
// When the toggle is in effect no writes propagate to the eeprom
|
||||
// This is useful if you have a usecase where you very often write into CoE registers, as writes to EEPROM after a long time degrage the EEPROM
|
||||
pub async fn set_mut_beckhoff_eeprom_lock_active<'a>(
|
||||
subdevice: &SubDeviceRef<'a, &mut SubDevice>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let code_word = match subdevice.sdo_read::<u32>(BECKHOFF_CODEWORD_INDEX, 0).await {
|
||||
Ok(code_word) => code_word,
|
||||
// This happens when the subdevice has no mailbox
|
||||
// There is NO check in ethercrab for Mailbox presence, so we just have to pray that it has one and send a request
|
||||
// If there is no Mailbox to write Coe Request to, then there is also no EEPROM meaning we achieved our goal in a sense
|
||||
// This is why it returns OK on an error for sdo_read
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
let eeprom_lock_toggled = match code_word {
|
||||
BECKHOFF_EEPROM_LOCK_CODEWORD => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if !eeprom_lock_toggled {
|
||||
subdevice
|
||||
.sdo_write(BECKHOFF_CODEWORD_INDEX, 0, BECKHOFF_EEPROM_LOCK_CODEWORD)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
"Activated EEPROM Lock for {} {}",
|
||||
subdevice.name(),
|
||||
subdevice.name()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_beckhoff_eeprom_lock_active<'a>(
|
||||
subdevice: &SubDeviceRef<'a, &SubDevice>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let code_word = match subdevice.sdo_read::<u32>(BECKHOFF_CODEWORD_INDEX, 0).await {
|
||||
Ok(code_word) => code_word,
|
||||
// This happens when the subdevice has no mailbox
|
||||
// There is NO check in ethercrab for Mailbox presence, so we just have to pray that it has one and send a request
|
||||
// If there is no Mailbox to write Coe Request to, then there is also no EEPROM meaning we achieved our goal in a sense
|
||||
// This is why it returns OK on an error for sdo_read
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
let eeprom_lock_toggled = match code_word {
|
||||
BECKHOFF_EEPROM_LOCK_CODEWORD => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if !eeprom_lock_toggled {
|
||||
subdevice
|
||||
.sdo_write(BECKHOFF_CODEWORD_INDEX, 0, BECKHOFF_EEPROM_LOCK_CODEWORD)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
"Activated EEPROM Lock for {} {}",
|
||||
subdevice.name(),
|
||||
subdevice.name()
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
use super::DigitalInputTestMachine;
|
||||
use crate::{MachineAct, MachineMessage, MachineValues};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for DigitalInputTestMachine {
|
||||
fn act(&mut self, now: Instant) {
|
||||
if let Ok(msg) = self.api_receiver.try_recv() {
|
||||
self.act_machine_message(msg);
|
||||
}
|
||||
|
||||
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.emit_state();
|
||||
self.last_state_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 => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) => {
|
||||
// Does not connect to any Machine; do nothing
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {
|
||||
// Does not connect to any Machine; do nothing
|
||||
}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::to_value(self.get_state())
|
||||
.expect("Failed to serialize state"),
|
||||
live_values: serde_json::Value::Null,
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{MachineApi, digital_input_test_machine::DigitalInputTestMachine};
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct StateEvent {
|
||||
pub led_on: [bool; 4],
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("StateEvent", self.clone())
|
||||
}
|
||||
}
|
||||
pub enum DigitalInputTestMachineEvents {
|
||||
State(Event<StateEvent>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "action", content = "value")]
|
||||
pub enum Mutation {
|
||||
SetLed { index: usize, on: bool },
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DigitalInputTestMachineNamespace {
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<DigitalInputTestMachineEvents> for DigitalInputTestMachineNamespace {
|
||||
fn emit(&mut self, events: DigitalInputTestMachineEvents) {
|
||||
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<DigitalInputTestMachineEvents> for DigitalInputTestMachineEvents {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
match self {
|
||||
DigitalInputTestMachineEvents::State(event) => event.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_first_and_last_event()
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineApi for DigitalInputTestMachine {
|
||||
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
|
||||
self.api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, _value: serde_json::Value) -> Result<(), anyhow::Error> {
|
||||
//Digital Input Test Machine does not Set Values
|
||||
// let mutation: Mutation = serde_json::from_value(value)?;
|
||||
// match mutation {
|
||||
// Mutation::SetLed { index, on } => self.set_led(index, on),
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
AsyncThreadMessage, DIGITAL_INPUT_TEST_MACHINE, Machine, MachineMessage, VENDOR_QITECH,
|
||||
digital_input_test_machine::api::{
|
||||
DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent,
|
||||
},
|
||||
machine_identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
};
|
||||
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod new;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalInputTestMachine {
|
||||
pub api_receiver: Receiver<MachineMessage>,
|
||||
pub api_sender: Sender<MachineMessage>,
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
pub namespace: DigitalInputTestMachineNamespace,
|
||||
pub last_state_emit: Instant,
|
||||
pub led_on: [bool; 4],
|
||||
pub digital_input: [DigitalInput; 4],
|
||||
}
|
||||
|
||||
impl Machine for DigitalInputTestMachine {
|
||||
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 DigitalInputTestMachine {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: DIGITAL_INPUT_TEST_MACHINE,
|
||||
};
|
||||
|
||||
pub fn get_state(&self) -> StateEvent {
|
||||
StateEvent {
|
||||
led_on: self.led_on,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
//Set Led_On after DigitalInput
|
||||
let mut i = 0;
|
||||
|
||||
for di in &self.digital_input {
|
||||
let value = match di.get_value() {
|
||||
Ok(v) => v,
|
||||
Err(_) => false,
|
||||
};
|
||||
self.led_on[i] = value;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let event = StateEvent {
|
||||
led_on: self.led_on,
|
||||
}
|
||||
.build();
|
||||
self.namespace
|
||||
.emit(DigitalInputTestMachineEvents::State(event));
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{
|
||||
devices::{
|
||||
EthercatDevice, downcast_device,
|
||||
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
|
||||
wago_modules::wago_750_402::{Wago750_402, Wago750_402InputPort},
|
||||
},
|
||||
io::digital_input::DigitalInput,
|
||||
};
|
||||
use smol::{block_on, lock::RwLock};
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait,
|
||||
digital_input_test_machine::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace},
|
||||
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
impl MachineNewTrait for DigitalInputTestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
.iter()
|
||||
.map(|device_identification| device_identification.clone())
|
||||
.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!(
|
||||
"[{}::EtherCATMachine/TestMachine::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
block_on(async {
|
||||
// Example usage of a Wago Coupler and a 750-402 in the first slot, where the Output Port 1,2,3,4 is used
|
||||
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
|
||||
hardware,
|
||||
params,
|
||||
0,
|
||||
[WAGO_750_354_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
|
||||
|
||||
let mut coupler = _wago_750_354.0.write().await;
|
||||
|
||||
for module in modules {
|
||||
coupler.set_module(module);
|
||||
}
|
||||
|
||||
coupler.init_slot_modules(_wago_750_354.1);
|
||||
let dev = coupler.slot_devices.get(0).unwrap().clone().unwrap();
|
||||
let wago750_402: Arc<RwLock<Wago750_402>> = downcast_device::<Wago750_402>(dev).await?;
|
||||
|
||||
let di1 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI1);
|
||||
let di2 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI2);
|
||||
let di3 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI3);
|
||||
let di4 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI4);
|
||||
|
||||
drop(coupler);
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut my_test = Self {
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: DigitalInputTestMachineNamespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_state_emit: Instant::now(),
|
||||
led_on: [false; 4],
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
digital_input: [di1, di2, di3, di4],
|
||||
};
|
||||
my_test.emit_state();
|
||||
Ok(my_test)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -21,20 +21,16 @@ pub mod analog_input_test_machine;
|
||||
pub mod aquapath1;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub mod buffer1;
|
||||
pub mod digital_input_test_machine;
|
||||
pub mod extruder1;
|
||||
pub mod extruder2;
|
||||
pub mod ip20_test_machine;
|
||||
pub mod laser;
|
||||
pub mod machine_identification;
|
||||
pub mod mock;
|
||||
pub mod motor_test_machine;
|
||||
pub mod registry;
|
||||
pub mod serial;
|
||||
pub mod test_machine;
|
||||
pub mod wago_8ch_dio_test_machine;
|
||||
pub mod wago_ai_test_machine;
|
||||
pub mod wago_do_test_machine;
|
||||
pub mod wago_power;
|
||||
pub mod winder2;
|
||||
|
||||
@@ -51,11 +47,7 @@ pub const TEST_MACHINE: u16 = 0x0033;
|
||||
pub const IP20_TEST_MACHINE: u16 = 0x0034;
|
||||
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 WAGO_8CH_IO_TEST_MACHINE: u16 = 0x0041;
|
||||
|
||||
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
|
||||
pub const WAGO_DO_TEST_MACHINE: u16 = 0x000E;
|
||||
use serde_json::Value;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
|
||||
@@ -63,8 +63,6 @@ impl MachineIdentification {
|
||||
x if x == IP20_TEST_MACHINE => "ip20_test_machine".to_string(),
|
||||
x if x == ANALOG_INPUT_TEST_MACHINE => "analog_input_test_machine".to_string(),
|
||||
x if x == WAGO_AI_TEST_MACHINE => "wago_ai_test_machine".to_string(),
|
||||
x if x == WAGO_DO_TEST_MACHINE => "wago_do_test_machine".to_string(),
|
||||
|
||||
_ => unreachable!("Unknown machine id"),
|
||||
}
|
||||
}
|
||||
@@ -189,7 +187,6 @@ use crate::MACHINE_WINDER_V1;
|
||||
use crate::TEST_MACHINE;
|
||||
use crate::VENDOR_QITECH;
|
||||
use crate::WAGO_AI_TEST_MACHINE;
|
||||
use crate::WAGO_DO_TEST_MACHINE;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineIdentificationAddresses {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::analog_input_test_machine::AnalogInputTestMachine;
|
||||
use crate::digital_input_test_machine::DigitalInputTestMachine;
|
||||
use crate::ip20_test_machine::IP20TestMachine;
|
||||
use crate::wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine;
|
||||
use crate::wago_ai_test_machine::WagoAiTestMachine;
|
||||
use crate::wago_do_test_machine::WagoDOTestMachine;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use crate::{
|
||||
extruder1::mock::ExtruderV2 as ExtruderV2Mock1, extruder2::mock::ExtruderV2 as ExtruderV2Mock2,
|
||||
@@ -26,7 +23,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};
|
||||
|
||||
@@ -135,15 +131,6 @@ lazy_static! {
|
||||
|
||||
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<MotorTestMachine>(MotorTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<WagoDOTestMachine>(WagoDOTestMachine::MACHINE_IDENTIFICATION);
|
||||
mc.register::<Wago8chDigitalIOTestMachine>(
|
||||
Wago8chDigitalIOTestMachine::MACHINE_IDENTIFICATION,
|
||||
);
|
||||
|
||||
mc
|
||||
};
|
||||
}
|
||||
@@ -78,6 +78,7 @@ impl MachineNewTrait for TestMachine {
|
||||
let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
|
||||
let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
|
||||
let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut my_test = Self {
|
||||
api_receiver: receiver,
|
||||
|
||||