Compare commits

..
Author SHA1 Message Date
Milan von dem Bussche 265100d699 Update README.md 2025-12-03 16:20:19 +01:00
59 changed files with 125 additions and 4814 deletions

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@ dist/
Cargo.lock
dhat-heap.json
build.sh
shell.nix
+34 -208
View File
@@ -74,9 +74,7 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
# More Docs docs
/assets
# More Docs
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
@@ -156,225 +154,53 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- [x] [NixOS Operating System](./docs/nixos/README.md)
## Minimal Example — LED Control on the EL2004
A complete hardware + software walkthrough
## Minimal Example
# Hello EtherCAT - LED Control on the EL2004
1. Introduction
2. Requirements
3. Wiring / Hardware Setup
4. Software Overview
5. Step-by-Step Implementation
6. Final Code Listing
7. Expected Behavior
---
## 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)
7. [Acknowledgements](#7-acknowledgements)
## 1. Introduction
The EL2004 LED Toggle Example is a minimal demonstration showing how to control digital outputs on a Beckhoff EL2004 EtherCAT terminal using the existing QiTech machine framework. It is designed as the simplest possible working example of hardware interaction: turning an LED on and off.
---
# 1. Introduction
## 2. Requirements
The EL2004 LED Toggle Example is a minimal demonstration showing how to control digital outputs on a **Beckhoff EL2004 EtherCAT terminal** using the QiTech machine framework.
It represents the simplest possible hardware interaction in the system:
**toggling LED outputs using the QiTech Control Dashboard.**
### Hardware
- Beckhoff EL2004 EtherCAT Terminal (Digital Output, 4 channels)
- Beckhoff EK1100 EtherCAT Coupler
- Power supply for EK1100 and EL2004 module (AC/DC Adapter with 24V Output )
- Connecting wires
- A PC connected to the EtherCAT network
### Software
- Rust toolchain (for building backend machine code)
- Node.js and npm/yarn (for frontend control interface)
- The project repository cloned locally
- Access to EtherCAT drivers and any required HAL bindings (available in the QITech control repository)
---
# 2. Requirements
## 3. Wiring / Hardware Setup
## Hardware
- Beckhoff **EL2004 EtherCAT Terminal** (4-channel digital output)
- Beckhoff **EK1100 EtherCAT Coupler**
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
- Jumper / bridge wires (0.5–1.5 mm² recommended)
- A **Linux PC** (Ubuntu/Debian recommended)
- Standard Ethernet cable
- Flat screwdriver
### Schematic
<img width="1123" height="859" alt="Screenshot 2025-12-03 at 16 12 26" src="https://github.com/user-attachments/assets/a04278d9-7f6a-442d-9ea7-7ace9ac44872" />
## Software
*(Installation steps in Section 4)*
- Rust toolchain
- Node.js + npm
- Git
- QiTech Control repository
- EtherCAT HAL (included inside repo)
---
### Needed Hardware Overview
# 3. Hardware Setup
<img width="813" height="1079" alt="Screenshot 2025-12-03 at 16 12 42" src="https://github.com/user-attachments/assets/af34a581-a668-4c6b-9d7f-062355813b6a" />
## 3.1 Schematic
![](./docs/assets/schematic.png)
Figure 1: Beckhoff EK1100 Bus Coupler — the device circles in red is a simple 24V Power Source
---
## 3.2 EK1100 Wiring
This wiring configuration powers the EL2004 and prepares it for LED control.
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.
![](./docs/assets/wiring.png)
---
## 3.2.2 Wiring Steps (Used in This Example)
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="./docs/assets/ek1100.jpeg" width="400">
---
## 3.3 EL2004 Integration
Slide the EL2004 onto the **right side of the EK1100** until it locks.
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
### **Figure 2 — EL2004 Terminal**
<img src="./docs/assets/el2004.jpeg" width="400">
---
## 3.4 Final Assembled Setup
### **Figure 3 — EK1100 + EL2004 Connected**
<img src="./docs/assets/complete.jpeg" width="400">
---
## 3.5 Power & Ethernet
### Power
Connect the 24 V adapter to the hollow plug used earlier.
**Example AC/DC Adapter (Figure 4):**
<img src="./docs/assets/adapter.jpeg" width="400">
### Ethernet
Use a standard LAN cable to connect your PC → EK1100.
The final powered up and connected setup should look like this:
<img src="./docs/assets/power.jpeg" 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 EK1100 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:
EK1100 Coupler
EL2004 Digital Output Terminal
![](./docs/assets/discovery.png)
Steps:
- Click Assign on the EK1100
- Select TestMachine V1
![](./docs/assets/setmachine.png)
- Enter a serial number (use the same for EK1100 + EL2004)
![](./docs/assets/serserial.png)
- Click Write
- Repeat for the EL2004
## 5.2 Testing LED Control
Navigate to:
Machines → TestMachine
![](./docs/assets/machinedetected.png)
You will see this interface:
![](./docs/assets/machinecontrol.png)
**You can now toggle the four digital outputs of the EL2004.**
# 6. References
This guide incorporates information from official Beckhoff documentation.
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
Referenced Manuals
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
[Beckhoff EL2004 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el20xx_el2124_de.pdf)
# 7. Acknowledgements
This tutorial is inspired by the clarity and educational quality of Beckhoff manuals.
All wiring illustrations and hardware descriptions in this guide are provided for demonstration purposes only and do not replace official Beckhoff installation guidelines.
Special thanks to the QiTech engineering team for providing the backend architecture, EtherCAT HAL abstraction, and the TestMachine framework that makes this example possible.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

+40 -17
View File
@@ -91,7 +91,7 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.7.3",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
@@ -175,6 +175,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -638,6 +639,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
},
@@ -660,6 +662,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1145,6 +1148,7 @@
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^4.1.1",
"fs-extra": "^9.0.1",
@@ -4091,6 +4095,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/react-store": "^0.7.0",
@@ -4172,6 +4177,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/store": "^0.7.0",
@@ -4405,8 +4411,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4563,6 +4568,7 @@
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.10.0"
}
@@ -4579,6 +4585,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4588,6 +4595,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4670,6 +4678,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
@@ -5024,6 +5033,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5471,6 +5481,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -6163,7 +6174,8 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -6436,8 +6448,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -7437,6 +7448,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7497,6 +7509,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8803,6 +8816,7 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -8865,6 +8879,7 @@
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -9539,6 +9554,7 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -10104,7 +10120,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -12045,11 +12060,11 @@
}
},
"node_modules/prettier": {
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz",
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12163,7 +12178,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12178,7 +12192,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12307,6 +12320,7 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12315,6 +12329,7 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -12326,6 +12341,7 @@
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -12366,8 +12382,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -13118,6 +13133,7 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -13907,7 +13923,8 @@
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -14009,7 +14026,8 @@
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"peer": true
},
"node_modules/tiny-warning": {
"version": "1.0.3",
@@ -14060,6 +14078,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14371,6 +14390,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14732,6 +14752,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",
@@ -14853,6 +14874,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15396,6 +15418,7 @@
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1 -1
View File
@@ -55,7 +55,7 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.7.3",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
@@ -152,7 +152,7 @@ export function Extruder2ControlPage() {
unit="rpm"
title="Target Output RPM"
min={0}
max={44}
max={106}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetRpm}
/>
@@ -1,258 +0,0 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import {
SelectionGroup,
SelectionGroupBoolean,
} from "@/control/SelectionGroup";
import { HeatingZone } from "../HeatingZone";
import { Label } from "@/control/Label";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { StatusBadge } from "@/control/StatusBadge";
export function Extruder3ControlPage() {
const {
state,
defaultState,
nozzleTemperature,
nozzlePower,
frontTemperature,
frontPower,
backTemperature,
backPower,
middleTemperature,
middlePower,
pressure,
motorScrewRpm,
motorPower,
motorCurrent,
totalEnergyKWh,
combinedPower,
setExtruderMode,
setBackHeatingTemperature,
setFrontHeatingTemperature,
setMiddleHeatingTemperature,
setNozzleHeatingTemperature,
setInverterRegulation,
setInverterTargetPressure,
setInverterTargetRpm,
isLoading,
isDisabled,
} = useExtruder3();
function isZoneReadyForExtrusion(
temperature: number,
targetTemperature: number,
) {
// if temperature is 90% of the target temperature, then we are ready for extrusion
return temperature >= 0.9 * targetTemperature && targetTemperature > 0.0;
}
const allZonesReadyForExtrude = () => {
const frontReady = isZoneReadyForExtrusion(
frontTemperature.current?.value ?? 0,
state?.heating_states.front.target_temperature ?? 1,
);
const middleReady = isZoneReadyForExtrusion(
middleTemperature.current?.value ?? 0,
state?.heating_states.middle.target_temperature ?? 1,
);
const backReady = isZoneReadyForExtrusion(
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
return (
<Page>
<ControlGrid>
<HeatingZone
title={"Heating Front"}
heatingState={state?.heating_states.front}
heatingTimeSeries={frontTemperature}
heatingPower={frontPower}
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Middle"}
heatingState={state?.heating_states.middle}
heatingTimeSeries={middleTemperature}
heatingPower={middlePower}
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Back"}
heatingState={state?.heating_states.back}
heatingTimeSeries={backTemperature}
heatingPower={backPower}
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Nozzle"}
heatingState={state?.heating_states.nozzle}
heatingTimeSeries={nozzleTemperature}
heatingPower={nozzlePower}
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
<StatusBadge variant="error">
Inverter is overloaded! Please check the extruder and reduce load
if necessary.
</StatusBadge>
) : state?.inverter_status_state.fault_occurence == true ? (
<StatusBadge variant="error">
Inverter encountered an error!! Press the restart button in Config
</StatusBadge>
) : state?.inverter_status_state.running == true &&
state.inverter_status_state.fault_occurence == false ? (
<StatusBadge variant="success">Running</StatusBadge>
) : null}
{state?.inverter_status_state.running == false &&
state.inverter_status_state.fault_occurence == false && (
<StatusBadge variant="success">Healthy</StatusBadge>
)}
<Label label="Regulation">
<SelectionGroupBoolean
value={state?.regulation_state.uses_rpm}
optionTrue={{ children: "RPM" }}
optionFalse={{ children: "Pressure" }}
onChange={setInverterRegulation}
disabled={isDisabled}
loading={isLoading}
/>
</Label>
<div className="flex flex-row flex-wrap gap-4">
<Label label="Target Output RPM">
<EditValue
value={state?.screw_state.target_rpm}
defaultValue={defaultState?.screw_state.target_rpm}
unit="rpm"
title="Target Output RPM"
min={0}
max={100}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetRpm}
/>
</Label>
<Label label="Target Pressure">
<EditValue
value={state?.pressure_state.target_bar}
defaultValue={defaultState?.pressure_state.target_bar}
unit="bar"
title="Target Pressure"
min={0.0}
max={270.0}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetPressure}
/>
</Label>
</div>
<div className="flex flex-row flex-wrap gap-4">
<TimeSeriesValueNumeric
label="Rpm"
unit="rpm"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorScrewRpm}
/>
{state?.pressure_state?.wiring_error && (
<StatusBadge variant="error">
Cant Measure Pressure! Check Pressure Sensor Wiring!
</StatusBadge>
)}
<TimeSeriesValueNumeric
label="Pressure"
unit="bar"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={pressure}
/>
</div>
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<"Standby" | "Heat" | "Extrude">
value={state?.mode_state.mode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Heat: {
children: "Heat",
icon: "lu:Flame",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Extrude: {
children: "Extrude",
icon: "lu:ArrowBigLeftDash",
isActiveClassName: "bg-green-600",
className: "h-full",
confirmation: allZonesReadyForExtrude()
? undefined
: "Temperature is too low. Are you sure you want to extrude?",
},
}}
onChange={setExtruderMode}
disabled={isDisabled}
loading={isLoading}
/>
</ControlCard>
<ControlCard className="bg-blue" title="Power Consumption">
<TimeSeriesValueNumeric
label="Total Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={combinedPower}
/>
<TimeSeriesValueNumeric
label="Motor Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorPower}
/>
<TimeSeriesValueNumeric
label="Motor Current"
unit="A"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={motorCurrent}
/>
<TimeSeriesValueNumeric
label="Total Energy Consumption"
unit="kWh"
renderValue={(value) => roundToDecimals(value, 3)}
timeseries={totalEnergyKWh}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,325 +0,0 @@
import { Page } from "@/components/Page";
import {
AutoSyncedBigGraph,
SyncedFloatingControlPanel,
useGraphSync,
type GraphConfig,
} from "@/components/graph";
import React from "react";
import { useExtruder3 } from "./useExtruder";
export function Extruder3GraphsPage() {
const {
state,
nozzleTemperature,
nozzlePower,
frontTemperature,
frontPower,
backTemperature,
backPower,
middleTemperature,
middlePower,
pressure,
motorScrewRpm,
motorCurrent,
motorPower,
combinedPower,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
// Base config
const baseConfig: GraphConfig = {
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
exportFilename: "extruder_data",
showLegend: true,
title: "extruder data",
};
const temperatureData = [
...(nozzleTemperature
? [
{
newData: nozzleTemperature,
title: "Nozzle",
color: "#ef4444",
lines:
state?.heating_states.nozzle?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
color: "#ef4444",
show: true,
},
]
: [],
},
]
: []),
...(frontTemperature
? [
{
newData: frontTemperature,
title: "Front",
color: "#f59e0b",
lines:
state?.heating_states.front?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
color: "#f59e0b",
show: true,
},
]
: [],
},
]
: []),
...(middleTemperature
? [
{
newData: middleTemperature,
title: "Middle",
color: "#8b5cf6",
lines:
state?.heating_states.middle?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
color: "#8b5cf6",
show: true,
},
]
: [],
},
]
: []),
...(backTemperature
? [
{
newData: backTemperature,
title: "Back",
color: "#3b82f6",
lines:
state?.heating_states.back?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
color: "#3b82f6",
show: true,
},
]
: [],
},
]
: []),
];
const temperatureConfig: GraphConfig = {
...baseConfig,
title: "Temperatures",
exportFilename: "temperatures_data",
colors: {
primary: "#ef4444",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Combined Power Graph (no target lines for power)
const powerData = [
...(nozzlePower
? [
{
newData: nozzlePower,
title: "Nozzle",
color: "#ef4444",
},
]
: []),
...(frontPower
? [
{
newData: frontPower,
title: "Front",
color: "#f59e0b",
},
]
: []),
...(middlePower
? [
{
newData: middlePower,
title: "Middle",
color: "#8b5cf6",
},
]
: []),
...(backPower
? [
{
newData: backPower,
title: "Back",
color: "#3b82f6",
},
]
: []),
...(motorPower
? [
{
newData: motorPower,
title: "Motor",
color: "#10b981",
},
]
: []),
...(combinedPower
? [
{
newData: combinedPower,
title: "Total",
color: "#000000",
},
]
: []),
];
const powerConfig: GraphConfig = {
...baseConfig,
title: "Power Outputs",
exportFilename: "power_data",
colors: {
primary: "#10b981",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
const currentConfig: GraphConfig = {
...baseConfig,
title: "Motor Current",
exportFilename: "motor_current_data",
colors: {
primary: "#3b82f6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Pressure Graph with connected target line
const pressureConfig: GraphConfig = {
...baseConfig,
title: "Pressure",
exportFilename: "pressure_data",
colors: {
primary: "#3b82f6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// RPM Graph with connected target line
const rpmConfig: GraphConfig = {
...baseConfig,
title: "RPM",
exportFilename: "rpm_data",
colors: {
primary: "#8b5cf6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
return (
<Page className="pb-25">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: pressure,
color: "#3b82f6",
lines:
state?.pressure_state.target_bar !== undefined
? [
{
type: "target" as const,
value: state.pressure_state.target_bar,
color: "#3b82f6",
show: true,
},
]
: [],
}}
config={pressureConfig}
unit="bar"
renderValue={(value) => value.toFixed(2)}
graphId="pressure-graph"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={temperatureData}
config={temperatureConfig}
unit="C"
renderValue={(value) => value.toFixed(1)}
graphId="combined-temperatures"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={powerData}
config={powerConfig}
unit="W"
renderValue={(value) => value.toFixed(1)}
graphId="combined-power"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: motorCurrent,
color: "#3b82f6",
}}
config={currentConfig}
unit="A"
renderValue={(value) => value.toFixed(2)}
graphId="motor-current"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: motorScrewRpm,
color: "#8b5cf6",
lines:
state?.screw_state.target_rpm !== undefined
? [
{
type: "target" as const,
value: state.screw_state.target_rpm,
color: "#8b5cf6",
show: true,
},
]
: [],
}}
config={rpmConfig}
unit="rpm"
renderValue={(value) => value.toFixed(0)}
graphId="rpm-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
@@ -1,12 +0,0 @@
import { Page } from "@/components/Page";
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
import React from "react";
import extruderManualContent from "@/assets/markdown/extruder/manual.md?raw";
export function ExtruderV3ManualPage() {
return (
<Page>
<MarkdownWithToc markdownContent={extruderManualContent} />
</Page>
);
}
@@ -1,44 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { extruder3Route } from "@/routes/routes";
import React from "react";
export function Extruder3Page() {
const { serial } = extruder3Route.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/extruder3/${serial}`}
items={[
{
link: "control",
title: "Control",
icon: "lu:CirclePlay",
activeLink: "control",
},
{
link: "graphs",
title: "Graphs",
icon: "lu:ChartSpline",
activeLink: "graphs",
},
{
link: "settings",
title: "Config",
icon: "lu:Settings",
activeLink: "settings",
},
{
link: "manual",
title: "Manual",
icon: "lu:BookOpen",
activeLink: "manual",
},
{
link: "presets",
title: "Presets",
icon: "lu:Save",
activeLink: "presets",
},
]}
/>
);
}
@@ -1,203 +0,0 @@
import React from "react";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { extruder3 } from "@/machines/properties";
import { useExtruder3 } from "./useExtruder";
import { z } from "zod";
import {
PresetPreviewEntries,
previewSeparator,
} from "@/components/preset/PresetPreviewTable";
import { Preset, PresetData } from "@/lib/preset/preset";
const extruder3PresetDataSchema = z
.object({
targetFrontHeatingTemperature: z.number(),
targetMiddleHeatingTemperature: z.number(),
targetBackHeatingTemperature: z.number(),
targetNozzleHeatingTemperature: z.number(),
targetInverterRpm: z.number(),
targetInverterPressure: z.number(),
inverterRegulation: z.enum(["RPM", "Pressure"]),
inverterRotationDirection: z.enum(["Forward", "Backward"]),
pressureLimit: z.number(),
pressureLimitEnabled: z.boolean(),
pidPressureKp: z.number(),
pidPressureKi: z.number(),
pidPressureKd: z.number(),
})
.partial();
type Extruder3 = typeof extruder3PresetDataSchema;
const schemas = new Map([[1, extruder3PresetDataSchema]]);
const previewEntries: PresetPreviewEntries<Extruder3> = [
{
name: "Target Front Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetFrontHeatingTemperature?.toFixed(1),
},
{
name: "Target Middle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetMiddleHeatingTemperature?.toFixed(1),
},
{
name: "Target Back Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetBackHeatingTemperature?.toFixed(1),
},
{
name: "Target Nozzle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetNozzleHeatingTemperature?.toFixed(1),
},
previewSeparator,
{
name: "Inverter Regulation",
renderValue: (data: PresetData<Extruder3>) => data.inverterRegulation,
},
{
name: "Target Inverter RPM",
unit: "rpm",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterRpm?.toFixed(2),
},
{
name: "Target Inverter Pressure",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterPressure?.toFixed(1),
},
{
name: "Inverter Direction",
renderValue: (data: PresetData<Extruder3>) =>
data.inverterRotationDirection,
},
previewSeparator,
{
name: "Pressure Limit",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimit?.toFixed(1),
},
{
name: "Enable Pressure Limit",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimitEnabled ? "on" : "off",
},
previewSeparator,
{
name: "PID Pressue Kp",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKp?.toFixed(4),
},
{
name: "PID Pressue Ki",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKi?.toFixed(4),
},
{
name: "PID Pressue Kd",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKd?.toFixed(4),
},
];
export function Extruder3PresetsPage() {
const {
state,
defaultState,
setFrontHeatingTemperature,
setMiddleHeatingTemperature,
setBackHeatingTemperature,
setNozzleHeatingTemperature,
setInverterTargetRpm,
setInverterTargetPressure,
setInverterRegulation,
setInverterRotationDirection,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKd,
setPressurePidKi,
setPressurePidKp,
} = useExtruder3();
const toPresetData = (s?: typeof state): PresetData<Extruder3> => ({
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
targetNozzleHeatingTemperature: s?.heating_states.nozzle.target_temperature,
targetInverterRpm: s?.screw_state.target_rpm,
targetInverterPressure: s?.pressure_state.target_bar,
inverterRegulation: s?.regulation_state.uses_rpm ? "RPM" : "Pressure",
inverterRotationDirection: s?.rotation_state?.forward
? "Forward"
: "Backward",
pressureLimit: s?.extruder_settings_state.pressure_limit,
pressureLimitEnabled: s?.extruder_settings_state.pressure_limit_enabled,
pidPressureKp: s?.pid_settings.pressure.kp,
pidPressureKi: s?.pid_settings.pressure.ki,
pidPressureKd: s?.pid_settings.pressure.kd,
});
const defaults = toPresetData(defaultState);
const applyPreset = (preset: Preset<Extruder3>) => {
setFrontHeatingTemperature(
preset.data.targetFrontHeatingTemperature ?? 150.0,
);
setMiddleHeatingTemperature(
preset.data.targetMiddleHeatingTemperature ?? 150.0,
);
setBackHeatingTemperature(
preset.data.targetBackHeatingTemperature ?? 150.0,
);
setNozzleHeatingTemperature(
preset.data.targetNozzleHeatingTemperature ?? 150.0,
);
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
setInverterRegulation(preset.data.inverterRegulation === "RPM");
setInverterRotationDirection(
preset.data.inverterRotationDirection === "Forward",
);
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
};
return (
<PresetsPage
machine_identification={extruder3.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
schemaVersion={1}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={defaults}
/>
);
}
@@ -1,291 +0,0 @@
import React, { useState } from "react";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { Label } from "@/control/Label";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { ControlGrid } from "@/control/ControlGrid";
export function Extruder3SettingsPage() {
const {
state,
defaultState,
setInverterRotationDirection,
resetInverter,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
return (
<Page>
<ControlCard className="bg-red" title="Inverter Settings">
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.rotation_state.forward}
optionTrue={{ children: "Forward" }}
optionFalse={{ children: "Backward" }}
onChange={setInverterRotationDirection}
/>
</Label>
<Label label="Reset Inverter">
<button
onClick={resetInverter}
className="inline-block w-fit max-w-max rounded bg-red-600 px-4 py-4 text-base whitespace-nowrap text-white hover:bg-red-700"
style={{ minWidth: "auto", width: "fit-content" }}
>
Reset Inverter
</button>
</Label>
</ControlCard>
<ControlCard className="bg-red" title="Extruder Settings">
<Label label="Nozzle Pressure Limit">
<EditValue
value={state?.extruder_settings_state.pressure_limit}
defaultValue={defaultState?.extruder_settings_state.pressure_limit}
unit="bar"
title="Nozzle Pressure Limit"
min={0}
max={350}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setExtruderPressureLimit}
/>
</Label>
<Label label="Nozzle Pressure Limit Enabled">
<SelectionGroupBoolean
value={state?.extruder_settings_state.pressure_limit_enabled}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
optionTrue={{ children: "Show" }}
optionFalse={{ children: "Hide" }}
onChange={setShowAdvanced}
/>
</Label>
</ControlCard>
{showAdvanced && (
<>
<ControlCard title="Pressure PID Settings ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.pressure.kp}
defaultValue={defaultState?.pid_settings.pressure.kp}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKp}
title="Pressure PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.pressure.ki}
defaultValue={defaultState?.pid_settings.pressure.ki}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKi}
title="Pressure PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.pressure.kd}
defaultValue={defaultState?.pid_settings.pressure.kd}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKd}
title="Pressure PID KD"
/>
</Label>
</ControlCard>
<ControlGrid>
<ControlCard title="Temperature PID Settings (Front) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.front.kp}
defaultValue={defaultState?.pid_settings.temperature.front.kp}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.front.ki}
defaultValue={defaultState?.pid_settings.temperature.front.ki}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.front.kd}
defaultValue={defaultState?.pid_settings.temperature.front.kd}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Middle) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.middle.kp}
defaultValue={
defaultState?.pid_settings.temperature.middle.kp
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.middle.ki}
defaultValue={
defaultState?.pid_settings.temperature.middle.ki
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.middle.kd}
defaultValue={
defaultState?.pid_settings.temperature.middle.kd
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Back) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.back.kp}
defaultValue={defaultState?.pid_settings.temperature.back.kp}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.back.ki}
defaultValue={defaultState?.pid_settings.temperature.back.ki}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.back.kd}
defaultValue={defaultState?.pid_settings.temperature.back.kd}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Nozzle) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.nozzle.kp}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.kp
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.nozzle.ki}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.ki
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.nozzle.kd}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.kd
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
</ControlGrid>
</>
)}
</Page>
);
}
@@ -1,433 +0,0 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
handleUnhandledEventError,
} from "../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
import { useMemo } from "react";
// ========== Event Schema Definitions ==========
/**
* Machine operation mode enum
*/
export const modeSchema = z.enum(["Standby", "Heat", "Extrude"]);
export type Mode = z.infer<typeof modeSchema>;
export const liveMotorStatusDataSchema = z.object({
screw_rpm: z.number(),
frequency: z.number(),
current: z.number(),
power: z.number(),
});
export type MotorStatus = z.infer<typeof liveMotorStatusDataSchema>;
/**
* Consolidated live values event schema (30FPS data)
*/
export const liveValuesEventDataSchema = z.object({
motor_status: liveMotorStatusDataSchema,
pressure: z.number(),
nozzle_temperature: z.number(),
front_temperature: z.number(),
back_temperature: z.number(),
middle_temperature: z.number(),
nozzle_power: z.number(),
front_power: z.number(),
back_power: z.number(),
middle_power: z.number(),
combined_power: z.number(),
total_energy_kwh: z.number(),
});
/**
* Rotation state schema
*/
export const rotationStateSchema = z.object({
forward: z.boolean(),
});
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
});
/**
* Regulation state schema
*/
export const regulationStateSchema = z.object({
uses_rpm: z.boolean(),
});
/**
* Pressure state schema
*/
export const pressureStateSchema = z.object({
target_bar: z.number(),
wiring_error: z.boolean(),
});
/**
* Screw state schema
*/
export const screwStateSchema = z.object({
target_rpm: z.number(),
});
/**
* Heating state schema
*/
export const heatingStateSchema = z.object({
target_temperature: z.number(),
wiring_error: z.boolean(),
});
export type HeatingState = z.infer<typeof heatingStateSchema>;
/**
* Heating states schema
*/
export const heatingStatesSchema = z.object({
nozzle: heatingStateSchema,
front: heatingStateSchema,
back: heatingStateSchema,
middle: heatingStateSchema,
});
/**
* Extruder settings state schema
*/
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
});
/**
* Inverter status state schema
*/
export const inverterStatusStateSchema = z.object({
running: z.boolean(),
forward_running: z.boolean(),
reverse_running: z.boolean(),
up_to_frequency: z.boolean(),
overload_warning: z.boolean(),
no_function: z.boolean(),
output_frequency_detection: z.boolean(),
abc_fault: z.boolean(),
fault_occurence: z.boolean(),
});
/**
* PID settings schema
*/
export const pidSettingsSchema = z.object({
temperature: z.object({
front: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
middle: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
back: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
nozzle: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
pressure: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
}),
});
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
rotation_state: rotationStateSchema,
mode_state: modeStateSchema,
regulation_state: regulationStateSchema,
pressure_state: pressureStateSchema,
screw_state: screwStateSchema,
heating_states: heatingStatesSchema,
extruder_settings_state: extruderSettingsStateSchema,
inverter_status_state: inverterStatusStateSchema,
pid_settings: pidSettingsSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
// ========== Type Inferences ==========
export type StateEvent = z.infer<typeof stateEventSchema>;
// Additional exports for backward compatibility
export const SetRegulationSchema = z.object({
uses_rpm: z.boolean(),
});
export const mode = z.object({
mode: modeSchema,
});
export type Extruder3NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
motorFrequency: TimeSeries;
motorCurrent: TimeSeries;
motorScrewRpm: TimeSeries;
motorPower: TimeSeries;
pressure: TimeSeries;
nozzleTemperature: TimeSeries;
frontTemperature: TimeSeries;
backTemperature: TimeSeries;
middleTemperature: TimeSeries;
nozzlePower: TimeSeries;
frontPower: TimeSeries;
middlePower: TimeSeries;
backPower: TimeSeries;
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
};
// Constants for time durations
const TWENTY_MILLISECOND = 20;
const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: frontPower, insert: addFrontPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: middlePower, insert: addMiddlePower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (
updater: (state: Extruder3NamespaceStore) => Extruder3NamespaceStore,
) => {
throttledUpdater.updateWith(updater);
};
try {
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
motorScrewRpm: addMotorScrewRpm(state.motorScrewRpm, {
value: liveValuesEvent.data.motor_status.screw_rpm,
timestamp,
}),
motorCurrent: addMotorCurrent(state.motorCurrent, {
value: liveValuesEvent.data.motor_status.current,
timestamp,
}),
motorFrequency: addMotorFrequency(state.motorFrequency, {
value: liveValuesEvent.data.motor_status.frequency,
timestamp,
}),
motorPower: addMotorPower(state.motorPower, {
value: liveValuesEvent.data.motor_status.power,
timestamp,
}),
pressure: addPressure(state.pressure, {
value: liveValuesEvent.data.pressure,
timestamp,
}),
nozzleTemperature: addNozzleTemperature(state.nozzleTemperature, {
value: liveValuesEvent.data.nozzle_temperature,
timestamp,
}),
frontTemperature: addFrontTemperature(state.frontTemperature, {
value: liveValuesEvent.data.front_temperature,
timestamp,
}),
backTemperature: addBackTemperature(state.backTemperature, {
value: liveValuesEvent.data.back_temperature,
timestamp,
}),
middleTemperature: addMiddleTemperature(state.middleTemperature, {
value: liveValuesEvent.data.middle_temperature,
timestamp,
}),
nozzlePower: addNozzlePower(state.nozzlePower, {
value: liveValuesEvent.data.nozzle_power,
timestamp,
}),
frontPower: addFrontPower(state.frontPower, {
value: liveValuesEvent.data.front_power,
timestamp,
}),
middlePower: addMiddlePower(state.middlePower, {
value: liveValuesEvent.data.middle_power,
timestamp,
}),
backPower: addBackPower(state.backPower, {
value: liveValuesEvent.data.back_power,
timestamp,
}),
combinedPower: addCombinedPower(state.combinedPower, {
value: liveValuesEvent.data.combined_power,
timestamp,
}),
totalEnergyKWh: addTotalEnergyKWh(state.totalEnergyKWh, {
value: liveValuesEvent.data.total_energy_kwh,
timestamp,
}),
}));
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
console.error(`Unexpected error processing ${eventName} event:`, error);
throw error;
}
};
}
export const createExtruder3NamespaceStore =
(): StoreApi<Extruder3NamespaceStore> =>
create<Extruder3NamespaceStore>(() => {
return {
state: null,
defaultState: null,
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
backPower,
middlePower,
combinedPower,
totalEnergyKWh,
};
});
const useExtruder3NamespaceImplementation =
createNamespaceHookImplementation<Extruder3NamespaceStore>({
createStore: createExtruder3NamespaceStore,
createEventHandler: extruder3MessageHandler,
});
export function useExtruder3Namespace(
machine_identification_unique: MachineIdentificationUnique,
): Extruder3NamespaceStore {
// Generate namespace ID from validated machine ID
const namespaceId = useMemo<NamespaceId>(
() => ({
type: "machine",
machine_identification_unique,
}),
[machine_identification_unique],
);
// Use the implementation with validated namespace ID
return useExtruder3NamespaceImplementation(namespaceId);
}
@@ -1,448 +0,0 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { MachineIdentificationUnique } from "@/machines/types";
import { extruder3 } from "@/machines/properties";
import { extruder3Route } from "@/routes/routes";
import { z } from "zod";
import { StateEvent, Mode, useExtruder3Namespace } from "./extruder3Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
export function useExtruder3() {
const { serial: serialString } = extruder3Route.useParams();
// Memoize the machine identification to keep it stable between renders
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: extruder3.machine_identification,
serial,
};
}, [serialString]);
// Get consolidated state and live values from namespace
const {
state,
defaultState,
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
};
// Action functions with verb-first names
const setInverterRotationDirection = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.rotation_state.forward = forward;
},
() =>
requestInverterRotationDirection({
machine_identification_unique: machineIdentification,
data: { SetInverterRotationDirection: forward },
}),
);
};
const setExtruderMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
},
() =>
requestExtruderMode({
machine_identification_unique: machineIdentification,
data: { SetExtruderMode: mode },
}),
);
};
const setInverterRegulation = (usesRpm: boolean) => {
updateStateOptimistically(
(current) => {
current.data.regulation_state.uses_rpm = usesRpm;
},
() =>
requestInverterRegulation({
machine_identification_unique: machineIdentification,
data: { SetInverterRegulation: usesRpm },
}),
);
};
const setInverterTargetRpm = (rpm: number) => {
updateStateOptimistically(
(current) => {
current.data.screw_state.target_rpm = rpm;
},
() =>
requestInverterTargetRpm({
machine_identification_unique: machineIdentification,
data: { SetInverterTargetRpm: rpm },
}),
);
};
const setInverterTargetPressure = (pressure: number) => {
updateStateOptimistically(
(current) => {
current.data.pressure_state.target_bar = pressure;
},
() =>
requestInverterTargetPressure({
machine_identification_unique: machineIdentification,
data: { SetInverterTargetPressure: pressure },
}),
);
};
const setNozzleHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.nozzle.target_temperature = temperature;
},
() =>
requestNozzleHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetNozzleHeatingTemperature: temperature },
}),
);
};
const setFrontHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.front.target_temperature = temperature;
},
() =>
requestFrontHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetFrontHeatingTargetTemperature: temperature },
}),
);
};
const setBackHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.back.target_temperature = temperature;
},
() =>
requestBackHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetBackHeatingTargetTemperature: temperature },
}),
);
};
const setMiddleHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.middle.target_temperature = temperature;
},
() =>
requestMiddleHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetMiddleHeatingTemperature: temperature },
}),
);
};
const setExtruderPressureLimit = (pressureLimit: number) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.pressure_limit = pressureLimit;
},
() =>
requestExtruderPressureLimit({
machine_identification_unique: machineIdentification,
data: { SetExtruderPressureLimit: pressureLimit },
}),
);
};
const setExtruderPressureLimitEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.pressure_limit_enabled = enabled;
},
() =>
requestExtruderPressureLimitEnabled({
machine_identification_unique: machineIdentification,
data: { SetExtruderPressureLimitIsEnabled: enabled },
}),
);
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.kp = kp;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.kp = kp;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setPressurePidKi = (ki: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.ki = ki;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.ki = ki;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setPressurePidKd = (kd: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.kd = kd;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.kd = kd;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setTemperaturePidValue = (
zone: "front" | "middle" | "back" | "nozzle",
key: "kp" | "ki" | "kd",
value: number,
) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.temperature[zone][key] = value;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.temperature[zone],
(draft) => {
draft[key] = value;
},
);
requestTemperaturePidSettings({
machine_identification_unique: machineIdentification,
data: { SetTemperaturePidSettings: settings },
});
}
},
);
};
const resetInverter = () => {
// No optimistic update needed for reset
requestResetInverter({
machine_identification_unique: machineIdentification,
data: { ResetInverter: true },
});
};
// Mutation hooks
const { request: requestInverterRotationDirection } = useMachineMutation(
z.object({ SetInverterRotationDirection: z.boolean() }),
);
const { request: requestExtruderMode } = useMachineMutation(
z.object({ SetExtruderMode: z.enum(["Heat", "Extrude", "Standby"]) }),
);
const { request: requestInverterRegulation } = useMachineMutation(
z.object({ SetInverterRegulation: z.boolean() }),
);
const { request: requestInverterTargetRpm } = useMachineMutation(
z.object({ SetInverterTargetRpm: z.number() }),
);
const { request: requestInverterTargetPressure } = useMachineMutation(
z.object({ SetInverterTargetPressure: z.number() }),
);
const { request: requestNozzleHeatingTemperature } = useMachineMutation(
z.object({ SetNozzleHeatingTemperature: z.number() }),
);
const { request: requestFrontHeatingTemperature } = useMachineMutation(
z.object({ SetFrontHeatingTargetTemperature: z.number() }),
);
const { request: requestBackHeatingTemperature } = useMachineMutation(
z.object({ SetBackHeatingTargetTemperature: z.number() }),
);
const { request: requestMiddleHeatingTemperature } = useMachineMutation(
z.object({ SetMiddleHeatingTemperature: z.number() }),
);
const { request: requestExtruderPressureLimit } = useMachineMutation(
z.object({ SetExtruderPressureLimit: z.number() }),
);
const { request: requestExtruderPressureLimitEnabled } = useMachineMutation(
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
}),
}),
);
const { request: requestTemperaturePidSettings } = useMachineMutation(
z.object({
SetTemperaturePidSettings: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
);
const { request: requestResetInverter } = useMachineMutation(
z.object({ ResetInverter: z.boolean() }),
);
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
// Action functions (verb-first)
setInverterRotationDirection,
setExtruderMode,
setInverterRegulation,
setInverterTargetRpm,
setInverterTargetPressure,
setNozzleHeatingTemperature,
setFrontHeatingTemperature,
setBackHeatingTemperature,
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
resetInverter,
};
}
-124
View File
@@ -103,94 +103,6 @@ export const winder2: MachineProperties = {
],
};
export const extruder3: MachineProperties = {
name: "Extruder V3",
version: "V3",
slug: "extruder3",
icon: "qi:Extruder",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0016,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "Inverter Interface",
allowed_devices: [
{
vendor_id: 2,
product_id: 394604626,
revision: 1376256,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x140000,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x160000,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x100000,
},
],
},
{
role: 2,
role_label: "Heating Elements",
allowed_devices: [
{
vendor_id: 2,
product_id: 131346514,
revision: 1179648,
},
],
},
{
role: 3,
role_label: "Pressure Sensor",
allowed_devices: [
{
vendor_id: 2,
product_id: 197996626,
revision: 1310720,
},
],
},
{
role: 4,
role_label: "Thermometers",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xc843052,
revision: 1441792,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x150000,
},
],
},
],
};
export const extruder2: MachineProperties = {
name: "Extruder",
version: "V2",
@@ -427,50 +339,14 @@ export const aquapath1: MachineProperties = {
},
],
};
export const testmachine: MachineProperties = {
name: "TestMachine",
version: "V1",
slug: "testmachine",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0033,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "EL2004",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x7d43052,
revision: 0x120000,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder3,
extruder2,
laser1,
mock1,
buffer1,
aquapath1,
testmachine,
];
export const getMachineProperties = (
@@ -1,62 +0,0 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroup } from "@/control/SelectionGroup";
import { Label } from "@/control/Label";
import { useTestMachine } from "./useTestMachine";
export function TestMachineControlPage() {
const { state, setLed, setAllLeds } = useTestMachine();
const safeState = state ?? { led_on: [false, false, false, false] };
return (
<Page>
<ControlGrid columns={2}>
{/* LED Controls */}
<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}`}>
<SelectionGroup<"On" | "Off">
value={led ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => setLed(index, value === "On")}
/>
</Label>
))}
</div>
</ControlCard>
{/* Master LED Control */}
<ControlCard title="Master LED Control">
<SelectionGroup<"On" | "Off">
value={safeState.led_on.every(Boolean) ? "On" : "Off"}
orientation="horizontal"
options={{
Off: { children: "Turn All Off" },
On: { children: "Turn All On" },
}}
onChange={(value) => setAllLeds(value === "On")}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,20 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { testMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMachinePage() {
const { serial } = testMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -1,76 +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 TestMachineNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineNamespaceStore =
(): StoreApi<TestMachineNamespaceStore> =>
create<TestMachineNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMachineMessageHandler(
store: StoreApi<TestMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (state: TestMachineNamespaceStore) => TestMachineNamespaceStore,
) => 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 useTestMachineNamespaceImplementation =
createNamespaceHookImplementation<TestMachineNamespaceStore>({
createStore: createTestMachineNamespaceStore,
createEventHandler: testMachineMessageHandler,
});
export function useTestMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMachineNamespaceImplementation(namespaceId);
}
@@ -1,95 +0,0 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMachineSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMachineNamespace, StateEvent } from "./testMachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { testmachine } from "@/machines/properties";
import { z } from "zod";
export function useTestMachine() {
const { serial: serialString } = testMachineSerialRoute.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: testmachine.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMachineNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setLed = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on[index] = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetLed", value: { index, on } },
}),
);
};
const setAllLeds = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on = [on, on, on, on];
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAllLeds", value: { on } },
}),
);
};
return {
state: stateOptimistic.value,
setLed,
setAllLeds,
};
}
+10 -83
View File
@@ -1,62 +1,46 @@
import { createRoute, Outlet } from "@tanstack/react-router";
import { RootRoute } from "./__root";
import React from "react";
import { z } from "zod";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { SidebarLayout } from "@/components/SidebarLayout";
import { SetupPage } from "@/setup/SetupPage";
import { EthercatPage } from "@/setup/EthercatPage";
import { MachinesPage } from "@/setup/MachinesPage";
import { ChangelogPage } from "@/setup/ChangelogPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2Page } from "@/machines/winder/winder2/Winder2Page";
import { Winder2ControlPage } from "@/machines/winder/winder2/Winder2ControlPage";
import { Winder2ManualPage } from "@/machines/winder/winder2/Winder2Manual";
import { Winder2SettingPage } from "@/machines/winder/winder2/Winder2Settings";
import { Winder2GraphsPage } from "@/machines/winder/winder2/Winder2Graphs";
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Extruder2Page } from "@/machines/extruder/extruder2/Extruder2Page";
import { Extruder2ControlPage } from "@/machines/extruder/extruder2/Extruder2ControlPage";
import { Extruder2SettingsPage } from "@/machines/extruder/extruder2/Extruder2Settings";
import { ExtruderV2ManualPage } from "@/machines/extruder/extruder2/Extruder2Manual";
import { Extruder2GraphsPage } from "@/machines/extruder/extruder2/Extruder2Graph";
import { Extruder2PresetsPage } from "@/machines/extruder/extruder2/Extruder2PresetsPage";
import { Extruder3Page } from "@/machines/extruder/extruder3/Extruder3Page";
import { Extruder3ControlPage } from "@/machines/extruder/extruder3/Extruder3ControlPage";
import { Extruder3SettingsPage } from "@/machines/extruder/extruder3/Extruder3Settings";
import { ExtruderV3ManualPage } from "@/machines/extruder/extruder3/Extruder3Manual";
import { Extruder3GraphsPage } from "@/machines/extruder/extruder3/Extruder3Graph";
import { Extruder3PresetsPage } from "@/machines/extruder/extruder3/Extruder3PresetsPage";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { Buffer1ControlPage } from "@/machines/buffer/buffer1/Buffer1ControlPage";
import { Buffer1Page } from "@/machines/buffer/buffer1/Buffer1Page";
import { Buffer1SettingsPage } from "@/machines/buffer/buffer1/Buffer1Settings";
import { ChangelogPage } from "@/setup/ChangelogPage";
import { Extruder2GraphsPage } from "@/machines/extruder/extruder2/Extruder2Graph";
import { Extruder2PresetsPage } from "@/machines/extruder/extruder2/Extruder2PresetsPage";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { Laser1ControlPage } from "@/machines/laser/laser1/Laser1ControlPage";
import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { z } from "zod";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
// _mainNavigation/configuration/a
@@ -64,18 +48,6 @@ import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
// the mainNavigation has a custom layout
// the winder2 winder2 and configuration also have a custom layout
// the leaf routes are just pages
export const testMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmachine/$serial",
component: () => <TestMachinePage />,
});
// Leaf route: control page
export const testMachineControlRoute = createRoute({
getParentRoute: () => testMachineSerialRoute,
path: "control",
component: () => <TestMachineControlPage />,
});
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
@@ -124,42 +96,6 @@ export const extruder2PresetsRoute = createRoute({
component: () => <Extruder2PresetsPage />,
});
export const extruder3Route = createRoute({
getParentRoute: () => machinesRoute,
path: "extruder3/$serial",
component: () => <Extruder3Page />,
});
export const extruder3ControlRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "control",
component: () => <Extruder3ControlPage />,
});
export const extruder3SettingsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "settings",
component: () => <Extruder3SettingsPage />,
});
export const extruder3ManualRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "manual",
component: () => <ExtruderV3ManualPage />,
});
export const extruder3GraphsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "graphs",
component: () => <Extruder3GraphsPage />,
});
export const extruder3PresetsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "presets",
component: () => <Extruder3PresetsPage />,
});
export const winder2SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "winder2/$serial",
@@ -377,7 +313,6 @@ export const rootTree = RootRoute.addChildren([
laser1GraphsRoute,
laser1PresetsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
@@ -400,14 +335,6 @@ export const rootTree = RootRoute.addChildren([
extruder2PresetsRoute,
]),
extruder3Route.addChildren([
extruder3ControlRoute,
extruder3SettingsRoute,
extruder3ManualRoute,
extruder3GraphsRoute,
extruder3PresetsRoute,
]),
mock1SerialRoute.addChildren([
mock1ControlRoute,
mock1GraphRoute,
+2 -2
View File
@@ -45,12 +45,12 @@ impl MachineAct for AquaPathV1 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Machine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Machine do nothing*/
{
()
}
+2 -2
View File
@@ -33,12 +33,12 @@ impl MachineAct for BufferV1 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Macine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Machine do nothing*/
{
()
}
+1 -1
View File
@@ -63,7 +63,7 @@ impl MachineAct for ExtruderV2 {
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Machine do nothing*/
{
()
}
+4 -10
View File
@@ -251,11 +251,8 @@ impl ExtruderV2 {
pub fn set_regulation(&mut self, uses_rpm: bool) {
if !self.screw_speed_controller.get_uses_rpm() && uses_rpm {
self.screw_speed_controller.set_target_screw_rpm(
self.screw_speed_controller.target_rpm,
AngularVelocity::new::<revolution_per_minute>(1500.0),
4,
);
self.screw_speed_controller
.set_target_screw_rpm(self.screw_speed_controller.target_rpm);
self.screw_speed_controller.set_uses_rpm(uses_rpm);
}
@@ -273,11 +270,8 @@ impl ExtruderV2 {
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_speed_controller.set_target_screw_rpm(
AngularVelocity::new::<revolution_per_minute>(rpm),
AngularVelocity::new::<revolution_per_minute>(1500.0),
4,
);
self.screw_speed_controller
.set_target_screw_rpm(AngularVelocity::new::<revolution_per_minute>(rpm));
self.emit_state();
}
@@ -443,6 +443,7 @@ impl MitsubishiCS80 {
pub fn set_frequency_target(&mut self, frequency: Frequency) {
let mut request: MitsubishiCS80Request =
MitsubishiCS80Requests::WriteRunningFrequency.into();
let result = self.convert_frequency_to_word(frequency);
let bytes = result.to_le_bytes();
request.request.data[2] = bytes[1];
+2 -9
View File
@@ -85,8 +85,6 @@ impl MachineNewTrait for ExtruderV2 {
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// Role 0 - Buscoupler EK1100
use control_core::transmission::fixed::FixedTransmission;
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
@@ -216,13 +214,8 @@ impl MachineNewTrait for ExtruderV2 {
let target_pressure = Pressure::new::<bar>(0.0);
let target_rpm = AngularVelocity::new::<revolution_per_minute>(0.0);
let screw_speed_controller = ScrewSpeedController::new(
inverter,
target_pressure,
target_rpm,
pressure_sensor,
FixedTransmission::new(1.0 / 34.0),
);
let screw_speed_controller =
ScrewSpeedController::new(inverter, target_pressure, target_rpm, pressure_sensor);
let (sender, receiver) = smol::channel::unbounded();
let mut extruder: ExtruderV2 = Self {
@@ -41,7 +41,6 @@ impl ScrewSpeedController {
target_pressure: Pressure,
target_rpm: AngularVelocity,
pressure_sensor: AnalogInput,
transmission: FixedTransmission,
) -> Self {
let now = Instant::now();
Self {
@@ -54,8 +53,7 @@ impl ScrewSpeedController {
pressure_sensor,
uses_rpm: true,
forward_rotation: true,
transmission: transmission,
//FixedTransmission::new(1.0 / 34.0),
transmission: FixedTransmission::new(1.0 / 34.0),
motor_on: false,
nozzle_pressure_limit: Pressure::new::<bar>(100.0),
nozzle_pressure_limit_enabled: true,
@@ -109,22 +107,15 @@ impl ScrewSpeedController {
self.target_pressure = target_pressure;
}
pub fn set_target_screw_rpm(
&mut self,
target_rpm: AngularVelocity,
_motor_rpm_rating: AngularVelocity,
motor_poles: usize,
) {
pub fn set_target_screw_rpm(&mut self, target_rpm: AngularVelocity) {
// Use uom here and perhaps clamp it
let target_motor_rpm = self
.transmission
.calculate_angular_velocity_input(target_rpm);
self.target_rpm = target_rpm;
let target_frequency: Frequency = Frequency::new::<hertz>(
target_motor_rpm.get::<revolution_per_minute>() as f64 / 120.0 * motor_poles as f64,
);
let target_frequency =
Frequency::new::<cycle_per_minute>(target_motor_rpm.get::<revolution_per_minute>());
self.inverter.set_frequency_target(target_frequency);
}
@@ -215,7 +206,14 @@ impl ScrewSpeedController {
pub fn update(&mut self, now: Instant, is_extruding: bool) {
// TODO: move this logic elsewhere or make non async
smol::block_on(self.inverter.act(now));
let wiring_error = self.get_wiring_error();
if wiring_error {
// emit in act
}
let measured_pressure = self.get_pressure();
if !self.uses_rpm && !is_extruding && self.motor_on {
let frequency = Frequency::new::<hertz>(0.0);
self.inverter.set_frequency_target(frequency);
-76
View File
@@ -1,76 +0,0 @@
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineAct, MachineMessage};
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for ExtruderV3 {
fn act(&mut self, now: Instant) {
use std::time::Duration;
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
self.temperature_controller_back.update(now);
self.temperature_controller_nozzle.update(now);
self.temperature_controller_front.update(now);
self.temperature_controller_middle.update(now);
if self.mode == super::ExtruderV3Mode::Extrude {
self.screw_speed_controller.update(now, true);
} else {
self.screw_speed_controller.update(now, false);
}
if self.mode == super::ExtruderV3Mode::Standby {
self.turn_heating_off();
}
if self.mode == super::ExtruderV3Mode::Extrude
&& !self.screw_speed_controller.get_motor_enabled()
{
self.switch_to_heat();
}
let now = Instant::now();
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
}
}
}
}
-240
View File
@@ -1,240 +0,0 @@
use std::sync::Arc;
use crate::extruder1::{
api::{
ExtruderSettingsState, HeatingStates, InverterStatusState, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
},
mitsubishi_cs80::MotorStatus,
};
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use serde_json::Value;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use tracing::instrument;
use units::{
angular_velocity::revolution_per_minute, electric_current::ampere, electric_potential::volt,
frequency::hertz,
};
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
use super::ExtruderV3Mode;
#[derive(Debug, Clone, Default, Serialize)]
pub struct MotorStatusValues {
pub screw_rpm: f64, // rpm of motor
pub frequency: f64, // frequency of motor
pub voltage: f64, // volt used for motor
pub current: f64, // current used for the motor
pub power: f64, // power in watts
}
impl From<MotorStatus> for MotorStatusValues {
fn from(status: MotorStatus) -> Self {
let voltage = status.voltage.get::<volt>();
let current = status.current.get::<ampere>();
Self {
screw_rpm: status.rpm.get::<revolution_per_minute>(),
frequency: status.frequency.get::<hertz>(),
voltage,
current,
power: voltage * current,
}
}
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
/// screw rpm
pub motor_status: MotorStatusValues,
/// pressure in bar
pub pressure: f64,
/// nozzle temperature in celsius
pub nozzle_temperature: f64,
/// front temperature in celsius
pub front_temperature: f64,
/// back temperature in celsius
pub back_temperature: f64,
/// middle temperature in celsius
pub middle_temperature: f64,
/// nozzle heating power in watts
pub nozzle_power: f64,
/// front heating power in watts
pub front_power: f64,
/// back heating power in watts
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
/// total energy consumption in kWh
pub total_energy_kwh: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, PartialEq, BuildEvent)]
pub struct StateEvent {
pub is_default_state: bool,
/// rotation state
pub rotation_state: RotationState,
/// mode state
pub mode_state: ModeState,
/// regulation state
pub regulation_state: RegulationState,
/// pressure state
pub pressure_state: PressureState,
/// screw state
pub screw_state: ScrewState,
/// heating states
pub heating_states: HeatingStates,
/// extruder settings state
pub extruder_settings_state: ExtruderSettingsState,
/// inverter status state
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ModeState {
pub mode: ExtruderV3Mode,
}
pub enum ExtruderV3Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
#[derive(Deserialize, Serialize)]
pub enum Mutation {
/// INVERTER
/// Frequency Control
// Set Rotation also starts the motor
SetInverterRotationDirection(bool),
SetInverterTargetPressure(f64),
SetInverterTargetRpm(f64),
SetInverterRegulation(bool),
//Mode
SetExtruderMode(ExtruderV3Mode),
SetFrontHeatingTargetTemperature(f64),
SetBackHeatingTargetTemperature(f64),
SetMiddleHeatingTemperature(f64),
SetNozzleHeatingTemperature(f64),
// SetPressure
SetExtruderPressureLimit(f64),
SetExtruderPressureLimitIsEnabled(bool),
// Pid Configure
SetPressurePidSettings(PidSettings),
SetTemperaturePidSettings(TemperaturePid),
// Reset
ResetInverter(bool),
}
#[derive(Debug)]
pub struct ExtruderV3Namespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<ExtruderV3Events> for ExtruderV3Namespace {
#[instrument(skip_all)]
fn emit(&mut self, events: ExtruderV3Events) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
impl CacheableEvents<Self> for ExtruderV3Events {
fn event_value(&self) -> GenericEvent {
match self {
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for ExtruderV3 {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
use crate::extruder1::HeatingType;
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetExtruderMode(mode) => self.set_mode_state(mode),
Mutation::SetInverterRotationDirection(forward) => self.set_rotation_state(forward),
Mutation::SetInverterRegulation(uses_rpm) => self.set_regulation(uses_rpm),
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
Mutation::SetInverterTargetRpm(rpm) => self.set_target_rpm(rpm),
Mutation::ResetInverter(_) => self.reset_inverter(),
Mutation::SetFrontHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Front)
}
Mutation::SetMiddleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Middle)
}
Mutation::SetBackHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
}
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
-355
View File
@@ -1,355 +0,0 @@
#[cfg(not(feature = "mock-machine"))]
// Contains Implementations for All functions that use emit_state
use crate::extruder1::{
HeatingType,
api::{
ExtruderSettingsState, HeatingState, HeatingStates, InverterStatusState, PidSettings,
PidSettingsStates, PressureState, RegulationState, RotationState, ScrewState,
TemperaturePid,
},
};
#[cfg(not(feature = "mock-machine"))]
use control_core::helpers::hasher_serializer::hash_with_serde_model;
#[cfg(not(feature = "mock-machine"))]
use control_core::socketio::event::BuildEvent;
#[cfg(not(feature = "mock-machine"))]
use control_core::socketio::namespace::NamespaceCacheingLogic;
#[cfg(not(feature = "mock-machine"))]
use units::angular_velocity::AngularVelocity;
#[cfg(not(feature = "mock-machine"))]
use units::pressure::{Pressure, bar};
#[cfg(not(feature = "mock-machine"))]
use units::thermodynamic_temperature::ThermodynamicTemperature;
#[cfg(not(feature = "mock-machine"))]
use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use super::{ExtruderV3, ExtruderV3Mode, api::StateEvent};
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
pub fn build_state_event(&mut self) -> StateEvent {
use crate::{
extruder1::api::{TemperaturePid, TemperaturePidStates},
extruder2::api::ModeState,
};
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: RotationState {
forward: self.screw_speed_controller.get_rotation_direction(),
},
mode_state: ModeState {
mode: self.mode.clone(),
},
regulation_state: RegulationState {
uses_rpm: self.screw_speed_controller.get_uses_rpm(),
},
pressure_state: PressureState {
target_bar: self
.screw_speed_controller
.get_target_pressure()
.get::<bar>(),
wiring_error: self.screw_speed_controller.get_wiring_error(),
},
screw_state: ScrewState {
target_rpm: self
.screw_speed_controller
.get_target_rpm()
.get::<revolution_per_minute>(),
},
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: self
.temperature_controller_nozzle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_nozzle.heating.wiring_error,
},
front: HeatingState {
target_temperature: self
.temperature_controller_front
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_front.heating.wiring_error,
},
back: HeatingState {
target_temperature: self
.temperature_controller_back
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_back.heating.wiring_error,
},
middle: HeatingState {
target_temperature: self
.temperature_controller_middle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_middle.heating.wiring_error,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: self
.screw_speed_controller
.get_nozzle_pressure_limit()
.get::<bar>(),
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
forward_running: self.screw_speed_controller.inverter.status.forward_running,
reverse_running: self.screw_speed_controller.inverter.status.reverse_running,
up_to_frequency: self.screw_speed_controller.inverter.status.su,
overload_warning: self.screw_speed_controller.inverter.status.ol,
no_function: self.screw_speed_controller.inverter.status.no_function,
output_frequency_detection: self.screw_speed_controller.inverter.status.fu,
abc_fault: self.screw_speed_controller.inverter.status.abc_,
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
},
pid_settings: PidSettingsStates {
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: self.temperature_controller_front.pid.get_ki(),
kp: self.temperature_controller_front.pid.get_kp(),
kd: self.temperature_controller_front.pid.get_kd(),
zone: String::from("front"),
},
middle: TemperaturePid {
ki: self.temperature_controller_middle.pid.get_ki(),
kp: self.temperature_controller_middle.pid.get_kp(),
kd: self.temperature_controller_middle.pid.get_kd(),
zone: String::from("middle"),
},
back: TemperaturePid {
ki: self.temperature_controller_back.pid.get_ki(),
kp: self.temperature_controller_back.pid.get_kp(),
kd: self.temperature_controller_back.pid.get_kd(),
zone: String::from("back"),
},
nozzle: TemperaturePid {
ki: self.temperature_controller_nozzle.pid.get_ki(),
kp: self.temperature_controller_nozzle.pid.get_kp(),
kd: self.temperature_controller_nozzle.pid.get_kd(),
zone: String::from("nozzle"),
},
},
pressure: PidSettings {
ki: self.screw_speed_controller.pid.get_ki(),
kp: self.screw_speed_controller.pid.get_kp(),
kd: self.screw_speed_controller.pid.get_kd(),
},
},
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
pub fn emit_state(&mut self) {
use super::api::ExtruderV3Events;
let state = self.build_state_event();
let hash = hash_with_serde_model(self.screw_speed_controller.get_inverter_status());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV3Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let status = self.screw_speed_controller.get_inverter_status();
let new_status_hash = hash_with_serde_model(status);
if new_status_hash != old_status_hash {
self.emit_state();
}
}
pub fn emit_live_values(&mut self) {
use std::time::Instant;
use crate::extruder2::api::{ExtruderV3Events, LiveValuesEvent};
let now = Instant::now();
let combined_power = self.calculate_combined_power();
self.update_total_energy(combined_power, now);
let live_values = LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
nozzle_temperature: self
.temperature_controller_nozzle
.heating
.temperature
.get::<degree_celsius>(),
front_temperature: self
.temperature_controller_front
.heating
.temperature
.get::<degree_celsius>(),
back_temperature: self
.temperature_controller_back
.heating
.temperature
.get::<degree_celsius>(),
middle_temperature: self
.temperature_controller_middle
.heating
.temperature
.get::<degree_celsius>(),
nozzle_power: self
.temperature_controller_nozzle
.get_heating_element_wattage(),
front_power: self
.temperature_controller_front
.get_heating_element_wattage(),
back_power: self
.temperature_controller_back
.get_heating_element_wattage(),
middle_power: self
.temperature_controller_middle
.get_heating_element_wattage(),
combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
.set_nozzle_pressure_limit_is_enabled(enabled);
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.screw_speed_controller
.set_nozzle_pressure_limit(Pressure::new::<bar>(pressure));
self.emit_state();
}
pub fn enable_heating(&mut self) {
self.temperature_controller_back.allow_heating();
self.temperature_controller_front.allow_heating();
self.temperature_controller_middle.allow_heating();
self.temperature_controller_nozzle.allow_heating();
self.emit_state();
}
pub fn set_rotation_state(&mut self, forward: bool) {
self.screw_speed_controller.set_rotation_direction(forward);
self.emit_state();
}
pub fn set_mode_state(&mut self, mode: ExtruderV3Mode) {
self.switch_mode(mode);
self.emit_state();
}
pub fn set_regulation(&mut self, uses_rpm: bool) {
if !self.screw_speed_controller.get_uses_rpm() && uses_rpm {
self.screw_speed_controller.set_target_screw_rpm(
self.screw_speed_controller.target_rpm,
AngularVelocity::new::<revolution_per_minute>(3000.0),
2,
);
self.screw_speed_controller.set_uses_rpm(uses_rpm);
}
if self.screw_speed_controller.get_uses_rpm() && !uses_rpm {
self.screw_speed_controller.set_uses_rpm(uses_rpm);
self.screw_speed_controller.start_pressure_regulation();
}
self.emit_state();
}
pub fn set_target_pressure(&mut self, pressure: f64) {
self.screw_speed_controller
.set_target_pressure(Pressure::new::<bar>(pressure));
self.emit_state();
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_speed_controller.set_target_screw_rpm(
AngularVelocity::new::<revolution_per_minute>(rpm),
AngularVelocity::new::<revolution_per_minute>(3000.0),
2,
);
self.emit_state();
}
pub fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
let target_temp = ThermodynamicTemperature::new::<degree_celsius>(target_temperature);
match heating_type {
HeatingType::Nozzle => self
.temperature_controller_nozzle
.set_target_temperature(target_temp),
HeatingType::Front => self
.temperature_controller_front
.set_target_temperature(target_temp),
HeatingType::Back => self
.temperature_controller_back
.set_target_temperature(target_temp),
HeatingType::Middle => self
.temperature_controller_middle
.set_target_temperature(target_temp),
}
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.screw_speed_controller
.pid
.configure(settings.ki, settings.kp, settings.kd);
self.emit_state();
}
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
match settings.zone.as_str() {
"front" => {
self.temperature_controller_front.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"middle" => {
self.temperature_controller_middle.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"back" => {
self.temperature_controller_back.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"nozzle" => {
self.temperature_controller_nozzle.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
}
}
-45
View File
@@ -1,45 +0,0 @@
use super::ExtruderV2;
use crate::MachineAct;
use crate::MachineMessage;
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
-57
View File
@@ -1,57 +0,0 @@
use super::ExtruderV2;
use crate::{
MachineApi,
extruder1::{HeatingType, api::Mutation},
};
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetExtruderMode(mode) => self.set_mode_state(mode),
Mutation::SetInverterRotationDirection(forward) => self.set_rotation_state(forward),
Mutation::SetInverterRegulation(uses_rpm) => self.set_regulation(uses_rpm),
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
Mutation::SetInverterTargetRpm(rpm) => self.set_target_rpm(rpm),
Mutation::ResetInverter(_) => (),
Mutation::SetFrontHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Front)
}
Mutation::SetMiddleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Middle)
}
Mutation::SetBackHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
}
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
}
Ok(())
}
fn api_event_namespace(
&mut self,
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
}
-164
View File
@@ -1,164 +0,0 @@
use crate::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
mock::ExtruderV2,
};
use control_core::{
helpers::hasher_serializer::hash_with_serde_model,
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
};
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
// bad performance wise, but doesnt matter its only a mock machine
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: self.rotation_state.clone(),
mode_state: self.mode_state.clone(),
regulation_state: self.regulation_state.clone(),
pressure_state: self.pressure_state.clone(),
screw_state: self.screw_state.clone(),
heating_states: self.heating_states.clone(),
extruder_settings_state: self.extruder_settings_state.clone(),
inverter_status_state: self.inverter_status_state.clone(),
pid_settings: self.pid_settings.clone(),
}
}
}
impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let hash = hash_with_serde_model(self.inverter_status_state.clone());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let status = self.inverter_status_state.clone();
let new_status_hash = hash_with_serde_model(status);
if new_status_hash != old_status_hash {
self.emit_state();
}
}
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
motor_status: self.motor_status.clone(),
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
front_temperature: self.front_temperature,
back_temperature: self.back_temperature,
middle_temperature: self.middle_temperature,
nozzle_power: self.nozzle_power,
front_power: self.front_power,
back_power: self.back_power,
middle_power: self.middle_power,
combined_power: self.combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.extruder_settings_state.pressure_limit_enabled = enabled;
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.extruder_settings_state.pressure_limit = pressure;
self.emit_state();
}
pub fn enable_heating(&mut self) {
self.back_heating_allowed = true;
self.front_heating_allowed = true;
self.nozzle_heating_allowed = true;
self.back_heating_allowed = true;
self.emit_state();
}
pub fn set_rotation_state(&mut self, forward: bool) {
self.rotation_state.forward = forward;
self.emit_state();
}
pub fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.mode_state = ModeState { mode };
self.emit_state();
}
pub fn set_regulation(&mut self, uses_rpm: bool) {
self.regulation_state.uses_rpm = uses_rpm;
self.emit_state();
}
pub fn set_target_pressure(&mut self, pressure: f64) {
self.target_pressure = pressure;
self.emit_state();
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_state.target_rpm = rpm;
self.emit_state();
}
pub fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
match heating_type {
HeatingType::Nozzle => {
self.heating_states.nozzle.target_temperature = target_temperature
}
HeatingType::Front => self.heating_states.front.target_temperature = target_temperature,
HeatingType::Back => self.heating_states.back.target_temperature = target_temperature,
HeatingType::Middle => {
self.heating_states.middle.target_temperature = target_temperature
}
}
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.pid_settings.pressure.ki = settings.ki;
self.pid_settings.pressure.kp = settings.kp;
self.pid_settings.pressure.kd = settings.kd;
self.emit_state();
}
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
match settings.zone.as_str() {
"front" => {
self.pid_settings.temperature.front.ki = settings.ki;
self.pid_settings.temperature.front.kp = settings.kp;
self.pid_settings.temperature.front.kd = settings.kd;
}
"middle" => {
self.pid_settings.temperature.middle.ki = settings.ki;
self.pid_settings.temperature.middle.kp = settings.kp;
self.pid_settings.temperature.middle.kd = settings.kd;
}
"back" => {
self.pid_settings.temperature.back.ki = settings.ki;
self.pid_settings.temperature.back.kp = settings.kp;
self.pid_settings.temperature.back.kd = settings.kd;
}
"nozzle" => {
self.pid_settings.temperature.nozzle.ki = settings.ki;
self.pid_settings.temperature.nozzle.kp = settings.kp;
self.pid_settings.temperature.nozzle.kd = settings.kd;
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
}
}
-129
View File
@@ -1,129 +0,0 @@
#[cfg(feature = "mock-machine")]
use smol::channel::Receiver;
#[cfg(feature = "mock-machine")]
use smol::channel::Sender;
#[cfg(feature = "mock-machine")]
use crate::{AsyncThreadMessage, Machine};
#[cfg(feature = "mock-machine")]
use crate::{
MachineIdentificationUnique, MachineMessage, machine_identification::MachineIdentification,
};
#[cfg(feature = "mock-machine")]
use std::time::Instant;
#[cfg(feature = "mock-machine")]
use crate::{
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingStates, InverterStatusState,
ModeState, MotorStatusValues, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState,
},
},
};
// Just checking mock-machine feature here to exclude these modules from compilation entirely
#[cfg(feature = "mock-machine")]
pub mod act;
#[cfg(feature = "mock-machine")]
pub mod api;
#[cfg(feature = "mock-machine")]
pub mod mock_emit;
#[cfg(feature = "mock-machine")]
pub mod new;
#[cfg(feature = "mock-machine")]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(feature = "mock-machine")]
#[derive(Debug)]
pub struct ExtruderV2 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
/// Energy tracking for total consumption calculation
pub total_energy_kwh: f64,
pub last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
pub target_pressure: f64,
pub is_default_state: bool,
/// rotation state
pub rotation_state: RotationState,
/// mode state
pub mode_state: ModeState,
/// regulation state
pub regulation_state: RegulationState,
/// pressure state
pub pressure_state: PressureState,
/// screw state
pub screw_state: ScrewState,
/// heating states
pub heating_states: HeatingStates,
/// extruder settings state
pub extruder_settings_state: ExtruderSettingsState,
/// inverter status state
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
pub motor_status: MotorStatusValues,
/// pressure in bar
pub pressure: f64,
/// nozzle temperature in celsius
pub nozzle_temperature: f64,
/// front temperature in celsius
pub front_temperature: f64,
/// back temperature in celsius
pub back_temperature: f64,
/// middle temperature in celsius
pub middle_temperature: f64,
/// nozzle heating power in watts
pub nozzle_power: f64,
/// front heating power in watts
pub front_power: f64,
/// back heating power in watts
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
pub nozzle_heating_allowed: bool,
pub front_heating_allowed: bool,
pub back_heating_allowed: bool,
pub middle_heating_allowed: bool,
}
#[cfg(feature = "mock-machine")]
impl ExtruderV2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_EXTRUDER_V1,
};
}
-157
View File
@@ -1,157 +0,0 @@
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
},
mock::ExtruderV2,
},
};
impl MachineNewTrait for ExtruderV2 {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
// Mock machine can work with either Serial or Ethercat hardware
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let (sender, receiver) = smol::channel::unbounded();
let mut extruder_mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
mode: ExtruderV2Mode::Standby, // Start in standby mode
emitted_default_state: false,
last_status_hash: None,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
is_default_state: false,
rotation_state: RotationState { forward: false },
mode_state: ModeState {
mode: ExtruderV2Mode::Standby,
},
regulation_state: RegulationState { uses_rpm: true },
pressure_state: PressureState {
target_bar: 0.0,
wiring_error: false,
},
screw_state: ScrewState { target_rpm: 0.0 },
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
front: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
back: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
middle: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
},
inverter_status_state: InverterStatusState {
running: false,
forward_running: true,
reverse_running: true,
up_to_frequency: false,
overload_warning: false,
no_function: false,
output_frequency_detection: false,
abc_fault: false,
fault_occurence: false,
},
pid_settings: PidSettingsStates {
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("front"),
},
middle: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("middle"),
},
back: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("back"),
},
nozzle: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("nozzle"),
},
},
pressure: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
},
},
motor_status: MotorStatusValues {
screw_rpm: 0.0,
frequency: 0.0,
voltage: 0.0,
current: 0.0,
power: 0.0,
},
pressure: 0.0,
nozzle_temperature: 0.0,
front_temperature: 0.0,
back_temperature: 0.0,
middle_temperature: 0.0,
nozzle_power: 0.0,
front_power: 0.0,
back_power: 0.0,
middle_power: 0.0,
combined_power: 0.0,
nozzle_heating_allowed: false,
front_heating_allowed: false,
back_heating_allowed: false,
middle_heating_allowed: false,
target_pressure: 0.0,
};
extruder_mock_machine.emit_state();
Ok(extruder_mock_machine)
}
}
-215
View File
@@ -1,215 +0,0 @@
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use api::ExtruderV3Namespace;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Receiver;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use units::electric_current::ampere;
#[cfg(not(feature = "mock-machine"))]
use units::electric_potential::volt;
#[cfg(not(feature = "mock-machine"))]
use crate::MACHINE_EXTRUDER_V2;
use crate::{AsyncThreadMessage, Machine};
#[cfg(not(feature = "mock-machine"))]
use crate::{
MachineMessage, VENDOR_QITECH,
extruder1::{
screw_speed_controller::ScrewSpeedController, temperature_controller::TemperatureController,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
pub mod act;
pub mod api;
pub mod emit;
pub mod mock;
pub mod new;
pub mod temperature_controller;
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub enum ExtruderV3Mode {
Standby,
Heat,
Extrude,
}
pub enum HeatingType {
Nozzle,
Front,
Back,
Middle,
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug)]
pub struct ExtruderV3 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV3Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV3Mode,
screw_speed_controller: ScrewSpeedController,
temperature_controller_front: TemperatureController,
temperature_controller_middle: TemperatureController,
temperature_controller_back: TemperatureController,
temperature_controller_nozzle: TemperatureController,
/// Energy tracking for total consumption calculation
total_energy_kwh: f64,
last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl Machine for ExtruderV3 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for ExtruderV3 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ExtruderV3")
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_EXTRUDER_V2,
};
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
/// Calculate combined power consumption in watts
fn calculate_combined_power(&mut self) -> f64 {
let motor_power = {
let motor_status = &self.screw_speed_controller.inverter.motor_status;
let voltage = motor_status.voltage.get::<volt>();
let current = motor_status.current.get::<ampere>();
voltage * current
};
let nozzle_power = self
.temperature_controller_nozzle
.get_heating_element_wattage();
let front_power = self
.temperature_controller_front
.get_heating_element_wattage();
let back_power = self
.temperature_controller_back
.get_heating_element_wattage();
let middle_power = self
.temperature_controller_middle
.get_heating_element_wattage();
motor_power + nozzle_power + front_power + back_power + middle_power
}
/// Update total energy consumption in kWh
fn update_total_energy(&mut self, current_power_watts: f64, now: Instant) {
if let Some(last_time) = self.last_energy_calculation_time {
let time_delta_hours = now.duration_since(last_time).as_secs_f64() / 3600.0;
let energy_delta_kwh = (current_power_watts / 1000.0) * time_delta_hours;
self.total_energy_kwh += energy_delta_kwh;
}
self.last_energy_calculation_time = Some(now);
}
// Funktionen ohne emit_state bleiben hier
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
fn turn_heating_off(&mut self) {
self.temperature_controller_back.disable();
self.temperature_controller_front.disable();
self.temperature_controller_middle.disable();
self.temperature_controller_nozzle.disable();
}
fn switch_to_standby(&mut self) {
match self.mode {
ExtruderV3Mode::Standby => (),
ExtruderV3Mode::Heat => {
self.turn_heating_off();
self.screw_speed_controller.reset_pid();
}
ExtruderV3Mode::Extrude => {
self.turn_heating_off();
self.screw_speed_controller.turn_motor_off();
self.screw_speed_controller.reset_pid();
}
};
self.mode = ExtruderV3Mode::Standby;
}
fn switch_to_heat(&mut self) {
match self.mode {
ExtruderV3Mode::Standby => self.enable_heating(),
ExtruderV3Mode::Heat => (),
ExtruderV3Mode::Extrude => {
self.screw_speed_controller.turn_motor_off();
self.screw_speed_controller.reset_pid();
}
}
self.mode = ExtruderV3Mode::Heat;
}
fn switch_to_extrude(&mut self) {
match self.mode {
ExtruderV3Mode::Standby => {
self.screw_speed_controller.turn_motor_on();
self.enable_heating();
self.screw_speed_controller.reset_pid();
}
ExtruderV3Mode::Heat => {
self.screw_speed_controller.turn_motor_on();
self.enable_heating();
self.screw_speed_controller.reset_pid();
}
ExtruderV3Mode::Extrude => (),
}
self.mode = ExtruderV3Mode::Extrude;
}
fn switch_mode(&mut self, mode: ExtruderV3Mode) {
if self.mode == mode {
return;
}
match mode {
ExtruderV3Mode::Standby => self.switch_to_standby(),
ExtruderV3Mode::Heat => self.switch_to_heat(),
ExtruderV3Mode::Extrude => self.switch_to_extrude(),
}
}
fn reset_inverter(&mut self) {
self.screw_speed_controller.inverter.reset_inverter();
}
}
-249
View File
@@ -1,249 +0,0 @@
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineNewParams, MachineNewTrait, get_ethercat_device};
#[cfg(not(feature = "mock-machine"))]
use anyhow::Error;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::coe::ConfigurableDevice;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::devices::{
EthercatDeviceUsed,
ek1100::EK1100,
ek1100::EK1100_IDENTITY_A,
el6021::EL6021,
el6021::EL6021Configuration,
el6021::EL6021Port,
el6021::{EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D},
};
#[cfg(not(feature = "mock-machine"))]
use std::time::Duration;
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use units::angular_velocity::AngularVelocity;
#[cfg(not(feature = "mock-machine"))]
use units::angular_velocity::revolution_per_minute;
#[cfg(not(feature = "mock-machine"))]
use units::pressure::Pressure;
#[cfg(not(feature = "mock-machine"))]
use units::pressure::bar;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::{
devices::{
el2004::{EL2004, EL2004_IDENTITY_A, EL2004Port},
el3021::{EL3021, EL3021_IDENTITY_A, EL3021Port},
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
},
io::{
analog_input::AnalogInput, digital_output::DigitalOutput,
serial_interface::SerialInterface, temperature_input::TemperatureInput,
},
};
#[cfg(not(feature = "mock-machine"))]
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use crate::extruder1::temperature_controller::TemperatureController;
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
#[cfg(not(feature = "mock-machine"))]
impl MachineNewTrait for ExtruderV3 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
use crate::{
MachineNewHardware, MachineNewHardwareEthercat, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
let device_identification = params.device_group.to_vec();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware: &&MachineNewHardwareEthercat<'_, '_, '_> = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
// using block_on because making this funciton async creates a lifetime issue
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// Role 0 - Buscoupler EK1100
use control_core::transmission::fixed::FixedTransmission;
use crate::{
extruder1::{
Heating, mitsubishi_cs80::MitsubishiCS80,
screw_speed_controller::ScrewSpeedController,
},
extruder2::{ExtruderV3Mode, api::ExtruderV3Namespace},
};
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
let el6021 = {
let identities = [
EL6021_IDENTITY_A,
EL6021_IDENTITY_B,
EL6021_IDENTITY_C,
EL6021_IDENTITY_D,
]
.to_vec();
let device = get_ethercat_device::<EL6021>(hardware, params, 1, identities).await?;
device
.0
.write()
.await
.write_config(&device.1, &EL6021Configuration::default())
.await?;
{
let mut device_guard = device.0.write().await;
device_guard.set_used(true);
}
device.0
};
let el2004 =
get_ethercat_device::<EL2004>(hardware, params, 2, [EL2004_IDENTITY_A].to_vec())
.await?
.0;
let el3021 =
get_ethercat_device::<EL3021>(hardware, params, 3, [EL3021_IDENTITY_A].to_vec())
.await?
.0;
let el3204 = get_ethercat_device::<EL3204>(
hardware,
params,
4,
[EL3204_IDENTITY_A, EL3204_IDENTITY_B].to_vec(),
)
.await?
.0;
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 t4 = TemperatureInput::new(el3204, EL3204Port::T4);
// For the Relais
let digital_out_1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let digital_out_2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let digital_out_3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let digital_out_4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
// The Extruders temparature Controllers should disable the relais when the max_temperature is reached
let extruder_max_temperature = ThermodynamicTemperature::new::<degree_celsius>(300.0);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_front = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t1,
digital_out_1,
Heating::default(),
Duration::from_millis(500),
700.0,
1.0,
);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_middle = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t2,
digital_out_2,
Heating::default(),
Duration::from_millis(500),
700.0,
1.0,
);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_back = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t3,
digital_out_3,
Heating::default(),
Duration::from_millis(500),
700.0,
1.0,
);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_nozzle = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t4,
digital_out_4,
Heating::default(),
Duration::from_millis(500),
200.0,
0.95,
);
let inverter = MitsubishiCS80::new(SerialInterface::new(el6021, EL6021Port::SI1));
let target_pressure = Pressure::new::<bar>(0.0);
let target_rpm = AngularVelocity::new::<revolution_per_minute>(0.0);
let screw_speed_controller = ScrewSpeedController::new(
inverter,
target_pressure,
target_rpm,
pressure_sensor,
FixedTransmission::new(1.0 / 30.0),
);
let (sender, receiver) = smol::channel::unbounded();
let mut extruder: ExtruderV3 = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV3Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: Instant::now(),
mode: ExtruderV3Mode::Standby,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
temperature_controller_front,
temperature_controller_middle,
temperature_controller_back,
temperature_controller_nozzle,
screw_speed_controller,
emitted_default_state: false,
last_status_hash: None,
};
extruder.emit_state();
Ok(extruder)
})
}
}
@@ -1,120 +0,0 @@
use crate::extruder1::Heating;
use control_core::controllers::pid::PidController;
use ethercat_hal::io::{digital_output::DigitalOutput, temperature_input::TemperatureInput};
use std::time::{Duration, Instant};
use units::f64::*;
use units::thermodynamic_temperature::degree_celsius;
#[derive(Debug)]
pub struct TemperatureController {
pub pid: PidController,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
pub heating: Heating,
pub target_temp: ThermodynamicTemperature,
window_start: Instant,
heating_allowed: bool,
pwm_period: Duration,
max_temperature: ThermodynamicTemperature,
temperature_pid_output: f64,
heating_element_wattage: f64,
max_clamp: f64,
}
impl TemperatureController {
pub fn disable(&mut self) {
self.relais.set(false);
self.heating.heating = false;
self.disallow_heating();
}
pub fn new(
kp: f64,
ki: f64,
kd: f64,
target_temp: ThermodynamicTemperature,
max_temperature: ThermodynamicTemperature,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
heating: Heating,
pwm_duration: Duration,
heating_element_wattage: f64,
max_clamp: f64,
) -> Self {
Self {
pid: PidController::new(kp, ki, kd),
target_temp,
window_start: Instant::now(),
temperature_sensor,
relais,
heating,
heating_allowed: false,
pwm_period: pwm_duration,
max_temperature,
temperature_pid_output: 0.0,
heating_element_wattage,
max_clamp,
}
}
pub fn set_target_temperature(&mut self, temp: ThermodynamicTemperature) {
self.heating.target_temperature = temp;
}
pub const fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
pub const fn allow_heating(&mut self) {
self.heating_allowed = true;
}
pub fn get_heating_element_wattage(&mut self) -> f64 {
self.temperature_pid_output * self.heating_element_wattage
}
pub fn update(&mut self, now: Instant) {
self.temperature_pid_output = 0.0;
let temperature = self.temperature_sensor.get_temperature();
let temperature_celsius = ThermodynamicTemperature::new::<degree_celsius>(
temperature.as_ref().unwrap_or(&0.0).to_owned(),
);
self.heating.wiring_error = temperature.is_err();
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the relais and return
self.relais.set(false);
self.heating.heating = false;
return;
}
if self.heating_allowed {
let error: f64 = self.heating.target_temperature.get::<degree_celsius>()
- self.heating.temperature.get::<degree_celsius>();
let control = self.pid.update(error, now); // PID output
// Clamp PID output to 0.0 – 1.0 (as duty cycle)
let duty = control.clamp(0.0, self.max_clamp);
self.temperature_pid_output = duty;
let elapsed = now.duration_since(self.window_start);
// Restart window if needed
if elapsed >= self.pwm_period {
self.window_start = now;
}
// Compare duty cycle to elapsed time
let on_time = self.pwm_period.mul_f64(duty);
// Relay is ON if within duty cycle window
let on = elapsed < on_time;
self.relais.set(on);
self.heating.heating = on;
}
}
}
+2 -2
View File
@@ -57,12 +57,12 @@ impl MachineAct for LaserMachine {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Macine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Machine do nothing*/
{
()
}
-4
View File
@@ -19,13 +19,11 @@ pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod extruder1;
pub mod extruder2;
pub mod laser;
pub mod machine_identification;
pub mod mock;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod winder2;
pub const VENDOR_QITECH: u16 = 0x0001;
@@ -36,8 +34,6 @@ pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_EXTRUDER_V2: u16 = 0x0016;
pub const TEST_MACHINE: u16 = 0x0033;
use serde_json::Value;
use smol::lock::RwLock;
+2 -2
View File
@@ -48,12 +48,12 @@ impl MachineAct for MockMachine {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Macine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
/*Doesnt connec to any Machine do nothing*/
{
()
}
+3 -9
View File
@@ -1,13 +1,11 @@
#[cfg(feature = "mock-machine")]
use crate::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
use crate::{
Machine, MachineNewParams, extruder1::ExtruderV2, machine_identification::MachineIdentification,
};
use crate::{Machine, MachineNewParams, machine_identification::MachineIdentification};
#[cfg(not(feature = "mock-machine"))]
use crate::{
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine,
test_machine::TestMachine, winder2::Winder2,
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
winder2::Winder2,
};
use lazy_static::lazy_static;
@@ -87,11 +85,8 @@ lazy_static! {
pub static ref MACHINE_REGISTRY: MachineRegistry = {
let mut mc = MachineRegistry::new();
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV3>(ExtruderV3::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
@@ -103,7 +98,6 @@ lazy_static! {
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc.register::<TestMachine>(TestMachine::MACHINE_IDENTIFICATION);
mc
};
}
-36
View File
@@ -1,36 +0,0 @@
use super::TestMachine;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for TestMachine {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::DisconnectMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
}
}
}
-84
View File
@@ -1,84 +0,0 @@
use super::TestMachine;
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 led_on: [bool; 4],
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum TestMachineEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetLed { index: usize, on: bool },
SetAllLeds { on: bool },
}
#[derive(Debug, Clone)]
pub struct TestMachineNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<TestMachineEvents> for TestMachineNamespace {
fn emit(&mut self, events: TestMachineEvents) {
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<TestMachineEvents> for TestMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
TestMachineEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for TestMachine {
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::SetLed { index, on } => self.set_led(index, on),
Mutation::SetAllLeds { on } => self.set_all_leds(on),
}
for (led, &on) in self.leds.iter().zip(self.led_on.iter()) {
led.set(on);
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
-65
View File
@@ -1,65 +0,0 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine::api::{StateEvent, TestMachineEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::channel::{Receiver, Sender};
use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine::api::TestMachineNamespace;
use crate::{TEST_MACHINE, VENDOR_QITECH};
use ethercat_hal::io::digital_output::DigitalOutput;
#[derive(Debug)]
pub struct TestMachine {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub namespace: TestMachineNamespace,
pub last_state_emit: Instant,
pub led_on: [bool; 4],
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub leds: [DigitalOutput; 4],
}
impl Machine for TestMachine {
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 TestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: TEST_MACHINE,
};
}
impl TestMachine {
pub fn emit_state(&mut self) {
let event = StateEvent {
led_on: self.led_on,
}
.build();
self.namespace.emit(TestMachineEvents::State(event));
}
/// Set the state of a specific LED
pub fn set_led(&mut self, index: usize, on: bool) {
if index < self.led_on.len() {
self.led_on[index] = on;
self.emit_state();
}
}
/// Set all LEDs at once
pub fn set_all_leds(&mut self, on: bool) {
self.led_on = [on; 4];
self.emit_state();
}
}
-60
View File
@@ -1,60 +0,0 @@
use crate::test_machine::TestMachine;
use crate::test_machine::api::TestMachineNamespace;
use smol::block_on;
use std::time::Instant;
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
use ethercat_hal::devices::el2004::{EL2004, EL2004_IDENTITY_A, EL2004Port};
use ethercat_hal::io::digital_output::DigitalOutput;
impl MachineNewTrait for TestMachine {
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!(
"[{}::MachineNewTrait/TestMachine::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
let el2004 =
get_ethercat_device::<EL2004>(hardware, params, 1, [EL2004_IDENTITY_A].to_vec())
.await?
.0;
let do1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: TestMachineNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
led_on: [false; 4],
main_sender: params.main_thread_channel.clone(),
leds: [do1, do2, do3, do4],
};
my_test.emit_state();
Ok(my_test)
})
}
}
+7 -5
View File
@@ -6,18 +6,19 @@ use crate::{
app_state::SharedState,
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
};
use control_core::realtime::set_core_affinity;
use control_core::socketio::namespace::NamespaceCacheingLogic;
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
use control_core::{irq_handling::set_irq_affinity, realtime::set_realtime_priority};
use ethercat_hal::debugging::diagnosis_history::get_most_recent_diagnosis_message;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use machines::machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
DeviceIdentificationIdentified, MachineIdentificationUnique, read_device_identifications,
};
use control_core::realtime::set_core_affinity;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use machines::registry::{MACHINE_REGISTRY, MachineRegistry};
use machines::{Machine, MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams};
use smol::channel::Sender;
@@ -155,6 +156,7 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
}),
}
}
let _ = shared_state
.rt_machine_creation_channel
.send(crate::app_state::HotThreadMessage::AddMachines(machines))
+1 -1
View File
@@ -324,7 +324,7 @@ fn main() {
#[cfg(feature = "development-build")]
let running = setup_ctrlc_handler();
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(700);
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(300);
// for the "hot thread"
let (sender, receiver) = smol::channel::unbounded();