Compare commits

..
Author SHA1 Message Date
just-some-entity 18171290b8 finalize prototype 2026-01-30 14:41:14 +00:00
just_some_entity 6885d992f9 initial commit 2026-01-30 14:41:14 +00:00
Christian e9adc61821 Enhance BottleSorter implementation with IP20 module configuration and improved DC sync handling 2026-01-30 14:41:14 +00:00
Christian 1915aaa3f4 Add Minimal Bottle Sorter machine implementation with control and routing 2026-01-30 14:41:14 +00:00
kraemr ce570f69bf fixed formatting 2026-01-30 14:41:14 +00:00
kraemr 6e8b69f976 fixed validation error 2026-01-30 14:41:14 +00:00
kraemr 5b2c6d1110 fixed formatting 2026-01-30 14:41:14 +00:00
Oshgnacknak 14dbbb6145 Fix rust enum zod check 2026-01-30 14:41:14 +00:00
kraemr 9b9a1e3ff9 fixed broken schema for ethercat devices event 2026-01-30 14:41:14 +00:00
Christian 48b5d26edc Set npmDepsHash for Electron package 2026-01-30 14:41:14 +00:00
Christian 7ecbcc519c Remove npmDepsHash for improved compatibility 2026-01-30 14:41:14 +00:00
Christian 732f89caef Updated package-lock 2026-01-30 14:41:14 +00:00
Christian 7e29db5f50 Update xlsx dependency to version 0.20.3 for improved security 2026-01-30 14:41:14 +00:00
Oshgnacknak 8beddf95ac Rest API: Docs 2026-01-30 14:41:14 +00:00
Oshgnacknak 8867a19d90 Rest API: Allow posting mutations to each machine 2026-01-30 14:41:14 +00:00
Oshgnacknak 81f2ab61d7 Rest API: Getting values from all machines 2026-01-30 14:41:14 +00:00
Oshgnacknak 8d1ce9ac61 Rest API: Getting values from all mock machines 2026-01-30 14:41:14 +00:00
Oshgnacknak 2c13d7844c Rest API: Get mock winder v1 values 2026-01-30 14:41:14 +00:00
Oshgnacknak 28dd994ef5 List machines in rest api 2026-01-30 14:41:14 +00:00
126 changed files with 2882 additions and 4790 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
.png filter=lfs diff=lfs merge=lfs -text
.jpg filter=lfs diff=lfs merge=lfs -text
.jpeg filter=lfs diff=lfs merge=lfs -text
-40
View File
@@ -1,40 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: 'BUG: '
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Navigate to '...'
2. Select/Press '....'
3. Execute operation '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/Photos**
If applicable, add screenshots from the HMI display or photos of the machine screen to help explain your problem.
**Machine Information (please complete the following):**
- Machine Type: [Winder2, Laser, Aquapath...]
- Control Software Version: [e.g. 2.4.1]
- Settings: [e.g. Different Gear Ratios, PID Settings, …]
**Device Information (if issue occurs on external device):**
- Device Type: [e.g. Tablet, PLC, Remote Control Panel]
- OS: [e.g. Windows 10, Linux, etc.]
- Connection Type: [e.g. Ethernet, USB]
**Error Messages/Codes**
If applicable, provide exact error codes or messages displayed or found in the troubleshooting terminal.
**Additional context**
Add any other context about the problem here (e.g., recent software updates, environmental factors).
-33
View File
@@ -1,33 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: 'Feat: '
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. During operation, I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions, workarounds, or features you've considered.
**Use Case/Application:**
- Machine Type(s): [e.g. All machines, Winder, Aquapath, ...]
- Frequency of Use: [e.g. Daily, During setup, Occasionally]
**Expected Benefits:**
- Efficiency Improvement: [e.g. Reduces setup time by X minutes, Eliminates manual steps]
- Safety Enhancement: [e.g. Prevents operator error, Adds safety checks]
- Production Impact: [e.g. Increases throughput, Reduces downtime, Improves quality]
- User Experience: [e.g. Simplifies workflow, Reduces training time]
**Mockups/Sketches**
If applicable, add sketches, mockups, or photos showing how the feature would appear on the HMI display or integrate with the machine.
**Additional context**
Add any other context, industry standards, or examples from other control systems about the feature request here.
-80
View File
@@ -1,80 +0,0 @@
name: Update npm deps hash
on:
pull_request:
paths:
- 'electron/package-lock.json'
- 'electron/package.json'
workflow_dispatch:
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
permissions:
contents: write
pull-requests: write
actions: write
jobs:
update-hash:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v34
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Calculate new npm deps hash
id: calc-hash
run: |
# Calculate the hash using nix run with nixpkgs
NEW_HASH=$(nix run 'nixpkgs#prefetch-npm-deps' electron/package-lock.json)
echo "hash=$NEW_HASH" >> $GITHUB_OUTPUT
echo "Calculated hash: $NEW_HASH"
- name: Update npmDepsHash in electron.nix
run: |
NEW_HASH="${{ steps.calc-hash.outputs.hash }}"
sed -i "s|npmDepsHash = \".*\";|npmDepsHash = \"$NEW_HASH\";|" nixos/packages/electron.nix
- name: Check if hash changed
id: check-changes
run: |
if git diff --quiet nixos/packages/electron.nix; then
echo "changed=false" >> $GITHUB_OUTPUT
echo "No changes needed - hash is already correct"
else
echo "changed=true" >> $GITHUB_OUTPUT
echo "Hash updated in electron.nix"
fi
- name: Commit and push if changed
if: steps.check-changes.outputs.changed == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add nixos/packages/electron.nix
git commit -m "chore: update npmDepsHash to ${{ steps.calc-hash.outputs.hash }}"
git push
- name: Comment on PR
if: steps.check-changes.outputs.changed == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Automatically updated `npmDepsHash` to `${{ steps.calc-hash.outputs.hash }}`\n\n**Next step:** Trigger the checks by pushing an empty commit:\n```bash\ngit commit --allow-empty -m "trigger ci" && git push\n```'
})
+1 -2
View File
@@ -7,5 +7,4 @@ build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
.idea/
compile_metrics.csv
View File
Whitespace-only changes.
+1 -12
View File
@@ -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
Generated
+40 -50
View File
@@ -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"
+137 -37
View File
@@ -2,67 +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://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click here to watch a video demo of our software.*
- [Getting Started](https://github.com/qitechgmbh/control/wiki/Getting-Started)
- [Architecture Overview](https://github.com/qitechgmbh/control/wiki/Architecture-Overview)
- [EtherCAT Basics](https://github.com/qitechgmbh/control/wiki/Ethercat-Basics)
- [Device Examples](https://github.com/qitechgmbh/control/wiki/Hardware-Examples)
- [REST API Reference](https://github.com/qitechgmbh/control/wiki/Rest-Api)
[![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)
*Click here to watch a full explainer Video of our Software.*
## Videos
# Repo Structure
| Software Demo | Full Explainer |
|---------------|----------------|
| [![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)<br/>*Click to watch a demo of the control software in action* | [![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)<br/>*Click to watch a complete overview of QiTech Control* |
Frontend
## Repository Structure
- `/electron`: Frontend code for the control software built with React and Electron.
- **`/electron`** - React + Electron frontend
- **`/server`** - Rust backend implementing machine logic
- **`/ethercat-hal`** - Hardware abstraction layer for EtherCAT devices
- **`/control-core`** - Core control logic
- **`/nixos`** - Custom Linux OS with realtime kernel
Backend
## Technology Stack
- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
- `/ethercat-hal-derive`: Macros for `ethercat-hal`
- `/control-core`: Core control logic for the server.
**Backend:** Rust with [Ethercrab](https://github.com/ethercrab-rs/ethercrab) for EtherCAT, [SocketIO](https://socket.io/) for real-time communication, [axum](https://docs.rs/axum/latest/axum/) for REST API
Operating System
**Frontend:** [Electron](https://www.electronjs.org/) + [React](https://react.dev/) with [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) styling
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
Other
- `/docs`: Documentation for the project.
# Technology Choices
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master software. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
We use [Smol](https://github.com/smol-rs/smol) for EtherCAT IO in the control loop for it's performance and [Tokio](https://tokio.rs/) for server IO because of it's ecosystem and maturity.
## Frontend
We could combine the code of the frontend and backend using [Doxius](https://dioxuslabs.com/) but it lacks good Linux support. We chose [Electron](https://www.electronjs.org/) with [React](https://react.dev/) for it's maturity and ecosystem. For the UI we use [Shadcn](https://ui.shadcn.com/) components and [Tailwind](https://tailwindcss.com/) for styling. For routing we use [TanStack Router](https://tanstack.com/router/v1).
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
## 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
## License
- [x] [Actors](./docs/actors.md)
- [ ] SocketIO
- Namespaces & Caching
- Joining leaving namespaces
- NamespaceId
- Caching
- Serverside Caching
- Clientside Caching
- [ ] REST
See [LICENSE](LICENSE) file for details.
- [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)
+2 -2
View File
@@ -9,11 +9,11 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
axum = "0.8.8"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serialport = "4.7.3"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

-81
View File
@@ -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.
![](../assets/wiring.png)
---
We supply power using a **DC hollow-plug adapter**, like this one:
https://www.amazon.de/dp/B093FTFZ8Q
Perform the following wiring on the EK1100:
1. Red wire **(+24 V)** → Terminal **2**
2. Black wire **(0 V)** → Terminal **3**
3. Jumper wire from **Terminal 1 → Terminal 6**
4. Jumper wire from **Terminal 5 → Terminal 7**
After wiring, your module should look like **Figure 1**.
---
#### **Figure 1 — EK1100 Minimal Wiring**
<img src="../assets/ek1100.jpeg" width="400">
---
#### **Figure 2 — EL7031-0030 Terminal**
<img src="../assets/EL7031_0030.jpg" width="300">
Slide the EL7031-0030 onto the right side of the EK1100 until it locks.
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
---
#### **Figure 3 — Motor**
<img src="../assets/motor.jpg" width="300">
---
#### **Figure 4 — Motor Wiring**
<img src="../assets/motor_wiring.jpg" width="300">
Now the motor is wired via the pins 4, 5, 12, 13 on the EL7031-0030.
---
#### **Figure 5 — EL7031 Integration Connected**
<img src="../assets/EL7031_0030_connected.jpg" width="400">
That's what the pin should look like.
---
### 3.2.2 Wiring (EL7031-0030)
**Crucial:** The EL7031 requires two power sources: E-Bus (side contacts) for logic, and Front Terminal (6 & 14) for motor power.
| Terminal Point | Function | Cable Color (Example) |
| :------------- | :--------------- | :-------------------- |
| **4** | Motor Coil A1 | Red |
| **12** | Motor Coil A2 | Blue |
| **5** | Motor Coil B1 | Green |
| **13** | Motor Coil B2 | Black |
| **6** | **Power +24V** | PSU Red (+) |
| **14** | **Power 0V/GND** | PSU Black (-) |
---
### 3.3 ⚠️ Safety Warning
- **Mandatory Power Supply:** Without 24V connected to **Pin 6 (+24V)** and **Pin 14 (GND)**, the terminal will remain in `PREOP` state or show a "Warning" LED (No Power). The motor will **not** move without this external supply.
#### Risk of Destruction (Short Circuit):
- **Never** connect the 24V Power Supply to the Motor Output pins (**4, 5, 12, 13**). This will instantly destroy the terminal (causing pins 12 & 13 to glow red/burn).
- Ensure strict separation: **Pins 4/5/12/13 are for the MOTOR ONLY**.
- **Pins 6/14 are for POWER ONLY**.
Also, see the documentation of the EtherCAT terminal ([EL7031-0030](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)) for the different power modes (page 46 ff.)
---
## 4. Software Setup
### 4.1 Installing on Linux (this depends on your distro)
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
### 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
### 4.3 Running the Frontend
```bash
# move to the control directory
cd electron
npm run start
```
This launches the QiTech Control dashboard.
---
## 5. Demo
### 5.1 Assigning Devices in the Dashboard
Once the backend + frontend are running, you should see:
![](../assets/UI_start_motor.png)
Make sure that under "Assign" → "Machine Assignment"
the correct serial number is selected (each device should have the same one, here it is 1).
Under "Machine" select your new Machine however you named it (I named it "TestMotor V1"), else it will detect that there is something connected to it but the connection won't work.
![](../assets/machine_assigment_motor.png)
**NOW THE MOTOR SHOULD TURN!**
In the interface "TestMotor" on the left side, you can now control the motor (its state and its speed).
![](../assets/UI_motor.png)
---
## 6. Documentation
Use the official documentation of the EL7031-0030 for more information:
[Beckhoff EL7031-0030 Documentation](https://download.beckhoff.com/download/document/io/ethercat-terminals/el7031-0030de.pdf)
---
## 7. Software Architecture
### Backend (Rust)
Located in [machines/src/motor_test_machine/](/machines/src/motor_test_machine/).
1. **`mod.rs`**: Defines the `MotorTestMachine` struct and holds the state (driver wrapper, enabled state, target velocity).
2. **`api.rs`**: Handles incoming JSON commands from the frontend (Enable/Disable, Set Velocity) via WebSockets/SocketIO.
3. **`act.rs`**: The real-time control loop. It updates the `StepperVelocityEL70x1` driver wrapper in every cycle based on the current state.
4. **`new.rs`**: Initializes the hardware.
### Frontend (TypeScript/React)
Located in [electron/src/machines/motor_test_machine/](/electron/src/machines/motor_test_machine/) and [electron/src/routes/routes.tsx](/electron/src/routes/routes.tsx).
1. **`useTestMotor.ts`**: Custom hook managing the optimistic state and communication with the backend.
2. **`TestMotorControlPage.tsx`**: The UI using QiTech UI components (`ControlCard`, `EditValue`, `SelectionGroupBoolean`) to match the look and feel of the Winder2.
3. **Routing**: Integrated into `routes.tsx` using TanStack Router, ensuring the machine appears in the sidebar and navigation works correctly.
@@ -1,196 +0,0 @@
# Minimal Example — Analog Input Wago 750-455
## Table of Contents
1. [Introduction](#1-introduction)
2. [Requirements](#2-requirements)
3. [Hardware Setup](#3-hardware-setup)
4. [Software Setup](#4-software-setup)
5. [Demo](#5-demo)
6. [References](#6-references)
## 1. Introduction
The Wago 750-455 Analog Input example demonstrates how to measure analog current on a **Wago 750-455 EtherCAT module** using the QiTech machine framework. This module provides 4 independent analog input channels that can read 4-20mA signals.
## 2. Requirements
### Hardware
- **Wago 750-354 EtherCAT Coupler**
- **Wago 750-602 Power Supply** (24V VDC)
- **Wago 750-455 EtherCAT Module** (4-channel analog input, 4-20mA)
- **Wago 750-600 End Module**
- **24 V DC power supply** for the Wago system
- **Adjustable DC power supply** (or any other means of producing 4-20mA DC @ <10V)
- Jumper / bridge wires (0.5–1.5 mm² recommended)
- A **Linux PC** (Ubuntu/Debian recommended)
- Standard Ethernet cable
- Flat screwdriver
### Software
*(Installation steps in Section 4)*
- Rust toolchain
- Node.js + npm
- Git
- QiTech Control repository
- EtherCAT HAL (included inside repo)
## 3. Hardware Setup
### 3.1 Overview
The Wago 750-455 is a 4-channel analog input module that measures current signals in the 4-20mA range. Each channel has its own input terminals and can operate independently.
#### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT modules can cause serious damage or electrical shock.
### 3.2 Wiring Procedure
#### 3.2.1 Safe Wiring (Wago Spring Clamp)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](../assets/wiring.png)
---
### 3.3 Wago 750-455 Module Wiring
The Wago 750-455 has 4 analog input channels. Each channel requires two terminals:
**Channel 1 (AI1):**
- Terminal 1: ⊕ (Positive input)
- Terminal 2: ⊖ (Negative input/Ground)
**Channel 2 (AI2):**
- Terminal 3: ⊕ (Positive input)
- Terminal 4: ⊖ (Negative input/Ground)
**Channel 3 (AI3):**
- Terminal 5: ⊕ (Positive input)
- Terminal 6: ⊖ (Negative input/Ground)
**Channel 4 (AI4):**
- Terminal 7: ⊕ (Positive input)
- Terminal 8: ⊖ (Negative input/Ground)
For this minimal example, we'll connect at least one channel to an adjustable power supply:
1. Wire adjustable power supply ⊕ to **Terminal 1** (Channel 1 positive)
2. Wire adjustable power supply ⊖ to **Terminal 2** (Channel 1 negative)
*Repeat for additional channels as needed.*
---
### 3.4 Wago I/O System Integration
The Wago 750-455 module plugs into a Wago fieldbus system. Consult your specific Wago controller/coupler documentation for:
- Power supply requirements
- Network configuration
- Module placement in the I/O rack
The module automatically connects to the backplane for EtherCAT communication and power.
---
### 3.5 Power & Ethernet
#### Power
Ensure your Wago system has adequate 24V DC power connected per manufacturer specifications.
#### Ethernet
Connect your PC to the Wago controller/coupler using a standard Ethernet cable.
---
## 4. Software Setup
### 4.1 Installing on Ubuntu/Debian
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
### 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
Ensure the Wago controller is connected and powered.
### 4.3 Running the Frontend
```bash
cd electron
npm run start
```
This launches the QiTech Control dashboard.
---
## 5. Demo
### 5.1 Assigning Devices in the Dashboard
Once both backend and frontend are running:
1. Navigate to **Setup → EtherCAT** page
2. You should see your Wago controller and the 750-455 module listed
3. Select the Device called 750-354 and assign the Wago AI Test V1 Machine, set the Serial to 1 and set the device role to 0 (Wago Bus Coupler)
4. Press write thn restart the backend to load the changes
### 5.2 Testing Analog Input
1. Set your adjustable power supply to output a current in the 4-20mA range
2. In the dashboard, navigate to the monitoring view
3. Observe the analog input value for the connected channel(s)
### 5.3 Channel Specifications
The Wago 750-455 provides:
- **Input Range:** 4-20mA
- **Wiring Error Detection:** Built-in detection for open/short circuit conditions
- **4 Independent Channels:** Each can be read separately
### 5.4 Error Detection
The module includes wiring error detection. If you see a wiring error flag:
- Check that your current source is within the 4-20mA range
- Verify all wire connections are secure
- Ensure the current loop is properly closed
---
## 6. References
### Wago Documentation
- [Wago 750-455 Product Page](https://www.wago.com/de-en/io-systems/4-channel-analog-input/p/750-455)
- Wago I/O System manual
- EtherCAT configuration guide for Wago systems
### QiTech Control
- [Architecture Overview](../architecture-overview.md)
- [EtherCAT Basics](../ethercat-basics.md)
- [Device Documentation](../devices.md)
- [Getting Started Guide](getting-started.md)
### Related Examples
- [Minimal Example — EL3021](minimal-example-el3021.md) (Single channel analog input)
- [Minimal Example — EL2004](minimal-example-el2004.md) (Digital output)
@@ -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
![](../assets/WAGO750_402_schematic.png)
---
#### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](../assets/wiring.png)
---
### 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)
![](../assets/WAGO750_402_discovery.png)
Steps:
- Click Assign on the wago module
- Select Digital Input Machine V1
![](../assets/WAGO750_402_setmachine.png)
- Enter a serial number except for 0
![](../assets/WAGO750_402_serserial.png)
- Click Write
### 5.2 Diyplaying LED
Navigate to:
Machines → DigitalInputMachine
![](../assets/WAGO750_402_machinedetected.png)
You will see this interface:
![](../assets/WAGO750_402_machinecontrol1.png)
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
![](../assets/WAGO750_402_machinecontrol2.png)
## 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)
+2 -8
View File
@@ -10,15 +10,9 @@ The Qitech Control Panel exposes a small HTTP interface for discovering machines
## Authentication
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. It is therefore suggested to run the entire production line in isolated operation.
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks.
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity. To let the outside world communicate with the production line, a router should be placed at the network bondary bridging the production line's subnet and the rest of the network. The router can run in **client/bridge mode** on the line's subnet and expose the panel's API via **port forwading** or through a **reverse proxy** where one can add authentication, logging, rate limiting, etc.
The following graphic depicts the suggested network topology for a single production line. Many lines could potentially share the same router.
<p align="center">
<img width="769" height="528" alt="Suggested network topology" src="./assets/subnet.png" />
</p>
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity (if configured). If you need authentication or access from outside the isolated subnet, place a router/device in **client/bridge mode** on the panel network and expose the panel through a **reverse proxy** where you can add authentication, logging, rate limiting, etc.
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
+274 -142
View File
@@ -1,12 +1,12 @@
{
"name": "qitech-control-electron",
"version": "2.4.1",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qitech-control-electron",
"version": "2.4.1",
"version": "1.0.0",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
@@ -40,12 +40,12 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^16.5.4",
"react-i18next": "^15.4.1",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.3",
"socket.io-client": "^4.8.1",
"socket.io-msgpack-parser": "^3.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
@@ -54,7 +54,7 @@
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.3.5",
"zod-validation-error": "^5.0.0",
"zustand": "^5.0.11"
"zustand": "^5.0.8"
},
"devDependencies": {
"@electron/fuses": "^1.8.0",
@@ -73,7 +73,7 @@
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.4",
@@ -90,7 +90,7 @@
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^4.0.18"
"vitest": "^3.0.6"
}
},
"node_modules/@adobe/css-tools": {
@@ -1305,9 +1305,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2889,13 +2889,6 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
@@ -4123,40 +4116,39 @@
}
},
"node_modules/@vitest/expect": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.0.18",
"@vitest/utils": "4.0.18",
"chai": "^6.2.1",
"tinyrainbow": "^3.0.3"
"@vitest/spy": "3.2.4",
"@vitest/utils": "3.2.4",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.0.18",
"@vitest/spy": "3.2.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
"magic-string": "^0.30.17"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0-0"
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
@@ -4168,41 +4160,42 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^3.0.3"
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.0.18",
"pathe": "^2.0.3"
"@vitest/utils": "3.2.4",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.18",
"magic-string": "^0.30.21",
"@vitest/pretty-format": "3.2.4",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
"funding": {
@@ -4210,24 +4203,28 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^4.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.18",
"tinyrainbow": "^3.0.3"
"@vitest/pretty-format": "3.2.4",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -4642,6 +4639,16 @@
"node": "*"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -4762,11 +4769,18 @@
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=18"
}
@@ -4828,6 +4842,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -5110,6 +5134,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -5778,9 +5812,9 @@
}
},
"node_modules/eslint": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5791,7 +5825,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.2",
"@eslint/js": "9.39.1",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -8035,6 +8069,13 @@
"loose-envify": "cli.js"
}
},
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -9191,17 +9232,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -9390,6 +9420,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -9746,17 +9786,16 @@
}
},
"node_modules/react-i18next": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz",
"integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"i18next": ">= 23.4.0",
"react": ">= 16.8.0",
"typescript": "^5"
},
@@ -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"
+7 -7
View File
@@ -1,8 +1,8 @@
{
"name": "qitech-control-electron",
"productName": "QiTech Control",
"version": "2.4.1",
"description": "Next Generation Machine Interface ",
"version": "1.0.0",
"description": "",
"main": ".vite/build/main.js",
"private": true,
"scripts": {
@@ -36,7 +36,7 @@
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.4",
@@ -53,7 +53,7 @@
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^4.0.18"
"vitest": "^3.0.6"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
@@ -88,12 +88,12 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^16.5.4",
"react-i18next": "^15.4.1",
"react-markdown": "^10.1.0",
"react-virtualized": "^9.22.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.3",
"socket.io-client": "^4.8.1",
"socket.io-msgpack-parser": "^3.0.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
@@ -102,6 +102,6 @@
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.3.5",
"zod-validation-error": "^5.0.0",
"zustand": "^5.0.11"
"zustand": "^5.0.8"
}
}
@@ -6,9 +6,7 @@ import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { useAquapath1 } from "./useAquapath";
import { SelectionGroup } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/control/Label";
import { HeatingZone } from "../../extruder/HeatingZone";
export function Aquapath1ControlPage() {
const {
@@ -18,12 +16,8 @@ export function Aquapath1ControlPage() {
back_flow,
front_temperature,
back_temperature,
front_revolutions,
back_revolutions,
front_power,
back_power,
front_total_energy,
back_total_energy,
front_temp_reservoir,
back_temp_reservoir,
setAquapathMode,
setFrontTemperature,
setBackTemperature,
@@ -35,55 +29,111 @@ export function Aquapath1ControlPage() {
const backTargetTemperature =
state?.temperature_states?.back.target_temperature ?? 0;
const frontCurrentTemperature =
state?.temperature_states?.front.temperature ?? 0;
const backCurrentTemperature =
state?.temperature_states?.back.temperature ?? 0;
const frontCoolingBoundary =
frontTargetTemperature + (state?.tolerance_states.front.cooling ?? 0);
const frontHeatingBoundary =
frontTargetTemperature - (state?.tolerance_states.front.heating ?? 0);
const backCoolingBoundary =
backTargetTemperature + (state?.tolerance_states.back.cooling ?? 0);
const backHeatingBoundary =
backTargetTemperature - (state?.tolerance_states.back.heating ?? 0);
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Reservoir 1">
<div className="grid grid-rows-5 gap-4">
<div className="flex flex-row">
<ControlCard title="Flow Measurements">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Flow"
label="Flow 1"
unit="l/min"
timeseries={front_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Temperature"
label="Flow 2"
unit="l/min"
timeseries={back_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
</div>
<div className="mt-8">
<Label label="Pump 1">
<SelectionGroup<"On" | "Off">
value={frontTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
if (value == "On") {
setFrontFlow(true);
} else {
setFrontFlow(false);
}
}}
/>
</Label>
<Label label="Pump 2">
<SelectionGroup<"On" | "Off">
value={backTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
if (value == "On") {
setBackFlow(true);
} else {
setBackFlow(false);
}
}}
/>
</Label>
</div>
</ControlCard>
<ControlCard title="Temperature Measurements">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Reservoir 1"
unit="C"
timeseries={front_temperature}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row gap-4">
<Label label="Set Target Temperature">
<Label label="Set Reservoir 1 Target Temperature">
<EditValue
title="Set Target Temperature"
min={0}
title="Set Reservoir 1 Target Temperature"
value={frontTargetTemperature}
max={80}
unit="C"
step={0.1}
min={0.0}
max={80.0}
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setFrontTemperature(val);
@@ -93,88 +143,23 @@ export function Aquapath1ControlPage() {
}
/>
</Label>
{frontCurrentTemperature < frontHeatingBoundary &&
(state?.flow_states.front.flow ?? 0) > 0 && (
<Badge variant="default">
Power: {front_power.current?.value} W<br />
Total: {front_total_energy.current?.value} kWh
</Badge>
)}
{frontCurrentTemperature > frontCoolingBoundary &&
frontTargetFlow &&
(state?.flow_states.front.flow ?? 0) && (
<Badge variant="secondary">Cooling</Badge>
)}
</div>
<div className="flex flex-row">
<div className="flex flex-col gap-8">
<TimeSeriesValueNumeric
label="Revolution Speed"
unit="%"
timeseries={front_revolutions}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row">
<Label label="Pump">
<SelectionGroup<"On" | "Off">
value={frontTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
setFrontFlow(value == "On");
}}
/>
</Label>
</div>
</div>
</ControlCard>
<ControlCard title="Reservoir 2">
<div className="grid grid-rows-4 gap-4">
<div className="flex flex-row">
<TimeSeriesValueNumeric
label="Flow"
unit="l/min"
timeseries={back_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row">
<TimeSeriesValueNumeric
label="Temperature"
label="Reservoir 2"
unit="C"
timeseries={back_temperature}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row gap-4">
<Label label="Set Target Temperature">
<Label label="Set Reservoir 2 Target Temperature">
<EditValue
title="Set Target Temperature"
min={0}
title="Set Reservoir 2 Target Temperature"
value={backTargetTemperature}
max={80}
unit="C"
step={0.1}
min={0.0}
max={80.0}
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setBackTemperature(val);
@@ -184,56 +169,6 @@ export function Aquapath1ControlPage() {
}
/>
</Label>
{backCurrentTemperature < backHeatingBoundary &&
backTargetFlow &&
(state?.flow_states.front.flow ?? 0) && (
<Badge variant="default">
Power: {back_power.current?.value} W<br />
Total: {back_total_energy.current?.value} kWh
</Badge>
)}
{backCurrentTemperature > backCoolingBoundary &&
frontTargetFlow &&
(state?.flow_states.front.flow ?? 0) && (
<Badge variant="secondary">Cooling</Badge>
)}
</div>
<div className="flex flex-row">
<TimeSeriesValueNumeric
label="Revolution Speed"
unit="%"
timeseries={back_revolutions}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row">
<Label label="Pump">
<SelectionGroup<"On" | "Off">
value={backTargetFlow ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => {
setBackFlow(value == "On");
}}
/>
</Label>
</div>
</div>
</ControlCard>
@@ -20,12 +20,6 @@ export function Aquapath1Page() {
icon: "lu:ChartSpline",
activeLink: "graphs",
},
{
link: "settings",
title: "Config",
icon: "lu:Settings",
activeLink: "settings",
},
]}
/>
);
@@ -1,121 +0,0 @@
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { ControlGrid } from "@/control/ControlGrid";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
import { useAquapath1 } from "./useAquapath";
import React from "react";
export function Aquapath1SettingsPage() {
const {
state,
setFrontRevolutions,
setBackRevolutions,
setFrontHeatingTolerance,
setFrontCoolingTolerance,
setBackHeatingTolerance,
setBackCoolingTolerance,
} = useAquapath1();
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Front Fan Revolutions">
<Label label="Set Max Revolution Speed">
<EditValue
title="Set Max Revolution Speed"
min={0}
value={state?.fan_states.front.revolutions}
max={100}
unit="%"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setFrontRevolutions(val);
}}
/>
</Label>
</ControlCard>
<ControlCard title="Back Fan Revolutions">
<Label label="Set Max Revolution Speed">
<EditValue
title="Set Max Revolution Speed"
min={0}
value={state?.fan_states.back.revolutions}
max={100}
unit="%"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setBackRevolutions(val);
}}
/>
</Label>
</ControlCard>
<ControlCard title="Front Temperature Tolerances">
<Label label="Set Heating Tolerance">
<EditValue
title="Set Heating Tolerance"
min={0}
value={state?.tolerance_states.front.heating}
max={10}
step={0.1}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setFrontHeatingTolerance(val);
}}
/>
</Label>
<Label label="Set Cooling Tolerance">
<EditValue
title="Set Cooling Tolerance"
min={0}
value={state?.tolerance_states.front.cooling}
max={10}
step={0.1}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setFrontCoolingTolerance(val);
}}
/>
</Label>
</ControlCard>
<ControlCard title="Back Temperature Tolerances">
<Label label="Set Heating Tolerance">
<EditValue
title="Set Heating Tolerance"
min={0}
value={state?.tolerance_states.back.heating}
max={10}
step={0.1}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setBackHeatingTolerance(val);
}}
/>
</Label>
<Label label="Set Cooling Tolerance">
<EditValue
title="Set Cooling Tolerance"
min={0}
value={state?.tolerance_states.back.cooling}
max={10}
step={0.1}
unit="C"
renderValue={(value) => value.toFixed(1)}
onChange={(val) => {
setBackCoolingTolerance(val);
}}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -51,23 +51,6 @@ export const flowStatesSchema = z.object({
back: flowStateSchema,
});
export const fanStateSchema = z.object({
revolutions: z.number(),
max_revolutions: z.number(),
});
export const fanStatesSchema = z.object({
back: fanStateSchema,
front: fanStateSchema,
});
export const toleranceStateSchema = z.object({
heating: z.number(),
cooling: z.number(),
});
export const toleranceStatesSchema = z.object({
back: toleranceStateSchema,
front: toleranceStateSchema,
});
/**
* Live values event schema (time-series data)
*/
@@ -78,12 +61,6 @@ export const liveValuesEventDataSchema = z.object({
back_temperature: z.number(),
front_temp_reservoir: z.number(),
back_temp_reservoir: z.number(),
front_revolutions: z.number(),
back_revolutions: z.number(),
back_power: z.number(),
front_power: z.number(),
front_total_energy: z.number(),
back_total_energy: z.number(),
});
/**
@@ -94,8 +71,6 @@ export const stateEventDataSchema = z.object({
mode_state: modeStateSchema,
flow_states: flowStatesSchema,
temperature_states: tempStatesSchema,
fan_states: fanStatesSchema,
tolerance_states: toleranceStatesSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -121,15 +96,6 @@ export type Aquapath1NamespaceStore = {
front_temp_reservoir: TimeSeries;
back_temp_reservoir: TimeSeries;
front_revolutions: TimeSeries;
back_revolutions: TimeSeries;
front_power: TimeSeries;
back_power: TimeSeries;
front_total_energy: TimeSeries;
back_total_energy: TimeSeries;
};
const { initialTimeSeries: front_temperature, insert: addTemperature1 } =
@@ -142,18 +108,6 @@ const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
createTimeSeries();
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries();
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries();
const { initialTimeSeries: front_revolutions, insert: addFan1 } =
createTimeSeries();
const { initialTimeSeries: back_revolutions, insert: addFan2 } =
createTimeSeries();
const { initialTimeSeries: front_power, insert: addFrontPower } =
createTimeSeries();
const { initialTimeSeries: back_power, insert: addBackPower } =
createTimeSeries();
const { initialTimeSeries: front_total_energy, insert: addFrontEnergy } =
createTimeSeries();
const { initialTimeSeries: back_total_energy, insert: addBackEnergy } =
createTimeSeries();
/**
* Factory function to create a new Aquapath namespace store
@@ -171,12 +125,6 @@ export const createAquapath1NamespaceStore =
back_flow: back_flow,
front_temp_reservoir: front_temp_reservoir,
back_temp_reservoir: back_temp_reservoir,
front_revolutions: front_revolutions,
back_revolutions: back_revolutions,
back_power: back_power,
front_power: front_power,
front_total_energy: front_total_energy,
back_total_energy: back_total_energy,
};
});
};
@@ -244,30 +192,6 @@ export function aquapath1MessageHandler(
value: liveValuesEvent.data.back_temp_reservoir,
timestamp: event.ts,
}),
front_revolutions: addFan1(state.front_revolutions, {
value: liveValuesEvent.data.front_revolutions,
timestamp: event.ts,
}),
back_revolutions: addFan2(state.back_revolutions, {
value: liveValuesEvent.data.back_revolutions,
timestamp: event.ts,
}),
front_power: addFrontPower(state.front_power, {
value: liveValuesEvent.data.front_power,
timestamp: event.ts,
}),
back_power: addBackPower(state.back_power, {
value: liveValuesEvent.data.back_power,
timestamp: event.ts,
}),
front_total_energy: addFrontEnergy(state.front_total_energy, {
value: liveValuesEvent.data.front_total_energy,
timestamp: event.ts,
}),
back_total_energy: addBackEnergy(state.back_total_energy, {
value: liveValuesEvent.data.back_total_energy,
timestamp: event.ts,
}),
}));
} else {
handleUnhandledEventError(eventName);
@@ -42,18 +42,12 @@ export function useAquapath1() {
const {
state,
defaultState,
front_temperature,
back_temperature,
front_flow,
back_flow,
front_temp_reservoir,
back_temp_reservoir,
front_revolutions,
back_revolutions,
front_power,
back_power,
front_total_energy,
back_total_energy,
front_temperature: front_temperature,
back_temperature: back_temperature,
front_flow: front_flow,
back_flow: back_flow,
front_temp_reservoir: front_temp_reservoir,
back_temp_reservoir: back_temp_reservoir,
} = useAquapath1Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -131,89 +125,6 @@ export function useAquapath1() {
);
};
const setFrontRevolutions = (revolutions: number) => {
updateStateOptimistically(
(current) => {
current.data.fan_states.back.revolutions = revolutions;
},
() => {
requestFrontRevolutions({
machine_identification_unique: machineIdentification,
data: { SetFrontRevolutions: revolutions },
});
},
);
};
const setBackRevolutions = (revolutions: number) => {
updateStateOptimistically(
(current) => {
current.data.fan_states.back.revolutions = revolutions;
},
() =>
requestBackRevolutions({
machine_identification_unique: machineIdentification,
data: { SetBackRevolutions: revolutions },
}),
);
};
const setFrontHeatingTolerance = (tolerance: number) => {
updateStateOptimistically(
(current) => {
current.data.tolerance_states.front.heating = tolerance;
},
() => {
requestFrontHeatingTolerance({
machine_identification_unique: machineIdentification,
data: { SetFrontHeatingTolerance: tolerance },
});
},
);
};
const setBackHeatingTolerance = (tolerance: number) => {
updateStateOptimistically(
(current) => {
current.data.tolerance_states.back.heating = tolerance;
},
() => {
requestBackHeatingTolerance({
machine_identification_unique: machineIdentification,
data: { SetBackHeatingTolerance: tolerance },
});
},
);
};
const setFrontCoolingTolerance = (tolerance: number) => {
updateStateOptimistically(
(current) => {
current.data.tolerance_states.front.cooling = tolerance;
},
() => {
requestFrontCoolingTolerance({
machine_identification_unique: machineIdentification,
data: { SetFrontCoolingTolerance: tolerance },
});
},
);
};
const setBackCoolingTolerance = (tolerance: number) => {
updateStateOptimistically(
(current) => {
current.data.tolerance_states.back.cooling = tolerance;
},
() => {
requestBackCoolingTolerance({
machine_identification_unique: machineIdentification,
data: { SetBackCoolingTolerance: tolerance },
});
},
);
};
// Mutation hooks
const { request: requestAquapathMode } = useMachineMutation(
z.object({ SetAquaPathMode: z.enum(["Standby", "Auto"]) }),
@@ -230,25 +141,6 @@ export function useAquapath1() {
const { request: requestBackFlow } = useMachineMutation(
z.object({ SetBackFlow: z.boolean() }),
);
const { request: requestFrontRevolutions } = useMachineMutation(
z.object({ SetFrontRevolutions: z.number() }),
);
const { request: requestBackRevolutions } = useMachineMutation(
z.object({ SetBackRevolutions: z.number() }),
);
const { request: requestFrontHeatingTolerance } = useMachineMutation(
z.object({ SetFrontHeatingTolerance: z.number() }),
);
const { request: requestFrontCoolingTolerance } = useMachineMutation(
z.object({ SetFrontCoolingTolerance: z.number() }),
);
const { request: requestBackHeatingTolerance } = useMachineMutation(
z.object({ SetBackHeatingTolerance: z.number() }),
);
const { request: requestBackCoolingTolerance } = useMachineMutation(
z.object({ SetBackCoolingTolerance: z.number() }),
);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
@@ -273,23 +165,11 @@ export function useAquapath1() {
back_temperature,
front_temp_reservoir,
back_temp_reservoir,
front_revolutions,
back_revolutions,
front_power,
back_power,
front_total_energy,
back_total_energy,
setAquapathMode,
setFrontTemperature,
setBackTemperature,
setFrontFlow,
setBackFlow,
setFrontRevolutions,
setBackRevolutions,
setFrontHeatingTolerance,
setBackHeatingTolerance,
setFrontCoolingTolerance,
setBackCoolingTolerance,
};
}
@@ -0,0 +1,155 @@
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 { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useBottleSorter } from "./useBottleSorter";
export function BottleSorterControlPage() {
const {
state,
liveValues,
setStepperSpeed,
setStepperEnabled,
pulseOutput,
} = useBottleSorter();
const safeState = state ?? {
outputs: [false, false, false, false, false, false, false, false],
stepper_speed_mm_s: 0,
stepper_enabled: false,
};
const safeLiveValues = liveValues ?? {
stepper_position: 0,
};
const handleSpeedChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value);
if (!isNaN(value)) {
setStepperSpeed(value);
}
};
return (
<Page>
<ControlGrid columns={2}>
{/* Stepper Motor Control */}
<ControlCard title="Stepper Motor Control">
<div className="flex flex-col gap-4">
<Label label="Speed (mm/s)">
<Input
type="number"
value={safeState.stepper_speed_mm_s}
onChange={handleSpeedChange}
step="0.1"
min="-100"
max="100"
/>
</Label>
<Label label="Direction">
<SelectionGroup<"Forward" | "Backward">
value={safeState.stepper_speed_mm_s >= 0 ? "Forward" : "Backward"}
orientation="horizontal"
options={{
Forward: {
children: "Forward",
icon: "lu:ArrowRight",
isActiveClassName: "bg-blue-600",
},
Backward: {
children: "Backward",
icon: "lu:ArrowLeft",
isActiveClassName: "bg-purple-600",
},
}}
onChange={(value) =>
setStepperSpeed(
value === "Forward"
? Math.abs(safeState.stepper_speed_mm_s)
: -Math.abs(safeState.stepper_speed_mm_s),
)
}
/>
</Label>
<Label label="Motor State">
<SelectionGroup<"Enabled" | "Disabled">
value={safeState.stepper_enabled ? "Enabled" : "Disabled"}
orientation="horizontal"
options={{
Disabled: {
children: "Disabled",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
},
Enabled: {
children: "Enabled",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
},
}}
onChange={(value) => setStepperEnabled(value === "Enabled")}
/>
</Label>
</div>
</ControlCard>
{/* Stepper Motor Status */}
<ControlCard title="Stepper Motor Status">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Current Position:</span>
<Badge className="bg-blue-500">
{safeLiveValues.stepper_position.toFixed(2)} mm
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Target Speed:</span>
<Badge className="bg-purple-500">
{safeState.stepper_speed_mm_s.toFixed(2)} mm/s
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Motor Status:</span>
<Badge
className={
safeState.stepper_enabled ? "bg-green-600" : "bg-gray-500"
}
>
{safeState.stepper_enabled ? "ENABLED" : "DISABLED"}
</Badge>
</div>
</div>
</ControlCard>
{/* Digital Output Pulse Controls */}
<ControlCard title="Digital Output Pulse Controls" className="col-span-2">
<div className="grid grid-cols-4 gap-4">
{safeState.outputs.map((output, index) => (
<div key={index} className="flex flex-col gap-2">
<Label label={`Output ${index + 1}`}>
<Button
onClick={() => pulseOutput(index)}
className={`w-full ${output ? "bg-green-600 hover:bg-green-700" : ""}`}
variant={output ? "default" : "outline"}
>
{output ? "PULSING" : "PULSE"}
</Button>
</Label>
</div>
))}
</div>
<div className="mt-4 text-sm text-muted-foreground">
Click a button to pulse the output for 100ms
</div>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,21 @@
import { Topbar } from "@/components/Topbar";
import { bottleSorterSerialRoute } from "@/routes/routes";
import React from "react";
export function BottleSorterPage(): React.JSX.Element {
const { serial } = bottleSorterSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/bottlesorter/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:ToggleLeft",
},
]}
/>
);
}
@@ -0,0 +1,91 @@
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({
outputs: z.array(z.boolean()).length(8),
stepper_speed_mm_s: z.number(),
stepper_enabled: z.boolean(),
});
export const liveValuesEventDataSchema = z.object({
stepper_position: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
export type LiveValuesEvent = z.infer<typeof liveValuesEventDataSchema>;
// ========== Store ==========
export type BottleSorterNamespaceStore = {
state: StateEvent | null;
liveValues: LiveValuesEvent | null;
};
export const createBottleSorterNamespaceStore =
(): StoreApi<BottleSorterNamespaceStore> =>
create<BottleSorterNamespaceStore>(() => ({
state: null,
liveValues: null,
}));
// ========== Message Handler ==========
export function bottleSorterMessageHandler(
store: StoreApi<BottleSorterNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<BottleSorterNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: BottleSorterNamespaceStore,
) => BottleSorterNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore((current) => ({ ...current, state: parsed.data }));
} else if (event.name === "LiveValuesEvent") {
const parsed = liveValuesEventSchema.parse(event);
updateStore((current) => ({ ...current, liveValues: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useBottleSorterNamespaceImplementation =
createNamespaceHookImplementation<BottleSorterNamespaceStore>({
createStore: createBottleSorterNamespaceStore,
createEventHandler: bottleSorterMessageHandler,
});
export function useBottleSorterNamespace(
machine_identification_unique: MachineIdentificationUnique,
): BottleSorterNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useBottleSorterNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,118 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { bottleSorterSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import {
useBottleSorterNamespace,
StateEvent,
LiveValuesEvent,
} from "./bottleSorterNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { bottleSorter } from "@/machines/properties";
import { z } from "zod";
export function useBottleSorter() {
const { serial: serialString } = bottleSorterSerialRoute.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: bottleSorter.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state, liveValues } = useBottleSorterNamespace(
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 setStepperSpeed = (speed_mm_s: number) => {
updateStateOptimistically(
(current) => {
current.stepper_speed_mm_s = speed_mm_s;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetStepperSpeed", value: { speed_mm_s } },
}),
);
};
const setStepperEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.stepper_enabled = enabled;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetStepperEnabled", value: { enabled } },
}),
);
};
const pulseOutput = (index: number) => {
// Immediately pulse the output
updateStateOptimistically(
(current) => {
current.outputs[index] = true;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "PulseOutput", value: { index } },
}),
);
};
return {
state: stateOptimistic.value,
liveValues,
setStepperSpeed,
setStepperEnabled,
pulseOutput,
};
}
@@ -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);
}
+73 -73
View File
@@ -1,3 +1,4 @@
import { MachineSelector } from "@/components/MachineConnectionDropdown";
import {
MachineProperties,
MachineIdentification,
@@ -462,17 +463,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 wagoPower1: MachineProperties = {
name: "WAGO Power",
version: "V1",
@@ -520,6 +510,76 @@ export const analogInputTestMachine: MachineProperties = {
],
};
export const bottleSorter: MachineProperties = {
name: "Bottle Sorter",
version: "V1",
slug: "bottlesorter",
icon: "lu:PackageOpen",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0036,
},
device_roles: [
{
role: 0,
role_label: "EK1100 Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "EL7041-0052 Stepper",
allowed_devices: [
{
vendor_id: 2,
product_id: 461451346,
revision: 1048628,
},
],
},
{
role: 2,
role_label: "IP20-EC-DI8-DO8",
allowed_devices: [
{
vendor_id: 0x741,
product_id: 0x117b6722,
revision: 0x1,
},
],
},
],
};
export const vacuum: MachineProperties = {
name: "Vacuum",
version: "V1",
slug: "vacuum",
icon: "lu:ToggleLeft",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0037,
},
device_roles: [
{
role: 0,
role_label: "IP20-EC-DI8-DO8",
allowed_devices: [
{
vendor_id: 0x741,
product_id: 0x117b6722,
revision: 0x1,
},
],
},
],
};
export const ip20TestMachine: MachineProperties = {
name: "IP20 Test",
version: "V1",
@@ -544,65 +604,6 @@ export const ip20TestMachine: MachineProperties = {
],
};
export const TestMotor: MachineProperties = {
name: "TestMotor",
version: "V1",
slug: "testmotor",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0011,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 0x2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "Stepper Motor",
allowed_devices: [
{
vendor_id: 0x2,
product_id: 0x1b773052,
revision: 0x10001e,
},
],
},
],
};
export const wagoAiTestMachine: MachineProperties = {
name: "Wago AI Test",
version: "V1",
slug: "wagoaitestmachine",
icon: "lu:Activity",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0036,
},
device_roles: [
{
role: 0,
role_label: "Wago 750-354 Bus Coupler",
allowed_devices: [
{
vendor_id: 0x21,
product_id: 0x07500354,
revision: 0x2,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder3,
@@ -614,10 +615,9 @@ export const machineProperties: MachineProperties[] = [
aquapath1,
testmachine,
analogInputTestMachine,
wagoAiTestMachine,
digitalInputTestMachine,
ip20TestMachine,
TestMotor,
bottleSorter,
vacuum,
];
export const getMachineProperties = (
@@ -0,0 +1,52 @@
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 { Badge } from "@/components/ui/badge";
import { useVacuum } from "./use";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
export function VacuumConfigPage() {
const { state, liveValues, setIntervalTimeOff, setIntervalTimeOn } = useVacuum();
const safeState = state ?? { mode: "Standby", interval_time_off: 0, interval_time_on: 0 };
const safeLiveValues = liveValues ?? {};
return (
<Page>
<ControlGrid columns={2}>
<ControlCard className="bg-red" title="Interval Settings">
<Label label="Interval (OFF) duration">
<EditValue
value={safeState.interval_time_off}
defaultValue={30}
unit="s"
title="Interval (OFF) duration"
min={0}
max={60}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={setIntervalTimeOff}
/>
</Label>
<Label label="Interval (ON) duration">
<EditValue
value={safeState.interval_time_on}
defaultValue={30}
unit="s"
title="Interval (ON) duration"
min={0}
max={60}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={setIntervalTimeOn}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,75 @@
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 { Badge } from "@/components/ui/badge";
import { useVacuum } from "./use";
import { Spool } from "../winder/Spool";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { roundToDecimals } from "@/lib/decimal";
export function VacuumControlPage() {
const { state, liveValues, remaining_time, spin_shitter, setMode } = useVacuum();
const safeState = state ?? { mode: "Idle", interval_time_off: 0, interval_time_on: 0, running: false };
const safeLiveValues = liveValues ?? { remaining_time: 0 };
return (
<Page>
<ControlGrid columns={2}>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<"Idle" | "On" | "Auto" | "Interval">
value={ safeState.mode }
onChange={setMode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Idle: {
children: "Idle",
icon: "lu:Power",
isActiveClassName: "bg-red-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Auto: {
children: "Auto",
icon: "lu:BrainCog",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Interval: {
children: "Interval",
icon: "lu:Activity",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
/>
</ControlCard>
<ControlCard title="Motor">
<Badge className={safeState.running ? "bg-green-500" : "bg-red-500"}>
{safeState.running ? "Running" : "Idle" }
</Badge>
<Spool rpm={spin_shitter.current?.value} />
<TimeSeriesValueNumeric
label="Remaining Time"
unit="s"
timeseries={remaining_time}
renderValue={(value) => roundToDecimals(value, 0)}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,169 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import { createTimeSeries, TimeSeries, TimeSeriesValue } from "@/lib/timeseries";
import { randomBytes } from "crypto";
// ========== Event Schema ==========
/**
* Machine operation mode enum
*/
export const modeSchema = z.enum(["Idle", "On", "Auto", "Interval"]);
export type Mode = z.infer<typeof modeSchema>;
export const stateEventDataSchema = z.object({
mode: modeSchema,
interval_time_off: z.number(),
interval_time_on: z.number(),
running: z.boolean(),
});
export const liveValuesEventDataSchema = z.object({
remaining_time: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
export type LiveValuesEvent = z.infer<typeof liveValuesEventDataSchema>;
// ========== Store ==========
export type VacuumNamespaceStore = {
state: StateEvent | null;
liveValues: LiveValuesEvent | null;
// Timer series data for live values
remaining_time: TimeSeries,
spin_shitter: TimeSeries,
};
const { initialTimeSeries: remaining_time, insert: addRemainingTime } =
createTimeSeries({ sampleIntervalLong: 1000 });
const { initialTimeSeries: spin_shitter, insert: addSpinShitter } =
createTimeSeries({ sampleIntervalLong: 1000 });
export const createVacuumNamespaceStore =
(): StoreApi<VacuumNamespaceStore> =>
create<VacuumNamespaceStore>(() => ({
state: null,
liveValues: null,
remaining_time,
spin_shitter,
}));
// ========== Message Handler ==========
export function vacuumTestMachineMessageHandler(
store: StoreApi<VacuumNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<VacuumNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: VacuumNamespaceStore,
) => VacuumNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent")
{
console.log("STATE EVENT");
const stateEvent = stateEventSchema.parse(event);
const rpm = Math.floor(Math.random() * (125 - 115 + 1)) + 115;
const value: TimeSeriesValue = {
value: stateEvent.data.running ? rpm : 0,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
state: stateEvent.data,
spin_shitter: addSpinShitter(state.spin_shitter, value)
}));
}
else if (event.name === "LiveValuesEvent")
{
console.log("LIVE VALUES EVENT");
const rpm = Math.floor(Math.random() * (125 - 115 + 1)) + 115;
const parsed = liveValuesEventSchema.parse(event);
const rpm_value: TimeSeriesValue = {
value: rpm,
timestamp: event.ts,
};
const rpm_value_0: TimeSeriesValue = {
value: 0,
timestamp: event.ts,
};
updateStore((liveValuesEvent) => ({
...liveValuesEvent,
liveValues: parsed.data,
spin_shitter: addSpinShitter(liveValuesEvent.spin_shitter, liveValuesEvent.state?.running ? rpm_value : rpm_value_0),
}));
if (parsed.data.remaining_time !== null)
{
const value: TimeSeriesValue = {
value: parsed.data.remaining_time,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
remaining_time: addRemainingTime(state.remaining_time, value),
}));
}
}
else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useVacuumNamespaceImplementation =
createNamespaceHookImplementation<VacuumNamespaceStore>({
createStore: createVacuumNamespaceStore,
createEventHandler: vacuumTestMachineMessageHandler,
});
export function useVacuumNamespace(
machine_identification_unique: MachineIdentificationUnique,
): VacuumNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useVacuumNamespaceImplementation(namespaceId);
}
+27
View File
@@ -0,0 +1,27 @@
import { Topbar } from "@/components/Topbar";
import { vacuumSerialRoute } from "@/routes/routes";
import React from "react";
export function VacuumPage(): React.JSX.Element {
const { serial } = vacuumSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/vacuum/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:ToggleLeft",
},
{
link: "config",
activeLink: "config",
title: "Config",
icon: "lu:Settings2",
},
]}
/>
);
}
+82
View File
@@ -0,0 +1,82 @@
import React, { useEffect } from "react";
type Props = {
rpm?: number;
};
export function Spool({ rpm }: Props) {
const [rotationState, setRotationState] = React.useState({
rotations: 0,
lastRpm: undefined as number | undefined,
lastRpmTime: undefined as number | undefined,
});
useEffect(() => {
if (rpm !== undefined) {
const now = Date.now();
// Calculate the new rotation based on the RPM and time elapsed
const dt = (now - (rotationState.lastRpmTime || now)) / 1000; // Convert ms to seconds
const newRotations = ((rpm / 60) * dt) % 1; // Convert RPM to rotations per second
setRotationState({
rotations: (rotationState.rotations + newRotations) % 1, // Keep it within 0-1
lastRpm: rpm,
lastRpmTime: now,
});
}
}, [rpm]);
const { rotations } = rotationState;
return (
<div className="flex w-full justify-center">
<div
className="aspect-square h-32"
style={{
transform: `rotate(-${rotations * 360}deg)`,
}}
>
<svg
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="221" y="8" width="70" height="182" fill="black" />
<rect
x="490.617"
y="351"
width="70"
height="182"
transform="rotate(120 490.617 351)"
fill="black"
/>
<rect
x="51"
y="403.033"
width="70"
height="182"
transform="rotate(-120 51 403.033)"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M256.498 511.998C398.159 511.998 512.998 397.159 512.998 255.498C512.998 113.837 398.159 -1.00195 256.498 -1.00195C114.837 -1.00195 -0.00195312 113.837 -0.00195312 255.498C-0.00195312 397.159 114.837 511.998 256.498 511.998ZM255.5 447C361.815 447 448 360.815 448 254.5C448 148.185 361.815 62 255.5 62C149.185 62 63 148.185 63 254.5C63 360.815 149.185 447 255.5 447Z"
fill="black"
/>
<path
d="M255.5 381C324.812 381 381 324.812 381 255.5C381 186.188 324.812 130 255.5 130C186.188 130 130 186.188 130 255.5C130 324.812 186.188 381 255.5 381ZM256 319C290.794 319 319 290.794 319 256C319 221.206 290.794 193 256 193C221.206 193 193 221.206 193 256C193 290.794 221.206 319 256 319Z"
fill="black"
/>
<circle
cx="256"
cy="255"
r="50"
fill="black"
className="fill-gray-300"
/>
</svg>
</div>
</div>
);
}
@@ -1,38 +1,48 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMotorSerialRoute } from "@/routes/routes";
import { vacuumSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMotorNamespace, StateEvent } from "./testMotorNamespace";
import {
useVacuumNamespace,
StateEvent,
LiveValuesEvent,
Mode,
} from "./MachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { TestMotor } from "@/machines/properties";
import { vacuum } from "@/machines/properties";
import { z } from "zod";
export function useTestMotor() {
const { serial: serialString } = testMotorSerialRoute.useParams();
export function useVacuum() {
const { serial: serialString } = vacuumSerialRoute.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,
machine_identification: vacuum.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMotorNamespace(machineIdentification);
const { state, liveValues, remaining_time, spin_shitter } = useVacuumNamespace(
machineIdentification,
);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -44,8 +54,8 @@ export function useTestMotor() {
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
type: z.string(),
payload: z.any(),
action: z.string(),
value: z.any(),
}),
);
@@ -59,38 +69,52 @@ export function useTestMotor() {
serverRequest?.();
};
// --- ACTIONS ---
const setMotorOn = (on: boolean) => {
const setMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.motor_enabled = on;
current.mode = mode;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
// Must exactly match the 'Mutation' enum!
data: { type: "SetMotorOn", payload: on },
data: { action: "SetMode", value: mode },
}),
);
};
const setVelocity = (velocity: number) => {
const setIntervalTimeOff = (value: number) => {
updateStateOptimistically(
(current) => {
current.motor_velocity = velocity;
current.interval_time_off = value;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { type: "SetMotorVelocity", payload: velocity },
data: { action: "SetIntervalTimeOff", value: value },
}),
);
};
const setIntervalTimeOn = (value: number) => {
updateStateOptimistically(
(current) => {
current.interval_time_on = value;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetIntervalTimeOn", value: value },
}),
);
};
return {
state: stateOptimistic.value,
setMotorOn,
setVelocity,
liveValues,
remaining_time,
spin_shitter,
setMode,
setIntervalTimeOff,
setIntervalTimeOn
};
}
}
@@ -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,114 +0,0 @@
import { MiniGraph } from "@/components/graph/MiniGraph";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { ControlGrid } from "@/control/ControlGrid";
import React from "react";
import { useWagoAiTestMachine } from "./useWagoAiTestMachine";
export function WagoAiTestMachineControl(): React.JSX.Element {
const { seriesData, state, updateMeasurementRate } = useWagoAiTestMachine();
if (!seriesData) return <div>Initializing Sensor...</div>;
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Measurement Control">
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<div>Measurement Rate (Hz)</div>
<input
type="number"
style={{
border: "2px solid transparent",
borderRadius: "4px",
padding: "3px",
}}
className={"bg-neutral-100 shadow"}
value={state.measurementRateHz}
onChange={(v) => {
const val = Number(v.currentTarget.value);
updateMeasurementRate(val);
}}
min={1}
></input>
</div>
</ControlCard>
<ControlCard title="Analog Input 1 (4-20mA)">
<div className="flex flex-row">
<MiniGraph
newData={seriesData.ai1}
width={400}
renderValue={(v) => v.toFixed(2)}
></MiniGraph>
<div className="ml-4 flex flex-col justify-center">
<div>{seriesData.ai1.current?.value.toFixed(2)} mA</div>
{state.wiringErrors?.[0] && (
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
Wiring Error
</div>
)}
</div>
</div>
</ControlCard>
<ControlCard title="Analog Input 2 (4-20mA)">
<div className="flex flex-row">
<MiniGraph
newData={seriesData.ai2}
width={400}
renderValue={(v) => v.toFixed(2)}
></MiniGraph>
<div className="ml-4 flex flex-col justify-center">
<div>{seriesData.ai2.current?.value.toFixed(2)} mA</div>
{state.wiringErrors?.[1] && (
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
Wiring Error
</div>
)}
</div>
</div>
</ControlCard>
<ControlCard title="Analog Input 3 (4-20mA)">
<div className="flex flex-row">
<MiniGraph
newData={seriesData.ai3}
width={400}
renderValue={(v) => v.toFixed(2)}
></MiniGraph>
<div className="ml-4 flex flex-col justify-center">
<div>{seriesData.ai3.current?.value.toFixed(2)} mA</div>
{state.wiringErrors?.[2] && (
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
Wiring Error
</div>
)}
</div>
</div>
</ControlCard>
<ControlCard title="Analog Input 4 (4-20mA)">
<div className="flex flex-row">
<MiniGraph
newData={seriesData.ai4}
width={400}
renderValue={(v) => v.toFixed(2)}
></MiniGraph>
<div className="ml-4 flex flex-col justify-center">
<div>{seriesData.ai4.current?.value.toFixed(2)} mA</div>
{state.wiringErrors?.[3] && (
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
Wiring Error
</div>
)}
</div>
</div>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,21 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { wagoAiTestMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function WagoAiTestMachine(): React.JSX.Element {
const { serial } = wagoAiTestMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/wagoaitestmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -1,163 +0,0 @@
import { useMachineMutate } from "@/client/useClient";
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import {
createTimeSeries,
TimeSeries,
TimeSeriesValue,
TimeSeriesWithInsert,
} from "@/lib/timeseries";
import { MachineIdentificationUnique } from "@/machines/types";
import { wagoAiTestMachineSerialRoute } from "@/routes/routes";
import { produce } from "immer";
import { useEffect, useMemo, useState } from "react";
import z from "zod";
import { wagoAiTestMachine } from "../properties";
import {
StateEvent,
useWagoAiTestMachineNamespace,
} from "./useWagoAiTestMachineNamespace";
export type AnalogInputTimeSeries = {
ai1: TimeSeries;
ai2: TimeSeries;
ai3: TimeSeries;
ai4: TimeSeries;
};
export function useWagoAiTestMachine() {
const { serial: serialString } = wagoAiTestMachineSerialRoute.useParams();
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: wagoAiTestMachine.machine_identification,
serial,
};
}, [serialString]);
const state = useWagoAiTestMachineNamespace({
type: "machine",
machine_identification_unique: machineIdentification,
});
const mutateMachine = useMachineMutate(
z.object({
measurement_rate_hz: z.number(),
}),
);
const stateOptimistic = useStateOptimistic<StateEvent>();
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const updateMeasurementRate = (val: number) => {
updateStateOptimistically(
(current) => {
current.measurementRateHz = val;
},
() => {
mutateMachine.request({
machine_identification_unique: machineIdentification,
data: { measurement_rate_hz: val },
});
},
);
};
// Create time series for each analog input
const timeSeriesInstances: {
ai1: TimeSeriesWithInsert;
ai2: TimeSeriesWithInsert;
ai3: TimeSeriesWithInsert;
ai4: TimeSeriesWithInsert;
} = {
ai1: createTimeSeries({
sampleIntervalShort: 20,
sampleIntervalLong: 1000,
retentionDurationShort: 5000,
retentionDurationLong: 60 * 60 * 1000,
}),
ai2: createTimeSeries({
sampleIntervalShort: 20,
sampleIntervalLong: 1000,
retentionDurationShort: 5000,
retentionDurationLong: 60 * 60 * 1000,
}),
ai3: createTimeSeries({
sampleIntervalShort: 20,
sampleIntervalLong: 1000,
retentionDurationShort: 5000,
retentionDurationLong: 60 * 60 * 1000,
}),
ai4: createTimeSeries({
sampleIntervalShort: 20,
sampleIntervalLong: 1000,
retentionDurationShort: 5000,
retentionDurationLong: 60 * 60 * 1000,
}),
};
const [seriesData, setSeriesData] = useState<AnalogInputTimeSeries | null>({
ai1: timeSeriesInstances.ai1.initialTimeSeries,
ai2: timeSeriesInstances.ai2.initialTimeSeries,
ai3: timeSeriesInstances.ai3.initialTimeSeries,
ai4: timeSeriesInstances.ai4.initialTimeSeries,
});
useEffect(() => {
const analogInputs = state.analogInputs;
if (analogInputs && seriesData) {
const [ai1, ai2, ai3, ai4, timestampStr] = analogInputs;
const timestamp = Number(timestampStr);
const dataPoint1: TimeSeriesValue = {
value: ai1 * 1000, // Convert to mA
timestamp,
};
const dataPoint2: TimeSeriesValue = {
value: ai2 * 1000,
timestamp,
};
const dataPoint3: TimeSeriesValue = {
value: ai3 * 1000,
timestamp,
};
const dataPoint4: TimeSeriesValue = {
value: ai4 * 1000,
timestamp,
};
setSeriesData({
ai1: timeSeriesInstances.ai1.insert(seriesData.ai1, dataPoint1),
ai2: timeSeriesInstances.ai2.insert(seriesData.ai2, dataPoint2),
ai3: timeSeriesInstances.ai3.insert(seriesData.ai3, dataPoint3),
ai4: timeSeriesInstances.ai4.insert(seriesData.ai4, dataPoint4),
});
}
}, [state]);
return {
seriesData,
state,
updateMeasurementRate,
};
}
@@ -1,86 +0,0 @@
import {
createNamespaceHookImplementation,
EventHandler,
eventSchema,
} from "@/client/socketioStore";
import z from "zod";
import { create, StoreApi } from "zustand";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
measurementRateHz: z.number().optional(),
analogInputs: z
.tuple([z.number(), z.number(), z.number(), z.number(), z.string()])
.optional(),
wiringErrors: z
.tuple([z.boolean(), z.boolean(), z.boolean(), z.boolean()])
.optional(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type WagoAiTestMachineNamespaceStore = z.infer<
typeof stateEventDataSchema
>;
const createMachineStore = (): StoreApi<WagoAiTestMachineNamespaceStore> =>
create<WagoAiTestMachineNamespaceStore>(() => ({
measurementRateHz: 1,
analogInputs: undefined,
wiringErrors: undefined,
}));
// ========== Message Handler ==========
function wagoAiTestMachineMessageHandler(
store: StoreApi<WagoAiTestMachineNamespaceStore>,
): EventHandler {
return (event) => {
const oldState = store.getState();
switch (event.name) {
case "MeasurementRateHz":
{
const newMeasurementRateHz = event.data["MeasurementRateHz"];
if (
newMeasurementRateHz &&
newMeasurementRateHz !== oldState.measurementRateHz
)
store.setState({
...oldState,
measurementRateHz: newMeasurementRateHz,
});
}
break;
case "AnalogInputs":
{
const newAnalogInputs = event.data["AnalogInputs"];
if (newAnalogInputs && newAnalogInputs !== oldState.analogInputs)
store.setState({
...oldState,
analogInputs: newAnalogInputs,
});
}
break;
case "WiringErrors":
{
const newWiringErrors = event.data["WiringErrors"];
if (newWiringErrors && newWiringErrors !== oldState.wiringErrors)
store.setState({
...oldState,
wiringErrors: newWiringErrors,
});
}
break;
}
};
}
// ========== Namespace Hook ==========
export const useWagoAiTestMachineNamespace = createNamespaceHookImplementation({
createStore: () => createMachineStore(),
createEventHandler: wagoAiTestMachineMessageHandler,
});
+40 -54
View File
@@ -52,24 +52,23 @@ import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Settings";
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
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 { BottleSorterPage } from "@/machines/bottlesorter/BottleSorterPage";
import { BottleSorterControlPage } from "@/machines/bottlesorter/BottleSorterControlPage";
// vacuum
import { VacuumPage } from "@/machines/vacuum/Page";
import { VacuumControlPage } from "@/machines/vacuum/ControlPage";
import { VacuumConfigPage } from "@/machines/vacuum/ConfigPage";
// ---
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
@@ -108,30 +107,6 @@ export const analogInputTestMachineControlRoute = createRoute({
component: () => <AnalogInputTestMachineControl />,
});
export const wagoAiTestMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "wagoaitestmachine/$serial",
component: () => <WagoAiTestMachine />,
});
export const wagoAiTestMachineControlRoute = createRoute({
getParentRoute: () => wagoAiTestMachineSerialRoute,
path: "control",
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",
@@ -144,16 +119,36 @@ export const ip20TestMachineControlRoute = createRoute({
component: () => <IP20TestMachineControlPage />,
});
export const testMotorSerialRoute = createRoute({
// vacuum
export const vacuumSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmotor/$serial",
component: () => <TestMotorPage />,
path: "vacuum/$serial",
component: () => <VacuumPage />,
});
export const testMotorControlRoute = createRoute({
getParentRoute: () => testMotorSerialRoute,
export const vacuumControlRoute = createRoute({
getParentRoute: () => vacuumSerialRoute,
path: "control",
component: () => <TestMotorControlPage />,
component: () => <VacuumControlPage />,
});
export const vacuumConfigRoute = createRoute({
getParentRoute: () => vacuumSerialRoute,
path: "config",
component: () => <VacuumConfigPage />,
});
// ---
export const bottleSorterSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "bottlesorter/$serial",
component: () => <BottleSorterPage />,
});
export const bottleSorterControlRoute = createRoute({
getParentRoute: () => bottleSorterSerialRoute,
path: "control",
component: () => <BottleSorterControlPage />,
});
export const sidebarRoute = createRoute({
@@ -359,12 +354,6 @@ export const aquapath1ControlRoute = createRoute({
component: () => <Aquapath1ControlPage />,
});
export const aquapath1SettingsRoute = createRoute({
getParentRoute: () => aquapath1SerialRoute,
path: "settings",
component: () => <Aquapath1SettingsPage />,
});
export const buffer1SettingsRoute = createRoute({
getParentRoute: () => buffer1SerialRoute,
path: "settings",
@@ -484,27 +473,19 @@ export const rootTree = RootRoute.addChildren([
laser1GraphsRoute,
laser1PresetsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
analogInputTestMachineSerialRoute.addChildren([
analogInputTestMachineControlRoute,
]),
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
digitalInputTestMachineSerialRoute.addChildren([
digitalInputTestMachineControlRoute,
]),
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
testMotorSerialRoute.addChildren([testMotorControlRoute]),
bottleSorterSerialRoute.addChildren([bottleSorterControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
aquapath1GraphRoute,
aquapath1SettingsRoute,
]),
winder2SerialRoute.addChildren([
@@ -515,6 +496,11 @@ export const rootTree = RootRoute.addChildren([
winder2PresetsRoute,
]),
vacuumSerialRoute.addChildren([
vacuumControlRoute,
vacuumConfigRoute,
]),
extruder2Route.addChildren([
extruder2ControlRoute,
extruder2SettingsRoute,
+7 -25
View File
@@ -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 = () => {
+1 -1
View File
@@ -8,7 +8,7 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
units = { path = "../units" }
bitvec = { version = "1.0.1", features = ["alloc"] }
+5
View File
@@ -112,6 +112,11 @@ impl EL4002 {
.write_channel_config(subdevice, 0x8010)
.await?;
self.configuration
.pdo_assignment
.txpdo_assignment()
.write_config(subdevice)
.await?;
self.configuration
.pdo_assignment
.rxpdo_assignment()
+1 -1
View File
@@ -180,6 +180,7 @@ impl Configuration for EL5152Configuration {
) -> Result<(), anyhow::Error> {
// Configure channel 1
self.channel1.write_channel_config(device, 0x8000).await?;
// Configure channel 2
self.channel2.write_channel_config(device, 0x8010).await?;
// Write PDO assignments
@@ -303,7 +304,6 @@ impl EL5152ChannelConfiguration {
device
.sdo_write(base_index, 0x11, self.frequency_window)
.await?;
device
.sdo_write(base_index, 0x13, self.frequency_scaling)
.await?;
@@ -351,7 +351,6 @@ pub enum EL7031_0030AnalogInputPort {
pub const EL7031_0030_VENDOR_ID: u32 = 0x2;
pub const EL7031_0030_PRODUCT_ID: u32 = 0x1b773052;
pub const EL7031_0030_REVISION_A: u32 = 0x10001E;
pub const EL7031_0030_IDENTITY_A: SubDeviceIdentityTuple = (
EL7031_0030_VENDOR_ID,
EL7031_0030_PRODUCT_ID,
+1 -2
View File
@@ -56,9 +56,8 @@ use std::{any::Any, fmt::Debug, sync::Arc};
use wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
use wago_modules::ip20_ec_di8_do8::{IP20_EC_DI8_DO8_IDENTITY, IP20EcDi8Do8};
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Module {
pub name: String,
pub slot: u16,
pub belongs_to_addr: u16,
pub has_tx: bool,
+52 -133
View File
@@ -2,16 +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},
wago_750_652::{WAGO_750_652_MODULE_IDENT, WAGO_750_652_PRODUCT_ID},
@@ -23,25 +18,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 +103,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 +132,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 +145,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 +220,6 @@ impl Wago750_354 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -271,37 +227,18 @@ 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 +256,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
@@ -362,9 +274,6 @@ impl Wago750_354 {
m.vendor_id,
m.product_id,
) {
WAGO_750_455_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_455::Wago750_455::new()))
}
WAGO_750_501_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_501::Wago750_501::new()))
}
@@ -377,13 +286,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 +296,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,8 +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;
pub mod wago_750_652;
@@ -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);
@@ -1,198 +0,0 @@
use bitvec::field::BitField;
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
};
use crate::io::analog_input::physical::AnalogInputRange;
use crate::io::analog_input::{AnalogInputDevice, AnalogInputInput};
use units::electric_current::milliampere;
use units::f64::ElectricCurrent;
#[derive(Clone, Debug)]
pub enum Wago750_455Port {
AI1,
AI2,
AI3,
AI4,
}
impl From<Wago750_455Port> for usize {
fn from(value: Wago750_455Port) -> Self {
match value {
Wago750_455Port::AI1 => 0,
Wago750_455Port::AI2 => 16,
Wago750_455Port::AI3 => 32,
Wago750_455Port::AI4 => 48,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_455TxPdo {
pub ai1: u16,
pub ai2: u16,
pub ai3: u16,
pub ai4: u16,
}
#[derive(Clone)]
pub struct Wago750_455 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
module: Option<Module>,
tx_pdo: Wago750_455TxPdo,
}
impl AnalogInputDevice<Wago750_455Port> for Wago750_455 {
fn get_input(&self, port: Wago750_455Port) -> AnalogInputInput {
let raw = match port {
Wago750_455Port::AI1 => self.tx_pdo.ai1,
Wago750_455Port::AI2 => self.tx_pdo.ai2,
Wago750_455Port::AI3 => self.tx_pdo.ai3,
Wago750_455Port::AI4 => self.tx_pdo.ai4,
};
let wiring_error = (raw & 0x0003) == 0x0003;
let raw_value = (raw & 0x7FF0) as i16;
let normalized = self.analog_input_range().raw_to_normalized(raw_value) as f32;
AnalogInputInput {
normalized,
wiring_error,
}
}
fn analog_input_range(&self) -> AnalogInputRange {
AnalogInputRange::Current {
min: ElectricCurrent::new::<milliampere>(4.0),
max: ElectricCurrent::new::<milliampere>(20.0),
min_raw: 0,
max_raw: 0x7FF0,
}
}
}
impl EthercatDeviceUsed for Wago750_455 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_455 {}
impl EthercatDynamicPDO for Wago750_455 {
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_455 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.tx_bit_offset;
let ai1 = input[base..(base + 16)].load_le::<u16>();
let ai2 = input[(base + 16)..(base + 32)].load_le::<u16>();
let ai3 = input[(base + 32)..(base + 48)].load_le::<u16>();
let ai4 = input[(base + 48)..(base + 64)].load_le::<u16>();
self.tx_pdo.ai1 = ai1;
self.tx_pdo.ai2 = ai2;
self.tx_pdo.ai3 = ai3;
self.tx_pdo.ai4 = ai4;
Ok(())
}
fn input_len(&self) -> usize {
64
}
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> {
self.input(input)
}
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.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl EthercatDeviceProcessing for Wago750_455 {}
impl NewEthercatDevice for Wago750_455 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
tx_pdo: Wago750_455TxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_455 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_455")
}
}
pub const WAGO_750_455_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_455_PRODUCT_ID: u32 = 0x045541b3;
pub const WAGO_750_455_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_455_VENDOR_ID, WAGO_750_455_PRODUCT_ID);
@@ -148,7 +148,8 @@ impl EthercatDevice for Wago750_652 {
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.rx_bit_offset;
let base = 32;
let mut ol0_i = 5; // Ol0 starts at bit 5 and goes to bit 7 (inclusive)
let mut ol1_i = 8;
@@ -173,10 +174,12 @@ impl EthercatDevice for Wago750_652 {
);
ol1_i += 1;
}
println!("{}", self.rx_pdo.control.output_length);
let data_start = base + 16;
for (i, &byte) in self.rx_pdo.out_buffer.iter().take(22).enumerate() {
output[(data_start + i * 8)..(data_start + (i + 1) * 8)].store_le(byte);
}
//println!("{:?}",self.rx_pdo.out_buffer);
Ok(())
}
+13
View File
@@ -0,0 +1,13 @@
# Git Hooks
This directory contains Git hooks for the project.
## Installation
To install the hooks, run the following command from the repository root:
```bash
# Copy the pre-commit hook to your local .git/hooks directory
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
#
# Pre-commit hook to generate Rust documentation
#
# This hook runs the rustdoc.sh script to update documentation
# before each commit.
echo "Running rustdoc generation..."
# Get the repository root
REPO_ROOT="$(git rev-parse --show-toplevel)"
# iteralte all directories thst contain a Cargo.toml file
for dir in $(find "$REPO_ROOT" -name "Cargo.toml" -exec dirname {} \;); do
echo "Generating documentation in $dir"
# cargotoml conatains workspace members, skip this directory
# HAS_MEMBERS=$(grep -c '\[workspace\]' "$dir/Cargo.toml")
# if [ "$HAS_MEMBERS" -gt 0 ]; then
# echo "Skipping workspace directory $dir"
# continue
# fi
# get package name from Cargo.toml using cargo metadata
PACKAGE_NAME=$(cargo metadata --format-version 1 --no-deps --manifest-path "$dir/Cargo.toml" | jq -r '.packages[0].name')
if [ -z "$PACKAGE_NAME" ]; then
echo "Could not determine package name for $dir"
continue
fi
# go into the directory
cd "$dir" || continue
# generate the documentation using rustdoc-md
cargo install rustdoc-md --git https://github.com/qitechgmbh/rustdoc-md --rev 6dc34f9c9d878799d9c1b4f2d2bcbb1f2757ede7
RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS="-Z unstable-options --output-format json" cargo doc --no-deps
rustdoc-md --path "${REPO_ROOT}/target/doc/${PACKAGE_NAME}.json" --output ./rustdoc.md
# stage the generated documentation file
git add "rustdoc.md"
# go back to the repository root
cd "$REPO_ROOT" || exit
done
git rm --cached rustdoc.md 2>/dev/null
rm rustdoc.md
exit 0
+3 -3
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
ethercrab = { git = "https://github.com/v-morlock/ethercrab", branch = "main" }
ethercrab = "0.6"
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
control_core_derive = { path = "../control-core-derive" }
@@ -15,13 +15,13 @@ anyhow = "1.0.100"
bitvec = "1.0.1"
lazy_static = "1.5.0"
euclid = "0.22.13"
euclid = "0.22.11"
# concurrency
smol = "2.0.2"
# web
serde_json = "1.0.149"
serde_json = "1.0.143"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
# serial
-56
View File
@@ -20,12 +20,6 @@ pub struct LiveValuesEvent {
pub back_temperature: f64,
pub front_temp_reservoir: f64,
pub back_temp_reservoir: f64,
pub front_revolutions: f64,
pub back_revolutions: f64,
pub front_power: f64,
pub back_power: f64,
pub front_total_energy: f64,
pub back_total_energy: f64,
}
impl LiveValuesEvent {
@@ -41,8 +35,6 @@ pub struct StateEvent {
pub mode_state: ModeState,
pub flow_states: FlowStates,
pub temperature_states: TempStates,
pub fan_states: FanStates,
pub tolerance_states: ToleranceStates,
}
impl StateEvent {
@@ -78,28 +70,6 @@ pub struct FlowState {
pub should_flow: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct FanState {
pub revolutions: f64,
pub max_revolutions: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct FanStates {
pub front: FanState,
pub back: FanState,
}
#[derive(Serialize, Debug, Clone)]
pub struct ToleranceState {
pub heating: f64,
pub cooling: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct ToleranceStates {
pub front: ToleranceState,
pub back: ToleranceState,
}
pub enum AquaPathV1Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
@@ -115,14 +85,6 @@ enum Mutation {
SetFrontFlow(bool),
SetBackFlow(bool),
SetFrontRevolutions(f64),
SetBackRevolutions(f64),
SetFrontHeatingTolerance(f64),
SetBackHeatingTolerance(f64),
SetFrontCoolingTolerance(f64),
SetBackCoolingTolerance(f64),
}
#[derive(Debug, Clone)]
@@ -183,24 +145,6 @@ impl MachineApi for AquaPathV1 {
Mutation::SetFrontFlow(should_pump) => {
self.set_should_pump(should_pump, super::AquaPathSideType::Front)
}
Mutation::SetBackRevolutions(revolution) => {
self.set_max_revolutions(revolution, super::AquaPathSideType::Back)
}
Mutation::SetFrontRevolutions(revolutions) => {
self.set_max_revolutions(revolutions, super::AquaPathSideType::Front)
}
Mutation::SetBackHeatingTolerance(tolerance) => {
self.set_heating_tolerance(tolerance, super::AquaPathSideType::Back)
}
Mutation::SetFrontHeatingTolerance(tolerance) => {
self.set_heating_tolerance(tolerance, super::AquaPathSideType::Front)
}
Mutation::SetBackCoolingTolerance(tolerance) => {
self.set_cooling_tolerance(tolerance, super::AquaPathSideType::Back);
}
Mutation::SetFrontCoolingTolerance(tolerance) => {
self.set_cooling_tolerance(tolerance, super::AquaPathSideType::Front);
}
}
Ok(())
}
+27 -82
View File
@@ -5,17 +5,17 @@ use ethercat_hal::io::encoder_input::EncoderInput;
use ethercat_hal::io::{
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
};
use std::time::Instant;
use units::AngularVelocity;
use units::angular_velocity::revolution_per_minute;
use std::time::{Duration, Instant};
use units::f64::ThermodynamicTemperature;
use units::thermodynamic_temperature::{degree_celsius, kelvin};
use units::thermodynamic_temperature::degree_celsius;
use units::volume_rate::liter_per_minute;
#[derive(Debug)]
pub struct Controller {
pub pid: PidController,
temperature_pid_output: f64,
window_start: Instant,
pwm_period: Duration,
pub temperature: Temperature,
pub target_temperature: ThermodynamicTemperature,
@@ -27,19 +27,10 @@ pub struct Controller {
pub cooling_controller: AnalogOutput,
pub cooling_relais: DigitalOutput,
pub cooling_tolerance: ThermodynamicTemperature,
pub heating_tolerance: ThermodynamicTemperature,
pub current_revolutions: AngularVelocity,
pub max_revolutions: AngularVelocity,
pub heating_relais_1: DigitalOutput,
pub temperature_sensor_in: TemperatureInput,
pub temperature_sensor_out: TemperatureInput,
pub power: f64,
pub total_energy: f64,
pub cooling_allowed: bool,
pub heating_allowed: bool,
@@ -57,6 +48,7 @@ impl Controller {
kp: f64,
ki: f64,
kd: f64,
pwm_duration: Duration,
temp: Temperature,
target_tempetature: ThermodynamicTemperature,
cooling_controller: AnalogOutput,
@@ -64,7 +56,6 @@ impl Controller {
heating_relais_1: DigitalOutput,
temp_sensor_in: TemperatureInput,
temp_sensor_out: TemperatureInput,
max_revolutions: AngularVelocity,
flow: Flow,
pump_relais: DigitalOutput,
@@ -72,7 +63,9 @@ impl Controller {
) -> Self {
Self {
pid: PidController::new(kp, ki, kd),
temperature_pid_output: 0.0,
window_start: Instant::now(),
pwm_period: pwm_duration,
target_temperature: target_tempetature,
current_temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
temp_reservoir: ThermodynamicTemperature::new::<degree_celsius>(25.0),
@@ -81,24 +74,13 @@ impl Controller {
temperature: temp,
cooling_controller: cooling_controller,
cooling_tolerance: ThermodynamicTemperature::new::<degree_celsius>(2.0),
heating_tolerance: ThermodynamicTemperature::new::<degree_celsius>(2.0),
current_revolutions: AngularVelocity::new::<revolution_per_minute>(0.0),
max_revolutions: max_revolutions,
cooling_relais: cooling_relais,
heating_relais_1: heating_relais_1,
cooling_allowed: false,
heating_allowed: false,
temperature_sensor_in: temp_sensor_in,
temperature_sensor_out: temp_sensor_out,
power: 700.0,
total_energy: 0.0,
flow: flow,
pump_relais: pump_relais,
flow_sensor: flow_sensor,
@@ -192,12 +174,12 @@ impl Controller {
pub fn turn_cooling_on(&mut self) {
self.cooling_relais.set(true);
self.cooling_controller.set(10.0);
self.temperature.cooling = true;
}
pub fn turn_cooling_off(&mut self) {
self.cooling_relais.set(false);
self.current_revolutions = AngularVelocity::new::<revolution_per_minute>(0.0);
self.temperature.cooling = false;
}
@@ -209,7 +191,6 @@ impl Controller {
pub fn turn_heating_off(&mut self) {
self.heating_relais_1.set(false);
self.temperature.heating = false;
self.power = 0.0;
}
pub fn set_should_pump(&mut self, should_pump: bool) {
@@ -243,35 +224,6 @@ impl Controller {
}
}
pub fn get_current_revolutions(&self) -> AngularVelocity {
self.current_revolutions
}
pub fn get_max_revolutions(&self) -> AngularVelocity {
self.max_revolutions
}
pub fn set_max_revolutions(&mut self, revolutions: AngularVelocity) {
self.max_revolutions = revolutions;
}
pub fn set_cooling_tolerance(&mut self, tolerance: ThermodynamicTemperature) {
self.cooling_tolerance = tolerance;
}
pub fn set_heating_tolerance(&mut self, tolerance: ThermodynamicTemperature) {
self.heating_tolerance = tolerance;
}
// no power/energy unit implemented
pub fn get_current_power(&self) -> f64 {
self.power
}
pub fn get_total_energy(&self) -> f64 {
self.total_energy
}
pub fn update(&mut self, now: Instant) -> () {
let current_flow = self.get_flow();
self.current_flow = current_flow;
@@ -298,50 +250,43 @@ impl Controller {
// Calculate PID error once
let error = self.target_temperature.get::<degree_celsius>()
- self.current_temperature.get::<degree_celsius>();
let control = self.pid.update(error, now);
self.temperature_pid_output = control;
let elapsed = now - self.window_start;
self.window_start = now;
let elapsed = now.duration_since(self.window_start);
if elapsed >= self.pwm_period {
self.window_start = now;
}
// Decide whether to heat or cool based on error
if error > self.heating_tolerance.get::<degree_celsius>() {
if error > 0.0 {
// Need heating (current < target)
if self.temperature.cooling {
self.turn_cooling_off();
}
if self.heating_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
self.turn_heating_on();
self.total_energy += self.get_current_power() * elapsed.as_secs_f64() / 3600.0;
// Only start heating if pump is on
let duty = control.clamp(0.0, 1.0);
let on_time = self.pwm_period.mul_f64(duty);
let on = elapsed < on_time;
if on && !self.temperature.heating {
self.turn_heating_on();
} else if !on && self.temperature.heating {
self.turn_heating_off();
}
} else {
// Pump is off or heating not allowed - don't heat
if self.temperature.heating {
self.turn_heating_off();
}
}
} else if error < -self.cooling_tolerance.get::<degree_celsius>() {
} else {
// Need cooling (current > target)
if self.temperature.heating {
self.turn_heating_off();
}
if self.cooling_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
if !self.temperature.cooling {
self.turn_cooling_on();
}
let max_revolutions = self.get_max_revolutions();
let temp_offset = self.current_temperature - self.target_temperature;
let target_revolutions = (temp_offset.get::<kelvin>() * 10.0)
.clamp(0.0, max_revolutions.get::<revolution_per_minute>());
self.cooling_controller
.set(target_revolutions as f32 / 10.0);
self.current_revolutions =
AngularVelocity::new::<revolution_per_minute>(target_revolutions);
} else {
if self.temperature.cooling {
self.turn_cooling_off();
}
if self.cooling_allowed && !self.temperature.cooling {
self.turn_cooling_on();
}
}
}
+2 -102
View File
@@ -1,8 +1,6 @@
use api::{ToleranceState, ToleranceStates};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use units::angular_velocity::revolution_per_minute;
use units::f64::*;
use units::{thermodynamic_temperature::degree_celsius, volume_rate::liter_per_minute};
@@ -11,8 +9,8 @@ use crate::{
MACHINE_AQUAPATH_V1, VENDOR_QITECH,
aquapath1::{
api::{
AquaPathV1Events, AquaPathV1Namespace, FanState, FanStates, FlowState, FlowStates,
LiveValuesEvent, ModeState, StateEvent, TempState, TempStates,
AquaPathV1Events, AquaPathV1Namespace, FlowState, FlowStates, LiveValuesEvent,
ModeState, StateEvent, TempState, TempStates,
},
controller::Controller,
},
@@ -125,18 +123,6 @@ impl AquaPathV1 {
back_flow: self.back_controller.current_flow.get::<liter_per_minute>(),
front_temp_reservoir: self.front_controller.temp_reservoir.get::<degree_celsius>(),
back_temp_reservoir: self.back_controller.temp_reservoir.get::<degree_celsius>(),
front_revolutions: self
.front_controller
.current_revolutions
.get::<revolution_per_minute>(),
back_revolutions: self
.back_controller
.current_revolutions
.get::<revolution_per_minute>(),
front_power: self.front_controller.get_current_power(),
back_power: self.back_controller.get_current_power(),
front_total_energy: self.front_controller.get_total_energy(),
back_total_energy: self.back_controller.get_total_energy(),
}
}
@@ -184,50 +170,6 @@ impl AquaPathV1 {
should_flow: self.back_controller.should_pump,
},
},
fan_states: FanStates {
front: FanState {
revolutions: self
.front_controller
.max_revolutions
.get::<revolution_per_minute>(),
max_revolutions: self
.front_controller
.max_revolutions
.get::<revolution_per_minute>(),
},
back: FanState {
revolutions: self
.back_controller
.max_revolutions
.get::<revolution_per_minute>(),
max_revolutions: self
.back_controller
.max_revolutions
.get::<revolution_per_minute>(),
},
},
tolerance_states: ToleranceStates {
front: ToleranceState {
heating: self
.front_controller
.heating_tolerance
.get::<degree_celsius>(),
cooling: self
.front_controller
.cooling_tolerance
.get::<degree_celsius>(),
},
back: ToleranceState {
heating: self
.back_controller
.heating_tolerance
.get::<degree_celsius>(),
cooling: self
.back_controller
.cooling_tolerance
.get::<degree_celsius>(),
},
},
}
}
@@ -332,45 +274,3 @@ impl AquaPathV1 {
self.emit_state();
}
}
impl AquaPathV1 {
fn set_max_revolutions(&mut self, revolutions: f64, fan_type: AquaPathSideType) {
match fan_type {
AquaPathSideType::Back => self
.back_controller
.set_max_revolutions(AngularVelocity::new::<revolution_per_minute>(revolutions)),
AquaPathSideType::Front => self
.front_controller
.set_max_revolutions(AngularVelocity::new::<revolution_per_minute>(revolutions)),
}
self.emit_state();
}
}
impl AquaPathV1 {
fn set_heating_tolerance(&mut self, tolerance: f64, tolerance_type: AquaPathSideType) {
match tolerance_type {
AquaPathSideType::Back => self
.back_controller
.set_heating_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
AquaPathSideType::Front => self
.front_controller
.set_heating_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
}
self.emit_state();
}
fn set_cooling_tolerance(&mut self, tolerance: f64, tolerance_type: AquaPathSideType) {
match tolerance_type {
AquaPathSideType::Back => self
.back_controller
.set_cooling_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
AquaPathSideType::Front => self
.front_controller
.set_cooling_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
}
self.emit_state();
}
}
+7 -15
View File
@@ -1,6 +1,7 @@
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use super::{
@@ -24,12 +25,8 @@ use ethercat_hal::{
temperature_input::TemperatureInput,
},
};
use std::time::Instant;
use units::{
AngularVelocity,
angular_velocity::revolution_per_minute,
thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius},
};
use std::time::{Duration, Instant};
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
impl MachineNewTrait for AquaPathV1 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
@@ -91,12 +88,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
@@ -139,6 +131,7 @@ impl MachineNewTrait for AquaPathV1 {
0.10,
0.0,
0.015,
Duration::from_millis(500),
Temperature::default(),
ThermodynamicTemperature::new::<degree_celsius>(25.0),
ao1,
@@ -146,7 +139,6 @@ impl MachineNewTrait for AquaPathV1 {
do2,
t1,
t2,
AngularVelocity::new::<revolution_per_minute>(100.0),
Flow::default(),
do1,
enc1,
@@ -156,6 +148,7 @@ impl MachineNewTrait for AquaPathV1 {
0.10,
0.0,
0.015,
Duration::from_millis(500),
Temperature::default(),
ThermodynamicTemperature::new::<degree_celsius>(25.0),
ao2,
@@ -163,7 +156,6 @@ impl MachineNewTrait for AquaPathV1 {
do6,
t3,
t4,
AngularVelocity::new::<revolution_per_minute>(100.0),
Flow::default(),
do5,
enc2,
@@ -1,17 +1,27 @@
use super::DigitalInputTestMachine;
use super::BottleSorter;
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for DigitalInputTestMachine {
impl MachineAct for BottleSorter {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
// Update pulse outputs
self.update_pulse_outputs();
// Emit state at 30 Hz
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_emit = now;
}
// Emit live values at 10 Hz
if now.duration_since(self.last_live_values_emit) > Duration::from_secs_f64(1.0 / 10.0) {
self.emit_live_values();
self.last_live_values_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
@@ -19,6 +29,7 @@ impl MachineAct for DigitalInputTestMachine {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
self.emit_live_values();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
@@ -36,10 +47,10 @@ impl MachineAct for DigitalInputTestMachine {
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::Value::Null,
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live_values"),
})
.expect("Failed to send values");
sender.close();
.unwrap();
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use super::BottleSorter;
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 std::sync::Arc;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub outputs: [bool; 8],
pub stepper_speed_mm_s: f64,
pub stepper_enabled: bool,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone)]
pub struct LiveValuesEvent {
pub stepper_position: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
pub enum BottleSorterEvents {
State(Event<StateEvent>),
LiveValues(Event<LiveValuesEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetStepperSpeed { speed_mm_s: f64 },
SetStepperEnabled { enabled: bool },
PulseOutput { index: usize },
}
#[derive(Debug, Clone)]
pub struct BottleSorterNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<BottleSorterEvents> for BottleSorterNamespace {
fn emit(&mut self, events: BottleSorterEvents) {
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<BottleSorterEvents> for BottleSorterEvents {
fn event_value(&self) -> GenericEvent {
match self {
BottleSorterEvents::State(event) => event.clone().into(),
BottleSorterEvents::LiveValues(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for BottleSorter {
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetStepperSpeed { speed_mm_s } => self.set_stepper_speed(speed_mm_s),
Mutation::SetStepperEnabled { enabled } => self.set_stepper_enabled(enabled),
Mutation::PulseOutput { index } => self.pulse_output(index),
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+125
View File
@@ -0,0 +1,125 @@
use crate::bottle_sorter::api::{BottleSorterEvents, LiveValuesEvent, StateEvent};
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_output::DigitalOutput;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use smol::channel::{Receiver, Sender};
use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::bottle_sorter::api::BottleSorterNamespace;
use crate::{BOTTLE_SORTER, VENDOR_QITECH};
#[derive(Debug)]
pub struct BottleSorter {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub namespace: BottleSorterNamespace,
pub last_state_emit: Instant,
pub last_live_values_emit: Instant,
// State
pub outputs: [bool; 8],
pub stepper_speed_mm_s: f64,
pub stepper_enabled: bool,
// Hardware
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub douts: [DigitalOutput; 8],
pub stepper: StepperVelocityEL70x1,
// Config
pub steps_per_mm: f64,
// Pulse control
pub pulse_outputs: [Option<Instant>; 8],
pub pulse_duration_ms: u64,
}
impl Machine for BottleSorter {
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 BottleSorter {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: BOTTLE_SORTER,
};
}
impl BottleSorter {
pub fn get_state(&self) -> StateEvent {
StateEvent {
outputs: self.outputs,
stepper_speed_mm_s: self.stepper_speed_mm_s,
stepper_enabled: self.stepper_enabled,
}
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(BottleSorterEvents::State(event));
}
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
stepper_position: self.stepper.get_position() as f64 / self.steps_per_mm,
}
}
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace
.emit(BottleSorterEvents::LiveValues(event));
}
/// Set the stepper speed in mm/s
pub fn set_stepper_speed(&mut self, speed_mm_s: f64) {
self.stepper_speed_mm_s = speed_mm_s;
let steps_per_s = speed_mm_s * self.steps_per_mm;
let _ = self.stepper.set_speed(steps_per_s);
self.emit_state();
}
/// Set the stepper enabled state
pub fn set_stepper_enabled(&mut self, enabled: bool) {
self.stepper_enabled = enabled;
self.stepper.set_enabled(enabled);
self.emit_state();
}
/// Pulse a specific output (turn on for a few milliseconds)
pub fn pulse_output(&mut self, index: usize) {
if index < self.outputs.len() {
self.outputs[index] = true;
self.douts[index].set(true);
self.pulse_outputs[index] = Some(Instant::now());
self.emit_state();
}
}
/// Check and turn off pulsed outputs
pub fn update_pulse_outputs(&mut self) {
let now = Instant::now();
for i in 0..8 {
if let Some(start_time) = self.pulse_outputs[i] {
if now.duration_since(start_time).as_millis() >= self.pulse_duration_ms as u128 {
self.outputs[i] = false;
self.douts[i].set(false);
self.pulse_outputs[i] = None;
}
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
use crate::bottle_sorter::api::BottleSorterNamespace;
use crate::bottle_sorter::BottleSorter;
use smol::block_on;
use std::time::Instant;
use crate::{
get_ethercat_device, validate_no_role_dublicates,
validate_same_machine_identification_unique, MachineNewHardware, MachineNewParams,
MachineNewTrait,
};
use anyhow::Error;
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::ek1100::{EK1100, EK1100_IDENTITY_A};
use ethercat_hal::devices::el7041_0052::{
coe::EL7041_0052Configuration, EL7041_0052, EL7041_0052Port, EL7041_0052_IDENTITY_A,
};
use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::{
IP20EcDi8Do8, IP20EcDi8Do8OutputPort, IP20_EC_DI8_DO8_IDENTITY,
};
use ethercat_hal::io::digital_output::DigitalOutput;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
impl MachineNewTrait for BottleSorter {
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 &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::EtherCATMachine/BottleSorter::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
// Get EK1100 bus coupler (role 0)
let _ek1100_device = get_ethercat_device::<EK1100>(
hardware,
params,
0,
vec![EK1100_IDENTITY_A],
)
.await?
.0;
// Get EL7041_0052 stepper terminal (role 1)
let el7041_device = get_ethercat_device::<EL7041_0052>(
hardware,
params,
1,
vec![EL7041_0052_IDENTITY_A],
)
.await?;
// Configure the stepper motor for velocity mode
let el7041_config = EL7041_0052Configuration {
stm_features: ethercat_hal::shared_config::el70x1::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 2800,
..Default::default()
},
..Default::default()
};
el7041_device
.0
.write()
.await
.write_config(&el7041_device.1, &el7041_config)
.await?;
let el7041_device = el7041_device.0;
// Get IP20 DI8/DO8 module (role 2)
let ip20_device = get_ethercat_device::<IP20EcDi8Do8>(
hardware,
params,
2,
vec![IP20_EC_DI8_DO8_IDENTITY],
)
.await?
.0;
// Create stepper motor controller
let stepper = StepperVelocityEL70x1::new(el7041_device.clone(), EL7041_0052Port::STM1);
// Create digital outputs for IP20 module
let do1 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO1);
let do2 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO2);
let do3 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO3);
let do4 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO4);
let do5 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO5);
let do6 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO6);
let do7 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO7);
let do8 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO8);
let (sender, receiver) = smol::channel::unbounded();
let mut machine = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: BottleSorterNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
last_live_values_emit: Instant::now(),
outputs: [false; 8],
stepper_speed_mm_s: 0.0,
stepper_enabled: false,
main_sender: params.main_thread_channel.clone(),
douts: [do1, do2, do3, do4, do5, do6, do7, do8],
stepper,
steps_per_mm: 200.0, // Default: 200 steps per mm (configurable)
pulse_outputs: [None; 8],
pulse_duration_ms: 100, // 100ms pulse duration
};
machine.emit_state();
machine.emit_live_values();
Ok(machine)
})
}
}
@@ -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 &params.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)
})
}
}
+4 -4
View File
@@ -159,7 +159,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_1,
Heating::default(),
Duration::from_millis(500),
900.0,
700.0,
1.0,
);
@@ -174,7 +174,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_2,
Heating::default(),
Duration::from_millis(500),
900.0,
700.0,
1.0,
);
@@ -189,7 +189,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_3,
Heating::default(),
Duration::from_millis(500),
900.0,
700.0,
1.0,
);
@@ -204,7 +204,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_4,
Heating::default(),
Duration::from_millis(500),
150.0,
200.0,
0.95,
);
+4 -7
View File
@@ -19,22 +19,21 @@ use std::sync::Arc;
use std::time::Instant;
pub mod analog_input_test_machine;
pub mod aquapath1;
pub mod bottle_sorter;
#[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_ai_test_machine;
pub mod wago_power;
pub mod winder2;
pub mod vacuum;
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
@@ -48,10 +47,8 @@ pub const MACHINE_EXTRUDER_V2: u16 = 0x0016;
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 MOTOR_TEST_MACHINE: u16 = 0x0011;
pub const BOTTLE_SORTER: u16 = 0x0036;
pub const MACHINE_VACUUM: u16 = 0x0037;
use serde_json::Value;
use smol::lock::RwLock;
+8 -4
View File
@@ -49,8 +49,10 @@ impl MachineIdentification {
}
}
pub fn slug(&self) -> String {
match self.machine {
pub fn slug(&self) -> String
{
match self.machine
{
x if x == MACHINE_WINDER_V1 => "winder_v1".to_string(),
x if x == MACHINE_EXTRUDER_V1 => "extruder_v1".to_string(),
x if x == MACHINE_LASER_V1 => "laser_v1".to_string(),
@@ -62,7 +64,8 @@ impl MachineIdentification {
x if x == TEST_MACHINE => "test_machine".to_string(),
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 == BOTTLE_SORTER => "bottle_sorter".to_string(),
x if x == MACHINE_VACUUM => "vacuum".to_string(),
_ => unreachable!("Unknown machine id"),
}
}
@@ -175,6 +178,7 @@ use ethercrab::MainDevice;
use ethercrab::SubDeviceIdentity;
use crate::ANALOG_INPUT_TEST_MACHINE;
use crate::BOTTLE_SORTER;
use crate::IP20_TEST_MACHINE;
use crate::MACHINE_AQUAPATH_V1;
use crate::MACHINE_BUFFER_V1;
@@ -182,11 +186,11 @@ use crate::MACHINE_EXTRUDER_V1;
use crate::MACHINE_EXTRUDER_V2;
use crate::MACHINE_LASER_V1;
use crate::MACHINE_MOCK;
use crate::MACHINE_VACUUM;
use crate::MACHINE_WAGO_POWER_V1;
use crate::MACHINE_WINDER_V1;
use crate::TEST_MACHINE;
use crate::VENDOR_QITECH;
use crate::WAGO_AI_TEST_MACHINE;
#[derive(Debug)]
pub struct MachineIdentificationAddresses {
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more