Compare commits

..
Author SHA1 Message Date
kraemr 22be4b79f0 fixed linter errors and added logic for 750_430 2026-01-30 16:33:50 +01:00
kraemr be84edf0fd removed unused parameter from ModulePDOMapping 2026-01-30 16:23:22 +01:00
kraemr ff3e787a03 fixed Wago Initialization Logic
fixed formatting
2026-01-30 16:16:10 +01:00
Christian 093663c4e6 Merge pull request #1086 from qitechgmbh/1085-change-heater-power-for-extruder-v3
Update heater power to the real values for extruder v3
2026-01-30 15:14:59 +01:00
Christian 1b2d6df459 Merge pull request #967 from qitechgmbh/Hamids-Branch
BECKHOFF minimal example motor
2026-01-30 13:24:54 +01:00
Christian cd2e15cb29 Update heater power to the real values for extruder v3 2026-01-30 13:23:46 +01:00
Christian f1571c0daa Merge pull request #1082 from qitechgmbh/revert-1077-1076-minimum-hardware-documentation-links
Revert "docs(readme): add documentation links and update machine table"
2026-01-29 14:50:33 +01:00
Christian 96e0fb83ef Revert "docs(readme): add documentation links and update machine table" 2026-01-29 14:50:10 +01:00
Christian 94cffbf717 Merge pull request #1077 from qitechgmbh/1076-minimum-hardware-documentation-links
docs(readme): add documentation links and update machine table
2026-01-29 13:22:14 +01:00
EinsPommes 38b2e907f9 docs(readme): update documentation links and improve machine table clarity 2026-01-28 17:16:10 +01:00
ᴏxᴏ.ᴍɪᴋᴀ 7e5498751a Merge branch 'master' into 1076-minimum-hardware-documentation-links 2026-01-28 17:13:52 +01:00
Christian eef9d63de2 Merge pull request #1074 from qitechgmbh/dependabot/npm_and_yarn/electron/multi-c5fe2c841a
Bump seroval and solid-js in /electron
2026-01-27 11:00:51 +01:00
Christian d9d9b9d344 update hash 2026-01-27 10:47:20 +01:00
Christian 2ff3d8df56 remove deps hash 2026-01-27 10:43:38 +01:00
Christian 462672bb6a Add peer dependency flag to multiple packages in package-lock.json 2026-01-27 10:38:58 +01:00
dependabot[bot] 84d539a4fa Bump seroval and solid-js in /electron
Bumps [seroval](https://github.com/lxsmnsyc/seroval) and [solid-js](https://github.com/solidjs/solid). These dependencies needed to be updated together.

Updates `seroval` from 1.3.2 to 1.5.0
- [Release notes](https://github.com/lxsmnsyc/seroval/releases)
- [Commits](https://github.com/lxsmnsyc/seroval/commits)

Updates `solid-js` from 1.9.10 to 1.9.11
- [Release notes](https://github.com/solidjs/solid/releases)
- [Changelog](https://github.com/solidjs/solid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/solidjs/solid/commits)

---
updated-dependencies:
- dependency-name: seroval
  dependency-version: 1.5.0
  dependency-type: indirect
- dependency-name: solid-js
  dependency-version: 1.9.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 10:38:58 +01:00
Robin Krämer 850b2d411d Merge pull request #1080 from qitechgmbh/changelog-2.14.1
updated Changelog
2026-01-26 16:37:03 +01:00
EinsPommes 6a6c09e283 docs(readme): add documentation links and update machine table 2026-01-25 22:03:53 +01:00
Oshgnacknak 61b9ac8a39 Merge branch 'master' into Hamids-Branch 2026-01-23 16:14:22 +01:00
Christian bb169e3b33 Fix formatting in registry.rs by removing extra blank line 2026-01-22 14:11:19 +01:00
Christian 3d735f8abd Merge branch 'master' into Hamids-Branch 2026-01-22 13:48:58 +01:00
Christian a9a973e336 Merge branch 'master' into Hamids-Branch 2026-01-21 17:11:58 +01:00
Christian b66a59dba9 Fix naming 2026-01-21 14:33:18 +01:00
Christian 9dbc962dbc Update file paths 2026-01-21 14:33:18 +01:00
Christian 93e2558803 Translate comments from German to English 2026-01-21 14:33:18 +01:00
Christian 8cf7480833 Fix path in routes.tsx 2026-01-21 14:33:18 +01:00
Christian 08d655effc Fix naming and folder structure 2026-01-21 14:33:18 +01:00
Christian c1c867fca1 Change minimal motor readme for current implementation 2026-01-21 14:33:18 +01:00
Hamid-el 1378c00e1a added .idea to .gitignore 2026-01-21 14:33:18 +01:00
Hamid-el 85f17befbe removed .idea 2026-01-21 14:33:18 +01:00
Hamid-el f2e764f93a finished stepper motor + doc finished (real version3) 2026-01-21 14:33:18 +01:00
Hamid-el 9adeeda490 finished stepper motor + doc finished (real version2) 2026-01-21 14:33:18 +01:00
Hamid-el 68bf075172 finished stepper motor + doc finished (real version) 2026-01-21 14:33:18 +01:00
Hamid-el 7d23190939 stepper motor doc finished 2026-01-21 14:33:18 +01:00
Hamid-el 5c68e2c78a finished stepper motor 2026-01-21 14:33:18 +01:00
Hamid-el 980e14a96e stepper motor with ethercat beckhoff Ek1100 and EL7031 0030 + UI works 2026-01-21 14:33:18 +01:00
Hamid-el d5fb45e4e3 stepper motor with ethercat beckhoff Ek1100 and EL7031 works 2026-01-21 14:33:18 +01:00
Hamid-el cdb7ab1e0c small adjustment 2026-01-21 14:33:18 +01:00
Hamid-el da934e0bb3 first version of manually running a stepper motor with ethercat beckhoff Ek1100 and EL7031
# Conflicts:
#	machines/src/aquapath1/act.rs
#	machines/src/registry.rs
#	server/src/ethercat/setup.rs
2026-01-21 14:33:18 +01:00
44 changed files with 1336 additions and 198 deletions

No files matched your search

+2 -1
View File
@@ -7,4 +7,5 @@ build.sh
shell.nix
node_modules/
runtime_metrics.csv
compile_metrics.csv
compile_metrics.csv
.idea/
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

+1
View File
@@ -89,6 +89,7 @@ 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.
@@ -0,0 +1,236 @@
# Minimal Example — Beckhoff EL7031-0030 Stepper Motor Integration
A complete hardware + software walkthrough
---
## Table of Contents
1. [Introduction](#1-introduction)
2. [Requirements](#2-requirements)
3. [Hardware Setup](#3-hardware-setup)
- [3.2 EK1100 Wiring](#32-ek1100-wiring)
- [3.2.1 Safe Wiring Procedure](#321-safe-wiring-procedure-beckhoff-recommended)
- [3.2.2 Wiring (EL7031-0030)](#322-wiring-el7031-0030)
- [3.3 Safety Warning](#33--safety-warning)
4. [Software Setup](#4-software-setup)
- [4.1 Installing on Linux](#41-installing-on-linux-this-depends-on-your-distro)
- [4.2 Running the Backend](#42-running-the-backend)
- [4.3 Running the Frontend](#43-running-the-frontend)
5. [Demo](#5-demo)
- [5.1 Assigning Devices in the Dashboard](#51-assigning-devices-in-the-dashboard)
6. [Documentation](#6-documentation)
7. [Software Architecture](#7-software-architecture)
---
## 1. Introduction
This project documents the successful integration of a **Beckhoff EL7031-0030 Stepper Motor Terminal** into the QiTech control software stack.
The goal was to control a 24V stepper motor via EtherCAT using a custom Rust backend and a React/Electron frontend.
---
## 2. Requirements
### Software
- Rust toolchain
- Node.js + npm
- Git
- QiTech Control repository
- EtherCAT HAL (included inside repo)
### Hardware
- **EtherCAT Master:** Linux PC
- **Bus Coupler:** EtherCAT Beckhoff EK1100
- **Stepper Driver:** Beckhoff EL7031-0030 (THIS IS DIFFERENT FROM EL7031)
- **Motor:** Standard 4-wire Stepper Motor
- **Power Supply:** 24V DC
- **Ethernet Cable:** Standard Ethernet cable
- **Wiring Tools:** Screwdriver, wires
---
## 3. Hardware Setup
### 3.2 EK1100 Wiring
This wiring configuration powers the EL7031-0030.
It is not the only possible wiring but is the **simplest functional setup**.
#### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
---
#### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](../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.
+47 -47
View File
@@ -162,6 +162,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -658,6 +659,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -681,6 +683,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -3280,6 +3283,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0",
@@ -3370,6 +3374,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0",
@@ -3590,8 +3595,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3757,6 +3761,7 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3773,6 +3778,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3783,6 +3789,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3870,6 +3877,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -4228,6 +4236,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4291,7 +4300,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4606,6 +4614,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4988,7 +4997,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -5261,8 +5271,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5808,6 +5817,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5868,6 +5878,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5969,6 +5980,7 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6927,6 +6939,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -6960,6 +6973,7 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -7562,6 +7576,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -8095,7 +8110,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9515,6 +9529,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9631,7 +9646,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9647,7 +9661,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9737,6 +9750,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9746,6 +9760,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9758,6 +9773,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9800,8 +9816,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -10311,18 +10326,19 @@
}
},
"node_modules/seroval": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
"integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz",
"integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.4.0.tgz",
"integrity": "sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.0.tgz",
"integrity": "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==",
"license": "MIT",
"engines": {
"node": ">=10"
@@ -10565,38 +10581,15 @@
}
},
"node_modules/solid-js": {
"version": "1.9.10",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
"version": "1.9.11",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz",
"integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.1.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",
"peer": true,
"engines": {
"node": ">=10"
},
"peerDependencies": {
"seroval": "^1.0"
"seroval": "~1.5.0",
"seroval-plugins": "~1.5.0"
}
},
"node_modules/sonner": {
@@ -10916,7 +10909,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -11005,6 +10999,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11268,6 +11263,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11604,6 +11600,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -11716,7 +11713,8 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
@@ -11754,6 +11752,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12188,6 +12187,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -9,7 +9,6 @@ import { EditValue } from "@/control/EditValue";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/control/Label";
import { HeatingZone } from "../../extruder/HeatingZone";
import { StatusBadge } from "@/control/StatusBadge";
export function Aquapath1ControlPage() {
const {
@@ -52,19 +51,21 @@ export function Aquapath1ControlPage() {
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
console.log(state);
return (
<Page>
<ControlGrid columns={2}>
<ControlCard title="Reservoir 1">
<div className="grid grid-rows-5 gap-4">
<div className="flex flex-row">
{ state?.front_flow === true ? (
<StatusBadge variant="success">Flow: active</StatusBadge>
) : (
<StatusBadge variant="error">Flow: inactive</StatusBadge>
)}
<TimeSeriesValueNumeric
label="Flow"
unit="l/min"
timeseries={front_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row">
<TimeSeriesValueNumeric
label="Temperature"
@@ -148,13 +149,13 @@ export function Aquapath1ControlPage() {
<ControlCard title="Reservoir 2">
<div className="grid grid-rows-4 gap-4">
<div className="flex flex-row">
<div className="flex flex-row">
{ state?.back_flow === true ? (
<StatusBadge variant="success">Flow: active</StatusBadge>
) : (
<StatusBadge variant="error">Flow: inactive</StatusBadge>
)}
</div></div>
<TimeSeriesValueNumeric
label="Flow"
unit="l/min"
timeseries={back_flow}
renderValue={(value) => value.toFixed(1)}
/>
</div>
<div className="flex flex-row">
<TimeSeriesValueNumeric
@@ -14,12 +14,12 @@ export function Aquapath1Page() {
title: "Control",
icon: "lu:CirclePlay",
},
//{
// link: "graph",
//title: "Graph",
//icon: "lu:ChartSpline",
//activeLink: "graphs",
//},
{
link: "graph",
title: "Graph",
icon: "lu:ChartSpline",
activeLink: "graphs",
},
{
link: "settings",
title: "Config",
@@ -72,6 +72,8 @@ export const toleranceStatesSchema = z.object({
* Live values event schema (time-series data)
*/
export const liveValuesEventDataSchema = z.object({
front_flow: z.number(),
back_flow: z.number(),
front_temperature: z.number(),
back_temperature: z.number(),
front_temp_reservoir: z.number(),
@@ -94,8 +96,6 @@ export const stateEventDataSchema = z.object({
temperature_states: tempStatesSchema,
fan_states: fanStatesSchema,
tolerance_states: toleranceStatesSchema,
front_flow: z.boolean(),
back_flow: z.boolean(),
});
// ========== Event Schemas with Wrappers ==========
@@ -113,6 +113,9 @@ export type Aquapath1NamespaceStore = {
state: StateEvent | null;
defaultState: StateEvent | null;
front_flow: TimeSeries;
back_flow: TimeSeries;
front_temperature: TimeSeries;
back_temperature: TimeSeries;
@@ -214,7 +217,7 @@ export function aquapath1MessageHandler(
// Live values events (time-series data)
else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
console.log(liveValuesEvent);
updateStore((state) => ({
...state,
front_temperature: addTemperature1(state.front_temperature, {
@@ -14,7 +14,7 @@ import {
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../client/socketioStore";
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
@@ -0,0 +1,53 @@
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>
);
}
@@ -0,0 +1,22 @@
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",
},
]}
/>
);
}
@@ -0,0 +1,73 @@
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);
}
@@ -0,0 +1,96 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMotorSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMotorNamespace, StateEvent } from "./testMotorNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { TestMotor } from "@/machines/properties";
import { z } from "zod";
export function useTestMotor() {
const { serial: serialString } = testMotorSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: TestMotor.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMotorNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
type: z.string(),
payload: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
// --- ACTIONS ---
const setMotorOn = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.motor_enabled = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
// Must exactly match the 'Mutation' enum!
data: { type: "SetMotorOn", payload: on },
}),
);
};
const setVelocity = (velocity: number) => {
updateStateOptimistically(
(current) => {
current.motor_velocity = velocity;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { type: "SetMotorVelocity", payload: velocity },
}),
);
};
return {
state: stateOptimistic.value,
setMotorOn,
setVelocity,
};
}
+39 -3
View File
@@ -416,12 +416,12 @@ export const aquapath1: MachineProperties = {
},
{
role: 4,
role_label: "EL1002",
role_label: "EL5152",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x3ea3052,
revision: 0x120000,
product_id: 0x14203052,
revision: 0x140000,
},
],
},
@@ -544,6 +544,41 @@ 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",
@@ -582,6 +617,7 @@ export const machineProperties: MachineProperties[] = [
wagoAiTestMachine,
digitalInputTestMachine,
ip20TestMachine,
TestMotor,
];
export const getMachineProperties = (
+17
View File
@@ -67,6 +67,8 @@ import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestm
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/motor_test_machine/TestMotorControlPage";
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
@@ -142,6 +144,18 @@ export const ip20TestMachineControlRoute = createRoute({
component: () => <IP20TestMachineControlPage />,
});
export const testMotorSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmotor/$serial",
component: () => <TestMotorPage />,
});
export const testMotorControlRoute = createRoute({
getParentRoute: () => testMotorSerialRoute,
path: "control",
component: () => <TestMotorControlPage />,
});
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
path: "_sidebar",
@@ -470,6 +484,7 @@ export const rootTree = RootRoute.addChildren([
laser1GraphsRoute,
laser1PresetsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
analogInputTestMachineSerialRoute.addChildren([
@@ -484,6 +499,8 @@ export const rootTree = RootRoute.addChildren([
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
testMotorSerialRoute.addChildren([testMotorControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
aquapath1GraphRoute,
+8 -2
View File
@@ -161,12 +161,18 @@ impl crate::coe::Configuration for EL3062_0030TxPdo {
len += 1;
device.sdo_write(0x1C13, len, 0x1A00u16).await?;
}
if let Some(_) = &self.ai_compact_channel1 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A01u16).await?;
}
if let Some(_) = &self.ai_standard_channel2 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A02u16).await?;
}
if let Some(_) = &self.ai_compact_channel2 {
len += 1;
device.sdo_write(0x1C13, len, 0x1A03u16).await?;
}
device.sdo_write(0x1C13, 0, len).await?;
Ok(())
}
@@ -351,6 +351,7 @@ 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,
+2 -1
View File
@@ -56,8 +56,9 @@ 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, Copy)]
#[derive(Debug, Clone)]
pub struct Module {
pub name: String,
pub slot: u16,
pub belongs_to_addr: u16,
pub has_tx: bool,
+118 -53
View File
@@ -2,6 +2,9 @@ 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::{
@@ -20,24 +23,25 @@ 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);
// 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
*/
#[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
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_offsets: Vec<usize>,
tx_offsets: Vec<usize>,
rx_pdo_mappings: Vec<ModulePdoMapping>,
tx_pdo_mappings: Vec<ModulePdoMapping>,
tx_size: usize,
rx_size: usize,
}
@@ -105,7 +109,7 @@ impl EthercatDevice for Wago750_354 {
}
fn set_module(&mut self, module: Module) {
self.slots[self.module_count] = Some(module);
self.slots[self.module_count] = Some(module.clone());
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
@@ -134,8 +138,8 @@ impl NewEthercatDevice for Wago750_354 {
dev_count: 0,
tx_size: 0,
rx_size: 0,
rx_offsets: vec![],
tx_offsets: vec![],
rx_pdo_mappings: vec![],
tx_pdo_mappings: vec![],
}
}
}
@@ -147,39 +151,76 @@ 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<usize> = vec![];
let mut vec: Vec<ModulePdoMapping> = vec![];
let mut bit_offset = 0;
let start_subindex = 0x1;
let mut module_i;
let start_subindex = 0x2;
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?;
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;
}
}
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);
}
}
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_offsets = vec;
self.tx_pdo_mappings = vec;
} else {
self.rx_offsets = vec;
self.rx_pdo_mappings = vec;
}
Ok(())
}
@@ -222,6 +263,7 @@ impl Wago750_354 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -229,26 +271,37 @@ 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: {}",
@@ -266,17 +319,42 @@ 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;
});
println!("{:?}\n\n{:?}", self.tx_offsets, self.rx_offsets);
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,
}
}
for module in self.slots {
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 {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
@@ -302,6 +380,10 @@ impl Wago750_354 {
WAGO_750_402_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
}
WAGO_750_430_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_430::Wago750_430::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -312,27 +394,10 @@ 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();
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 => (),
}
//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);
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);
self.slots[self.module_count] = Some(module.clone());
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
@@ -378,6 +378,7 @@ impl IP20EcDi8Do8 {
has_rx: false,
tx_offset: 0,
rx_offset: 0,
name: "".to_string(),
};
match ident_iom {
@@ -415,7 +416,7 @@ impl IP20EcDi8Do8 {
let _ = self.get_pdo_offsets(device, false).await;
});
for module in self.slots {
for module in &self.slots {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
@@ -1,6 +1,7 @@
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;
@@ -0,0 +1,211 @@
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
SubDeviceProductTuple,
};
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput};
#[derive(Clone)]
pub struct Wago750_430 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub txpdo: Wago750_430TxPdo,
module: Option<Module>,
}
#[derive(Debug, Clone)]
pub enum Wago750_430Port {
Port1,
Port2,
Port3,
Port4,
Port5,
Port6,
Port7,
Port8,
}
impl From<Wago750_430Port> for usize {
fn from(value: Wago750_430Port) -> Self {
match value {
Wago750_430Port::Port1 => 0,
Wago750_430Port::Port2 => 1,
Wago750_430Port::Port3 => 2,
Wago750_430Port::Port4 => 3,
Wago750_430Port::Port5 => 4,
Wago750_430Port::Port6 => 5,
Wago750_430Port::Port7 => 6,
Wago750_430Port::Port8 => 7,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_430TxPdo {
pub port1: bool,
pub port2: bool,
pub port3: bool,
pub port4: bool,
pub port5: bool,
pub port6: bool,
pub port7: bool,
pub port8: bool,
}
impl DigitalInputDevice<Wago750_430Port> for Wago750_430 {
fn get_input(
&self,
port: Wago750_430Port,
) -> Result<crate::io::digital_input::DigitalInputInput, anyhow::Error> {
match port {
Wago750_430Port::Port1 => Ok(DigitalInputInput {
value: self.txpdo.port1,
}),
Wago750_430Port::Port2 => Ok(DigitalInputInput {
value: self.txpdo.port2,
}),
Wago750_430Port::Port3 => Ok(DigitalInputInput {
value: self.txpdo.port3,
}),
Wago750_430Port::Port4 => Ok(DigitalInputInput {
value: self.txpdo.port4,
}),
Wago750_430Port::Port5 => Ok(DigitalInputInput {
value: self.txpdo.port5,
}),
Wago750_430Port::Port6 => Ok(DigitalInputInput {
value: self.txpdo.port6,
}),
Wago750_430Port::Port7 => Ok(DigitalInputInput {
value: self.txpdo.port7,
}),
Wago750_430Port::Port8 => Ok(DigitalInputInput {
value: self.txpdo.port8,
}),
}
}
}
impl EthercatDeviceUsed for Wago750_430 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_430 {}
impl EthercatDynamicPDO for Wago750_430 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
}
fn get_rx_offset(&self) -> usize {
self.rx_bit_offset
}
fn set_tx_offset(&mut self, offset: usize) {
self.tx_bit_offset = offset
}
fn set_rx_offset(&mut self, offset: usize) {
self.rx_bit_offset = offset
}
}
impl EthercatDevice for Wago750_430 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
self.txpdo.port1 = input[0];
self.txpdo.port2 = input[1];
self.txpdo.port3 = input[2];
self.txpdo.port4 = input[3];
self.txpdo.port5 = input[4];
self.txpdo.port6 = input[5];
self.txpdo.port7 = input[6];
self.txpdo.port8 = input[7];
Ok(())
}
fn input_len(&self) -> usize {
8
}
fn output(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_len(&self) -> usize {
0
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_checked(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn get_module(&self) -> Option<crate::devices::Module> {
self.module.clone()
}
fn set_module(&mut self, module: crate::devices::Module) {
self.tx_bit_offset = module.tx_offset;
self.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl EthercatDeviceProcessing for Wago750_430 {}
impl NewEthercatDevice for Wago750_430 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
txpdo: Wago750_430TxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_430 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_430")
}
}
pub const WAGO_750_430_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_430_PRODUCT_ID: u32 = 2147483777;
pub const WAGO_750_430_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_430_VENDOR_ID, WAGO_750_430_PRODUCT_ID);
@@ -148,8 +148,7 @@ impl EthercatDevice for Wago750_652 {
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = 32;
let base = self.rx_bit_offset;
let mut ol0_i = 5; // Ol0 starts at bit 5 and goes to bit 7 (inclusive)
let mut ol1_i = 8;
@@ -174,12 +173,10 @@ impl EthercatDevice for Wago750_652 {
);
ol1_i += 1;
}
println!("{}", self.rx_pdo.control.output_length);
let data_start = base + 16;
for (i, &byte) in self.rx_pdo.out_buffer.iter().take(22).enumerate() {
output[(data_start + i * 8)..(data_start + (i + 1) * 8)].store_le(byte);
}
//println!("{:?}",self.rx_pdo.out_buffer);
Ok(())
}
+2 -7
View File
@@ -23,13 +23,8 @@ impl MachineAct for AquaPathV1 {
let now = Instant::now();
if self.front_controller.update(now_ts) {
self.emit_state();
}
if self.back_controller.update(now_ts) {
self.emit_state();
}
self.front_controller.update(now_ts);
self.back_controller.update(now_ts);
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
+2 -2
View File
@@ -14,6 +14,8 @@ use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
pub front_flow: f64,
pub back_flow: f64,
pub front_temperature: f64,
pub back_temperature: f64,
pub front_temp_reservoir: f64,
@@ -41,8 +43,6 @@ pub struct StateEvent {
pub temperature_states: TempStates,
pub fan_states: FanStates,
pub tolerance_states: ToleranceStates,
pub front_flow: bool,
pub back_flow: bool,
}
impl StateEvent {
+25 -17
View File
@@ -1,8 +1,7 @@
use crate::aquapath1::VolumeRate;
use crate::aquapath1::{Flow, Temperature};
use control_core::controllers::pid::PidController;
use ethercat_hal::io::analog_input::AnalogInput;
use ethercat_hal::io::digital_input::DigitalInput;
use ethercat_hal::io::encoder_input::EncoderInput;
use ethercat_hal::io::{
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
};
@@ -45,10 +44,9 @@ pub struct Controller {
pub heating_allowed: bool,
pub flow: Flow,
pub is_flowing : bool,
pump_relais: DigitalOutput,
pub should_pump: bool,
pub flow_sensor: AnalogInput,
pub flow_sensor: EncoderInput,
pub pump_allowed: bool,
pub current_flow: VolumeRate,
pub max_flow: VolumeRate,
@@ -70,7 +68,7 @@ impl Controller {
flow: Flow,
pump_relais: DigitalOutput,
flow_sensor: AnalogInput,
flow_sensor: EncoderInput,
) -> Self {
Self {
pid: PidController::new(kp, ki, kd),
@@ -95,7 +93,6 @@ impl Controller {
cooling_allowed: false,
heating_allowed: false,
is_flowing : false,
temperature_sensor_in: temp_sensor_in,
temperature_sensor_out: temp_sensor_out,
@@ -223,15 +220,27 @@ impl Controller {
self.should_pump
}
pub fn get_flow(&mut self) -> bool {
/* let value = match self.flow_sensor.get_value() {
pub fn get_flow(&mut self) -> VolumeRate {
let value = match self.flow_sensor.get_frequency_value() {
Ok(val) => val,
Err(_e) => {
false
return VolumeRate::new::<liter_per_minute>(0.0);
}
};*/
};
false
match value {
Some(val) => {
if val == 0 {
return VolumeRate::new::<liter_per_minute>(0.0);
}
// Formula: f = 8.1*q - 3, so q = (f + 3) / 8.1
let actual_flow = ((val / 100) as f32 + 3.0) / 8.1;
VolumeRate::new::<liter_per_minute>(actual_flow.into())
}
None => {
return VolumeRate::new::<liter_per_minute>(0.0);
}
}
}
pub fn get_current_revolutions(&self) -> AngularVelocity {
@@ -263,10 +272,10 @@ impl Controller {
self.total_energy
}
pub fn update(&mut self, now: Instant) -> bool {
pub fn update(&mut self, now: Instant) -> () {
let current_flow = self.get_flow();
let flow_changed = current_flow == self.is_flowing;
self.is_flowing = current_flow;
self.current_flow = current_flow;
self.flow.flow = current_flow;
let should_flow = self.get_should_pump();
self.flow.should_pump = should_flow;
@@ -299,7 +308,7 @@ impl Controller {
if self.temperature.cooling {
self.turn_cooling_off();
}
if self.heating_allowed && self.is_flowing {
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;
@@ -314,7 +323,7 @@ impl Controller {
if self.temperature.heating {
self.turn_heating_off();
}
if self.cooling_allowed && self.is_flowing {
if self.cooling_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
if !self.temperature.cooling {
self.turn_cooling_on();
}
@@ -335,6 +344,5 @@ impl Controller {
}
}
}
flow_changed
}
}
+2 -2
View File
@@ -121,6 +121,8 @@ impl AquaPathV1 {
.back_controller
.current_temperature
.get::<degree_celsius>(),
front_flow: self.front_controller.current_flow.get::<liter_per_minute>(),
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
@@ -226,8 +228,6 @@ impl AquaPathV1 {
.get::<degree_celsius>(),
},
},
front_flow: self.front_controller.is_flowing,
back_flow: self.back_controller.is_flowing,
}
}
+46 -24
View File
@@ -11,13 +11,18 @@ use anyhow::Error;
use ethercat_hal::{
coe::ConfigurableDevice,
devices::{
ek1100::{EK1100, EK1100_IDENTITY_A}, el1002::{EL1002, EL1002_IDENTITY_A}, el2008::{EL2008, EL2008_IDENTITY_A, EL2008_IDENTITY_B, EL2008Port}, el3062_0030::{EL3062_0030, EL3062_0030_IDENTITY_A, EL3062_0030Configuration}, el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port}, el4002::{EL4002, EL4002_IDENTITY_A, EL4002Port}, el5152::{
ek1100::{EK1100, EK1100_IDENTITY_A},
el2008::{EL2008, EL2008_IDENTITY_A, EL2008_IDENTITY_B, EL2008Port},
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
el4002::{EL4002, EL4002_IDENTITY_A, EL4002Port},
el5152::{
EL5152, EL5152_IDENTITY_A, EL5152Configuration, EL5152Port,
EL5152PredefinedPdoAssignment,
}
},
},
io::{
analog_input::AnalogInput, analog_output::AnalogOutput, digital_input::DigitalInput, digital_output::DigitalOutput, encoder_input::EncoderInput, temperature_input::TemperatureInput
analog_output::AnalogOutput, digital_output::DigitalOutput, encoder_input::EncoderInput,
temperature_input::TemperatureInput,
},
};
use std::time::Instant;
@@ -78,32 +83,49 @@ impl MachineNewTrait for AquaPathV1 {
.await?
.0;
let el3062_0030 =
get_ethercat_device::<EL3062_0030>(hardware, params, 4, [EL3062_0030_IDENTITY_A].to_vec())
let el5152 =
get_ethercat_device::<EL5152>(hardware, params, 4, [EL5152_IDENTITY_A].to_vec())
.await?
.0;
let el3062_0030_subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
let mut guard = el3062_0030.write().await;
let config = EL3062_0030Configuration::default();
guard.write_config(el3062_0030_subdevice,&config).await;
drop(guard);
let config = EL5152Configuration {
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
..Default::default()
};
let subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
el5152
.write()
.await
.write_config(&subdevice, &config)
.await?;
let ai1 = AnalogInput::new(el3062_0030.clone(),ethercat_hal::devices::el3062_0030::EL3062_0030Port::AI1);
let ai2 = AnalogInput::new(el3062_0030.clone(), ethercat_hal::devices::el3062_0030::EL3062_0030Port::AI2);
let enc1 = EncoderInput::new(el5152.clone(), EL5152Port::ENC1);
let enc2 = EncoderInput::new(el5152.clone(), EL5152Port::ENC2);
//after heating
let t1 = TemperatureInput::new(el3204.clone(), EL3204Port::T1);
let t2 = TemperatureInput::new(el3204.clone(), EL3204Port::T2);
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t1 = TemperatureInput::new(el3204.clone(), EL3204Port::T1);
//in reservoir
let t2 = TemperatureInput::new(el3204.clone(), EL3204Port::T2);
//after heating
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
//in reservoir
let t4 = TemperatureInput::new(el3204.clone(), EL3204Port::T4);
//pump flow control
let do1 = DigitalOutput::new(el2008.clone(), EL2008Port::DO1);
let do2 = DigitalOutput::new(el2008.clone(), EL2008Port::DO2);
let do4 = DigitalOutput::new(el2008.clone(), EL2008Port::DO4);
let do5 = DigitalOutput::new(el2008.clone(), EL2008Port::DO5);
let do6 = DigitalOutput::new(el2008.clone(), EL2008Port::DO6);
//pump flow control
//phys 1
let do1 = DigitalOutput::new(el2008.clone(), EL2008Port::DO1);
//phys 5
let do2 = DigitalOutput::new(el2008.clone(), EL2008Port::DO2);
//heating
//phys 6
let do4 = DigitalOutput::new(el2008.clone(), EL2008Port::DO4);
//phys 3
let do5 = DigitalOutput::new(el2008.clone(), EL2008Port::DO5);
//phys 7
let do6 = DigitalOutput::new(el2008.clone(), EL2008Port::DO6);
//cooling power cut
//phys 8
let do8 = DigitalOutput::new(el2008.clone(), EL2008Port::DO8);
let ao1 = AnalogOutput::new(el4002.clone(), EL4002Port::AO1);
@@ -123,7 +145,7 @@ impl MachineNewTrait for AquaPathV1 {
AngularVelocity::new::<revolution_per_minute>(100.0),
Flow::default(),
do1,
ai1,
enc1,
);
let back_controller = Controller::new(
@@ -140,7 +162,7 @@ impl MachineNewTrait for AquaPathV1 {
AngularVelocity::new::<revolution_per_minute>(100.0),
Flow::default(),
do5,
ai2,
enc2,
);
let (sender, receiver) = smol::channel::unbounded();
let mut water_cooling = Self {
+4 -4
View File
@@ -159,7 +159,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_1,
Heating::default(),
Duration::from_millis(500),
700.0,
900.0,
1.0,
);
@@ -174,7 +174,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_2,
Heating::default(),
Duration::from_millis(500),
700.0,
900.0,
1.0,
);
@@ -189,7 +189,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_3,
Heating::default(),
Duration::from_millis(500),
700.0,
900.0,
1.0,
);
@@ -204,7 +204,7 @@ impl MachineNewTrait for ExtruderV3 {
digital_out_4,
Heating::default(),
Duration::from_millis(500),
200.0,
150.0,
0.95,
);
+3
View File
@@ -28,6 +28,7 @@ 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;
@@ -50,6 +51,8 @@ pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
use serde_json::Value;
use smol::lock::RwLock;
+40
View File
@@ -0,0 +1,40 @@
use super::MotorTestMachine;
use crate::{MachineAct, MachineMessage};
use std::time::Instant;
impl MachineAct for MotorTestMachine {
fn act(&mut self, _now_ts: Instant) {
// println!("[{}::act] Running act", module_path!());
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
self.motor_driver.set_enabled(self.motor_state.enabled);
if self.motor_state.enabled {
let steps_per_rev = 200.0; // Adjust to match motor
let steps_per_second = (self.motor_state.target_velocity as f64) * steps_per_rev / 60.0;
let _ = self.motor_driver.set_speed(steps_per_second);
} else {
let _ = self.motor_driver.set_speed(0.0);
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(ns) => {
self.namespace.namespace = Some(ns);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => {
self.namespace.namespace = None;
}
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _ = self.api_mutate(value);
}
_ => {}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
use super::MotorTestMachine;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub motor_enabled: bool,
pub motor_velocity: i32,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum BeckhoffEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(tag = "type", content = "payload")]
enum Mutation {
SetMotorOn(bool),
SetMotorVelocity(i32),
SetMotorOff(bool),
}
#[derive(Debug, Clone)]
pub struct BeckhoffNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<BeckhoffEvents> for BeckhoffNamespace {
#[instrument(skip_all)]
fn emit(&mut self, events: BeckhoffEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<BeckhoffEvents> for BeckhoffEvents {
fn event_value(&self) -> GenericEvent {
match self {
BeckhoffEvents::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for MotorTestMachine {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetMotorOn(t) => {
if t {
self.turn_motor_on();
} else {
self.turn_motor_off();
}
}
Mutation::SetMotorVelocity(vel) => {
self.motor_state.target_velocity = vel;
self.emit_state();
}
_ => {}
}
Ok(())
}
}
+64
View File
@@ -0,0 +1,64 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use crate::{MOTOR_TEST_MACHINE, VENDOR_QITECH};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use smol::channel::{Receiver, Sender};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug, Clone, PartialEq)]
pub struct MotorState {
pub enabled: bool,
pub target_velocity: i32,
}
#[derive(Debug)]
pub struct MotorTestMachine {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: api::BeckhoffNamespace,
pub motor_driver: StepperVelocityEL70x1,
pub motor_state: MotorState,
}
impl Machine for MotorTestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl MotorTestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MOTOR_TEST_MACHINE,
};
pub fn emit_state(&mut self) {
let event = api::StateEvent {
motor_enabled: self.motor_state.enabled,
motor_velocity: self.motor_state.target_velocity,
};
self.namespace
.emit(api::BeckhoffEvents::State(event.build()));
}
pub fn turn_motor_on(&mut self) {
self.motor_state.enabled = true;
self.emit_state();
}
pub fn turn_motor_off(&mut self) {
self.motor_state.enabled = false;
self.emit_state();
}
}
+91
View File
@@ -0,0 +1,91 @@
use super::{MotorState, MotorTestMachine, api::BeckhoffNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::ek1100::{EK1100, EK1100_IDENTITY_A};
use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
use ethercat_hal::devices::el7031_0030::{
EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030StepperPort,
};
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
impl MachineNewTrait for MotorTestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
println!("[{}::new] Creating new MotorTestMachine", module_path!());
let device_identification = params.device_group.iter().cloned().collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => return Err(anyhow::anyhow!("Hardware is not EtherCAT")),
};
smol::block_on(async {
// Role 0: EK1100 (Koppler)
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
// Role 1: EL7031 (Stepper Motor)
let el7031 = {
let device = get_ethercat_device::<EL7031_0030>(
hardware,
params,
1,
vec![EL7031_0030_IDENTITY_A],
)
.await?;
let el7031_config = EL7031_0030Configuration {
stm_features: ethercat_hal::devices::el7031_0030::coe::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 1500,
..Default::default()
},
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_config)
.await?;
device.0
};
let motor_driver =
StepperVelocityEL70x1::new(el7031.clone(), EL7031_0030StepperPort::STM1);
let (sender, receiver) = smol::channel::unbounded();
Ok(Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: BeckhoffNamespace {
namespace: params.namespace.clone(),
},
motor_driver,
motor_state: MotorState {
enabled: true,
target_velocity: 100,
},
})
})
}
}
+3
View File
@@ -24,6 +24,7 @@ use crate::test_machine::TestMachine;
use lazy_static::lazy_static;
use crate::motor_test_machine::MotorTestMachine;
use anyhow::Error;
use std::{any::TypeId, collections::HashMap};
@@ -132,6 +133,8 @@ lazy_static! {
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
mc.register::<MotorTestMachine>(MotorTestMachine::MACHINE_IDENTIFICATION);
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
mc
+1 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-twp+30+hPKoMUojuGyJI1R6dknWWx9i5waqFw2FlHPc=";
npmDepsHash = "sha256-wzVjsgcZsViPOyQKlKTb5gJIeY416KyCSiheSXSRZoc=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
+2 -4
View File
@@ -364,7 +364,7 @@ pub async fn setup_loop(
Some(meta) => {
let meta_data = EtherCatDeviceMetaData {
configured_address: module.slot,
name: "wago module".to_owned(),
name:module.name,
vendor_id:module.vendor_id,
product_id: module.product_id,
revision: 0x2,
@@ -388,7 +388,7 @@ pub async fn setup_loop(
Some(meta) => {
let meta_data = EtherCatDeviceMetaData {
configured_address: module.slot,
name: "IP20 EC DI8 DO8 module".to_owned(),
name:module.name,
vendor_id:module.vendor_id,
product_id: module.product_id,
revision: 0x1,
@@ -433,8 +433,6 @@ pub async fn setup_loop(
)
.await?;
smol::Timer::after(Duration::from_millis(3000)).await;
let group_safe = match group_preop.into_safe_op(&maindevice).await {
Ok(group_op) => {
tracing::info!("Group in Safe-OP state");