Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f4f3de32a | ||
|
|
c9f49198c7 | ||
|
|
e2c12a0168 | ||
|
|
cb6e066512 | ||
|
|
1dd0421f96 | ||
|
|
64855f2e7a | ||
|
|
855ae7b3cf | ||
|
|
5f9a8605c9 | ||
|
|
567730a8d4 | ||
|
|
6b2164b88b | ||
|
|
930ea10409 | ||
|
|
24b703e262 | ||
|
|
152dd3efa0 | ||
|
|
ebc5bae55f | ||
|
|
88214c46a8 | ||
|
|
c9485a92e9 | ||
|
|
9df42c4d13 | ||
|
|
fc9a439ae8 | ||
|
|
e451153406 | ||
|
|
500e2fe481 | ||
|
|
a5884d93e7 | ||
|
|
d2e32cdd78 | ||
|
|
f66c6d4f65 | ||
|
|
fb85cc6db7 | ||
|
|
4e128ded36 | ||
|
|
f50388b141 | ||
|
|
4025a68fe1 | ||
|
|
7a05fa206f | ||
|
|
8594b06c3b | ||
|
|
17523574d8 | ||
|
|
7971797178 | ||
|
|
a8b0837e58 | ||
|
|
e419974844 | ||
|
|
773d0f6c15 | ||
|
|
afc4e0d8b4 | ||
|
|
5e86988ac4 | ||
|
|
2d28c18b1a | ||
|
|
b19764281a | ||
|
|
c25d63f781 | ||
|
|
d79efc412f | ||
|
|
7ad74cf089 | ||
|
|
6073d37892 | ||
|
|
c04937553f | ||
|
|
e08bdfa3c7 | ||
|
|
d282678b19 | ||
|
|
9ff4243350 | ||
|
|
6c79b33752 | ||
|
|
861b5da7a3 |
No files matched your search
@@ -4,4 +4,4 @@ dist/
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
shell.nix
|
||||
@@ -1,3 +1,19 @@
|
||||
# `2.12.0`
|
||||
_17.11.2025_
|
||||
|
||||
## General
|
||||
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
|
||||
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
|
||||
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
|
||||
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
|
||||
|
||||
## Laser
|
||||
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
|
||||
|
||||
## Extruder V2
|
||||
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
|
||||
|
||||
|
||||
# `2.11.0`
|
||||
_21.10.2025_
|
||||
|
||||
|
||||
@@ -74,7 +74,9 @@ 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
|
||||
# More Docs docs
|
||||
|
||||
/assets
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
|
||||
@@ -152,3 +154,227 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
|
||||
|
||||
## Minimal Example — LED Control on the EL2004
|
||||
A complete hardware + software walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
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 QiTech machine framework.
|
||||
It represents the simplest possible hardware interaction in the system:
|
||||
**toggling LED outputs using the QiTech Control Dashboard.**
|
||||
|
||||
---
|
||||
|
||||
# 2. Requirements
|
||||
|
||||
## 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
|
||||
|
||||
## Software
|
||||
*(Installation steps in Section 4)*
|
||||
- Rust toolchain
|
||||
- Node.js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL (included inside repo)
|
||||
|
||||
---
|
||||
|
||||
# 3. Hardware Setup
|
||||
|
||||
## 3.1 Schematic
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||

|
||||
|
||||
Steps:
|
||||
|
||||
- Click Assign on the EK1100
|
||||
|
||||
- Select TestMachine V1
|
||||
|
||||

|
||||
|
||||
|
||||
- Enter a serial number (use the same for EK1100 + EL2004)
|
||||
|
||||

|
||||
|
||||
- Click Write
|
||||
|
||||
- Repeat for the EL2004
|
||||
|
||||
## 5.2 Testing LED Control
|
||||
Navigate to:
|
||||
|
||||
Machines → TestMachine
|
||||

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

|
||||
|
||||
**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.
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 74 KiB |
@@ -1,43 +0,0 @@
|
||||
# Installation Guide for Qitech-Control Software on Linux
|
||||
|
||||
This guide assumes you have an already installed linux distribution like debian,fedora etc.
|
||||
It needs to a kernel that either has PREEMPT_RT patched or which we heavily recommend a mainline kernel later that 6.12, as versions afterwards have PREEMPT_RT integrated and can be switched with a kernel parameter.
|
||||
|
||||
Additionally it is assumed that at least four cpu cores are usable.
|
||||
|
||||
## Kernel Parameters
|
||||
For our Ethercat control to run determinstically while also being as fast as possible you will need to configure a few kernel parameters.
|
||||
Most Distributions use something like grub to persistently configure kernel parameters.
|
||||
|
||||
For Grub add the kernel parameters to the LINUX_COMMANDLINE_DEFAULT:
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
isolcpus=2,3 isolates the third and fourth core from the system scheduler, cutting down latency
|
||||
nohz_full=2,3 isolates third anf fourth core from kernel interrupts
|
||||
rcu_nocbs=2,3 isolates read copy update from the given cpu cores, you guessed it less latency
|
||||
preempt="full" turns linux into a realtime capable OS. Offering deterministic Execution for programs
|
||||
|
||||
After you changed the kernel parameters you have to rebuild the initramfs and bootloader, with something like grub.
|
||||
|
||||
```
|
||||
# On Most Systems you need this command
|
||||
sudo update-grub
|
||||
# Or this one
|
||||
sudo update-grub2
|
||||
```
|
||||
|
||||
If those commands dont work for you or you dont use grub then simply,
|
||||
look up for your given distro what you need to do to persist kernel parameters.
|
||||
|
||||
## Installation On Generic Linux
|
||||
|
||||
This applies to any linux distro, that is not explicitily covered by this guide.
|
||||
|
||||
### Download Rust-toolchain
|
||||
The easiest way is through rustup, simply execute the script as you home user and for everything keep it at defaults
|
||||
|
||||
### Download Npm and Node
|
||||
The exact commands depend on which distro you use, but for most npm should exist as a package you can download in the package manager
|
||||
@@ -91,7 +91,7 @@
|
||||
"globals": "^16.2.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
@@ -175,7 +175,6 @@
|
||||
"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",
|
||||
@@ -639,7 +638,6 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -662,7 +660,6 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -1148,7 +1145,6 @@
|
||||
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
@@ -4095,7 +4091,6 @@
|
||||
"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",
|
||||
@@ -4177,7 +4172,6 @@
|
||||
"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",
|
||||
@@ -4411,7 +4405,8 @@
|
||||
"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
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -4568,7 +4563,6 @@
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
@@ -4585,7 +4579,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -4595,7 +4588,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -4678,7 +4670,6 @@
|
||||
"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",
|
||||
@@ -5033,7 +5024,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -5481,7 +5471,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
@@ -6174,8 +6163,7 @@
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"peer": true
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
@@ -6448,7 +6436,8 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
@@ -7448,7 +7437,6 @@
|
||||
"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",
|
||||
@@ -7509,7 +7497,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -8816,7 +8803,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -8879,7 +8865,6 @@
|
||||
"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"
|
||||
@@ -9554,7 +9539,6 @@
|
||||
"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",
|
||||
@@ -10120,6 +10104,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -12060,11 +12045,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz",
|
||||
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -12178,6 +12163,7 @@
|
||||
"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",
|
||||
@@ -12192,6 +12178,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -12320,7 +12307,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -12329,7 +12315,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -12341,7 +12326,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -12382,7 +12366,8 @@
|
||||
"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
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
@@ -13133,7 +13118,6 @@
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -13923,8 +13907,7 @@
|
||||
"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==",
|
||||
"peer": true
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
@@ -14026,8 +14009,7 @@
|
||||
"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==",
|
||||
"peer": true
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
@@ -14078,7 +14060,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -14390,7 +14371,6 @@
|
||||
"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"
|
||||
@@ -14752,7 +14732,6 @@
|
||||
"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",
|
||||
@@ -14874,7 +14853,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15418,7 +15396,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"globals": "^16.2.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
@@ -720,3 +720,7 @@ The following persons must not work on the machine:
|
||||

|
||||
|
||||

|
||||
|
||||
## Mechanical Diagrams
|
||||
|
||||

|
||||
@@ -0,0 +1,173 @@
|
||||
import { Icon, IconName } from "@/components/Icon";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import {
|
||||
getUnitIcon,
|
||||
renderValueToReactNode,
|
||||
renderUnitSymbol,
|
||||
Unit,
|
||||
} from "./units";
|
||||
import { Label } from "./Label";
|
||||
import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries";
|
||||
|
||||
type TimeframeOption = {
|
||||
label: string;
|
||||
value: number; // in milliseconds
|
||||
};
|
||||
|
||||
export const TIMEFRAME_OPTIONS: TimeframeOption[] = [
|
||||
{ label: "1 min", value: 1 * 60 * 1000 },
|
||||
{ label: "5 min", value: 5 * 60 * 1000 },
|
||||
{ label: "10 min", value: 10 * 60 * 1000 },
|
||||
{ label: "30 min", value: 30 * 60 * 1000 },
|
||||
{ label: "1 hour", value: 60 * 60 * 1000 },
|
||||
];
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
unit?: Unit;
|
||||
timeseries: TimeSeries;
|
||||
icon?: IconName;
|
||||
renderValue?: (value: number) => string;
|
||||
defaultTimeframe?: number; // in milliseconds, defaults to 5 minutes
|
||||
/** Optional externally-controlled timeframe (ms). When provided, component becomes controlled. */
|
||||
timeframe?: number;
|
||||
/** Optional callback when timeframe is changed via this component's selector */
|
||||
onTimeframeChange?: (t: number) => void;
|
||||
/** When true, hide the built-in timeframe selector (useful when sharing a selector outside) */
|
||||
hideSelector?: boolean;
|
||||
};
|
||||
|
||||
export function MinMaxValue({
|
||||
unit,
|
||||
timeseries,
|
||||
icon,
|
||||
label,
|
||||
renderValue,
|
||||
defaultTimeframe = 5 * 60 * 1000, // 5 minutes default
|
||||
timeframe,
|
||||
onTimeframeChange,
|
||||
hideSelector = false,
|
||||
}: Props) {
|
||||
const [internalTimeframe, setInternalTimeframe] = useState(defaultTimeframe);
|
||||
const selectedTimeframe = timeframe ?? internalTimeframe;
|
||||
|
||||
// Calculate min/max from the timeseries using the selected timeframe
|
||||
const { min, max, hasData } = useMemo(() => {
|
||||
// Use short series for timeframes <= 5 seconds, otherwise use long series
|
||||
// Short series typically has ~5 second retention with high resolution
|
||||
// Long series typically has ~1 hour retention with lower resolution
|
||||
const series =
|
||||
selectedTimeframe <= 5000 ? timeseries.short : timeseries.long;
|
||||
|
||||
// Manually calculate min/max to exclude zero values (which might be invalid readings)
|
||||
const cutoffTime = series.lastTimestamp - selectedTimeframe;
|
||||
let min = Number.POSITIVE_INFINITY;
|
||||
let max = Number.NEGATIVE_INFINITY;
|
||||
let hasValidData = false;
|
||||
let dataCount = 0;
|
||||
|
||||
const { values: raw, index, size, validCount } = series;
|
||||
|
||||
const startIdx = validCount < size ? 0 : index;
|
||||
|
||||
for (let i = 0; i < validCount; i++) {
|
||||
const idx = (startIdx + i) % size;
|
||||
const val = raw[idx];
|
||||
|
||||
if (
|
||||
val &&
|
||||
val.timestamp > 0 &&
|
||||
val.timestamp >= cutoffTime &&
|
||||
val.value > 0
|
||||
) {
|
||||
hasValidData = true;
|
||||
dataCount++;
|
||||
if (val.value < min) min = val.value;
|
||||
if (val.value > max) max = val.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidData) {
|
||||
return { min: 0, max: 0, hasData: false };
|
||||
}
|
||||
|
||||
return { min, max, hasData: true };
|
||||
}, [
|
||||
timeseries.short,
|
||||
timeseries.long,
|
||||
timeseries.current,
|
||||
selectedTimeframe,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<Label label={label}>
|
||||
<div className="flex flex-row items-center gap-6">
|
||||
{/* Min Value */}
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Icon
|
||||
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
|
||||
className="size-5 text-blue-500"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500">Min</span>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<span className="font-mono text-xl font-bold">
|
||||
{hasData
|
||||
? renderValueToReactNode(min, unit, renderValue)
|
||||
: "-"}
|
||||
</span>
|
||||
{hasData && (
|
||||
<span className="text-sm">{renderUnitSymbol(unit)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Max Value */}
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Icon
|
||||
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
|
||||
className="size-5 text-red-500"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500">Max</span>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<span className="font-mono text-xl font-bold">
|
||||
{hasData
|
||||
? renderValueToReactNode(max, unit, renderValue)
|
||||
: "-"}
|
||||
</span>
|
||||
{hasData && (
|
||||
<span className="text-sm">{renderUnitSymbol(unit)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{/* Timeframe Selector (hidden when sharing selector externally) */}
|
||||
{!hideSelector && (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{TIMEFRAME_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
if (onTimeframeChange) onTimeframeChange(option.value);
|
||||
else setInternalTimeframe(option.value);
|
||||
}}
|
||||
className={`rounded-md px-3 py-1 text-sm transition-colors ${
|
||||
selectedTimeframe === option.value
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export function Extruder2ControlPage() {
|
||||
unit="rpm"
|
||||
title="Target Output RPM"
|
||||
min={0}
|
||||
max={106}
|
||||
max={44}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={setInverterTargetRpm}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue";
|
||||
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Label } from "@/control/Label";
|
||||
@@ -28,6 +29,12 @@ export function Laser1ControlPage() {
|
||||
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
|
||||
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
|
||||
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
|
||||
|
||||
// Detect if this is a 2-axis laser
|
||||
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
|
||||
// Shared timeframe state (default 5 minutes)
|
||||
const [timeframe, setTimeframe] = React.useState<number>(5 * 60 * 1000);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
@@ -77,6 +84,57 @@ export function Laser1ControlPage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 border-t pt-4">
|
||||
{isTwoAxis ? (
|
||||
// For 2-axis lasers: show diameter and roundness min/max side by side
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<MinMaxValue
|
||||
label="Diameter Range"
|
||||
unit="mm"
|
||||
timeseries={diameter}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
timeframe={timeframe}
|
||||
hideSelector
|
||||
/>
|
||||
{roundness?.current && (
|
||||
<MinMaxValue
|
||||
label="Roundness Range"
|
||||
timeseries={roundness}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
timeframe={timeframe}
|
||||
hideSelector
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// For single-axis lasers: show only diameter min/max with shared selector
|
||||
<MinMaxValue
|
||||
label="Diameter Range"
|
||||
unit="mm"
|
||||
timeseries={diameter}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
timeframe={timeframe}
|
||||
hideSelector
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shared timeframe selector for diameter/roundness */}
|
||||
<div className="mt-3 flex flex-row flex-wrap gap-2">
|
||||
{TIMEFRAME_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setTimeframe(option.value)}
|
||||
className={`rounded-md px-3 py-1 text-sm transition-colors ${
|
||||
timeframe === option.value
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
<ControlCard title="Settings">
|
||||
<Label label="Set Target Diameter">
|
||||
|
||||
@@ -103,6 +103,94 @@ 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",
|
||||
@@ -339,14 +427,50 @@ 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 = (
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,46 +1,62 @@
|
||||
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 { ChooseVersionPage } from "@/setup/ChooseVersionPage";
|
||||
import { fallback, zodValidator } from "@tanstack/zod-adapter";
|
||||
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 { 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 { z } from "zod";
|
||||
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
|
||||
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
|
||||
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
|
||||
|
||||
// make a route tree like this
|
||||
// _mainNavigation/machines/winder2/$serial/control
|
||||
// _mainNavigation/configuration/a
|
||||
@@ -48,6 +64,18 @@ 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,
|
||||
@@ -96,6 +124,42 @@ 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",
|
||||
@@ -313,6 +377,7 @@ export const rootTree = RootRoute.addChildren([
|
||||
laser1GraphsRoute,
|
||||
laser1PresetsRoute,
|
||||
]),
|
||||
testMachineSerialRoute.addChildren([testMachineControlRoute]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
aquapath1ControlRoute,
|
||||
@@ -335,6 +400,14 @@ export const rootTree = RootRoute.addChildren([
|
||||
extruder2PresetsRoute,
|
||||
]),
|
||||
|
||||
extruder3Route.addChildren([
|
||||
extruder3ControlRoute,
|
||||
extruder3SettingsRoute,
|
||||
extruder3ManualRoute,
|
||||
extruder3GraphsRoute,
|
||||
extruder3PresetsRoute,
|
||||
]),
|
||||
|
||||
mock1SerialRoute.addChildren([
|
||||
mock1ControlRoute,
|
||||
mock1GraphRoute,
|
||||
|
||||
@@ -10,10 +10,10 @@ workspace = true
|
||||
clap = { version = "4.5.40", features = ["derive"] }
|
||||
ethercrab = "0.6.0"
|
||||
interfaces = "0.0.9"
|
||||
smol = "2.0.2"
|
||||
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
control_core = { path = "../control-core" }
|
||||
futures = "0.3.31"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use ethercat_hal::{
|
||||
helpers::ethercrab_types::EthercrabSubDeviceGroupPreoperational,
|
||||
};
|
||||
use ethercrab::MainDevice;
|
||||
use futures::executor::block_on;
|
||||
use smol;
|
||||
|
||||
use crate::{MAX_SUBDEVICES, PDI_LEN, print::print_markdown_table};
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn ls(
|
||||
let driver = device_from_subdevice_identity(&identity);
|
||||
let identification_adresses = get_identification_addresses(&identity, device.name());
|
||||
let identification = match identification_adresses {
|
||||
Ok(_) => block_on(machine_device_identification(&device, maindevice)),
|
||||
Ok(_) => smol::block_on(machine_device_identification(&device, maindevice)),
|
||||
Err(_) => Err(anyhow!("")),
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ pub fn ls(
|
||||
print_markdown_table(&table, true);
|
||||
|
||||
// Put group into op state
|
||||
match block_on(group.into_op(maindevice)) {
|
||||
match smol::block_on(group.into_op(maindevice)) {
|
||||
Ok(_) => println!("Successfully put group into OP state"),
|
||||
Err(e) => println!("Failed to put group into OP state: {}", e),
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ use std::sync::Arc;
|
||||
|
||||
use dump::dump_eeprom;
|
||||
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, std::ethercat_now};
|
||||
use futures::executor::block_on;
|
||||
use read::read_eeprom;
|
||||
use restore::restore_eeoprom;
|
||||
use smol;
|
||||
|
||||
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
|
||||
const MAX_SUBDEVICES: usize = 16;
|
||||
/// Maximum PDU data payload size - set this to the max PDI size or higher.
|
||||
const MAX_PDU_DATA: usize = PduStorage::element_size(256);
|
||||
const MAX_PDU_DATA: usize = PduStorage::element_size(1024);
|
||||
/// Maximum number of EtherCAT frames that can be in flight at any one time.
|
||||
const MAX_FRAMES: usize = 16;
|
||||
/// Maximum total PDI length.
|
||||
const PDI_LEN: usize = 256;
|
||||
const PDI_LEN: usize = 1024;
|
||||
|
||||
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn main() {
|
||||
.get_one::<usize>("SUBDEVICE")
|
||||
.expect("subdevice index is required");
|
||||
let file = sub_matches.get_one::<String>("file");
|
||||
let result = block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
|
||||
let result = smol::block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
|
||||
if let Err(e) = result {
|
||||
eprintln!("Error reading EEPROM: {}", e);
|
||||
}
|
||||
@@ -69,7 +69,8 @@ async fn main() {
|
||||
let file = sub_matches
|
||||
.get_one::<String>("file")
|
||||
.expect("file is required");
|
||||
let result = block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
|
||||
let result =
|
||||
smol::block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
|
||||
if let Err(e) = result {
|
||||
eprintln!("Error writing EEPROM: {}", e);
|
||||
}
|
||||
@@ -78,7 +79,7 @@ async fn main() {
|
||||
let file = sub_matches
|
||||
.get_one::<String>("file")
|
||||
.expect("file is required");
|
||||
let result = block_on(read_eeprom(file));
|
||||
let result = smol::block_on(read_eeprom(file));
|
||||
if let Err(e) = result {
|
||||
eprintln!("Error parsing EEPROM: {}", e);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ units = { path = "../units" }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
smol = "2.0.2"
|
||||
rand = "0.9.2"
|
||||
futures = "0.3.31"
|
||||
tracing = "0.1.41"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use std::fmt;
|
||||
|
||||
const DIAGNOSIS_HISTORY_INDEX: u16 = 0x10f3;
|
||||
const NEWEST_MESSAGE: u8 = 02;
|
||||
const NEW_MESSAGE_AVAILABLE: u8 = 04;
|
||||
const DIAG_MESSAGE_LENGTH: usize = 26;
|
||||
|
||||
pub struct SubdeviceDiagnosisEntry {
|
||||
pub diag_code: u32,
|
||||
pub flags: u16,
|
||||
pub text_id: u16,
|
||||
pub timestamp: u64,
|
||||
pub data_type_p1: u16,
|
||||
pub p1: u8,
|
||||
pub flags_p2: u16,
|
||||
pub p2: [u8; 5],
|
||||
}
|
||||
|
||||
impl fmt::Display for SubdeviceDiagnosisEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"SubdeviceDiagnosisEntry {{ diag_code: {}, flags: {}, text_id: {}, timestamp: {}, data_type_p1: {}, p1: {}, flags_p2: {}, p2: {:?} }}",
|
||||
self.diag_code,
|
||||
self.flags,
|
||||
self.text_id,
|
||||
self.timestamp,
|
||||
self.data_type_p1,
|
||||
self.p1,
|
||||
self.flags_p2,
|
||||
self.p2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SubdeviceDiagnosisEntry {
|
||||
pub fn to_pretty_string(&self) -> String {
|
||||
// Decode message type
|
||||
let msg_type = match self.flags {
|
||||
0x0000 => "Info",
|
||||
0x0001 => "Warning",
|
||||
0x0002 => "Error",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
// Decode data type of P1
|
||||
let p1_type = match self.data_type_p1 {
|
||||
0x0005 => "UINT8",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
// Decode data type of P2
|
||||
let p2_type = match self.flags_p2 {
|
||||
0x0005 => "BYTE[5]",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
// Format P2 bytes as hex list
|
||||
let p2_hex: Vec<String> = self.p2.iter().map(|b| format!("0x{:02X}", b)).collect();
|
||||
|
||||
format!(
|
||||
"Diagnosis Entry:\n\
|
||||
├─ diag_code: 0x{:08X}\n\
|
||||
├─ message type: {} (0x{:04X})\n\
|
||||
├─ text_id: {}{}\n\
|
||||
├─ timestamp: {} (64-bit)\n\
|
||||
├─ P1 type: {} (0x{:04X})\n\
|
||||
├─ P1 value: {}\n\
|
||||
├─ P2 type: {} (0x{:04X})\n\
|
||||
└─ P2 data: [{}]",
|
||||
self.diag_code,
|
||||
msg_type,
|
||||
self.flags,
|
||||
self.text_id,
|
||||
if self.text_id == 0 { " (no text)" } else { "" },
|
||||
self.timestamp,
|
||||
// P1
|
||||
p1_type,
|
||||
self.data_type_p1,
|
||||
self.p1,
|
||||
// P2
|
||||
p2_type,
|
||||
self.flags_p2,
|
||||
p2_hex.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Could be tested with this:
|
||||
00 E8 00 A0 02 00 00 00 5F FB D5 A5 A6 0B 00 00 05 00 11 05 10 08 2A 00 2A 00
|
||||
*/
|
||||
|
||||
pub fn convert_raw_diagnosis_bytes(
|
||||
message: [u8; DIAG_MESSAGE_LENGTH],
|
||||
) -> Result<SubdeviceDiagnosisEntry, anyhow::Error> {
|
||||
let diag_code_bytes = [message[0], message[1], message[2], message[3]];
|
||||
let diag_code = u32::from_le_bytes(diag_code_bytes);
|
||||
|
||||
let flags_code_bytes = [message[4], message[5]];
|
||||
let flags = u16::from_le_bytes(flags_code_bytes);
|
||||
|
||||
let text_id_bytes = [message[6], message[7]];
|
||||
let text_id = u16::from_le_bytes(text_id_bytes);
|
||||
|
||||
let timestamp_bytes = [
|
||||
message[8],
|
||||
message[9],
|
||||
message[10],
|
||||
message[11],
|
||||
message[12],
|
||||
message[13],
|
||||
message[14],
|
||||
message[15],
|
||||
];
|
||||
let timestamp = u64::from_le_bytes(timestamp_bytes);
|
||||
|
||||
let data_type_p1_bytes = [message[16], message[17]];
|
||||
let data_type_p1 = u16::from_le_bytes(data_type_p1_bytes);
|
||||
|
||||
// TODO: Find the Constants
|
||||
// 0x0005 Apparently Means p1 variable is u8
|
||||
// For now we only handle u8
|
||||
let p1 = match data_type_p1 {
|
||||
0x0005 => message[18],
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if p1 == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"convert_raw_diagnosis_bytes: Datatype_p1 is unknown {:?}",
|
||||
data_type_p1
|
||||
));
|
||||
}
|
||||
|
||||
// flags_p2 is packed, whatever thats supposed to mean
|
||||
let flags_p2_bytes = [message[19], message[20]];
|
||||
let flags_p2 = u16::from_le_bytes(flags_p2_bytes);
|
||||
|
||||
let p2 = [
|
||||
message[21],
|
||||
message[22],
|
||||
message[23],
|
||||
message[24],
|
||||
message[25],
|
||||
];
|
||||
return Ok(SubdeviceDiagnosisEntry {
|
||||
diag_code,
|
||||
flags,
|
||||
text_id,
|
||||
timestamp,
|
||||
data_type_p1,
|
||||
p1,
|
||||
flags_p2,
|
||||
p2,
|
||||
});
|
||||
}
|
||||
|
||||
/// Expects the SubdeviceRef to be the Coupler or any other device with a diag history index
|
||||
pub async fn get_most_recent_diagnosis_message(
|
||||
device: &SubDeviceRef<'_, &SubDevice>,
|
||||
) -> Option<String> {
|
||||
let new_message_available = device
|
||||
.sdo_read::<bool>(DIAGNOSIS_HISTORY_INDEX, NEW_MESSAGE_AVAILABLE)
|
||||
.await;
|
||||
let new_message_available = match new_message_available {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"get_most_recent_diagnosis_message: Failed to read new_message_available {:?}",
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if !new_message_available {
|
||||
tracing::info!("get_most_recent_diagnosis_message: No Diagnosis Messages found");
|
||||
// No messages available
|
||||
return None;
|
||||
}
|
||||
|
||||
let newest_message_index = device
|
||||
.sdo_read::<u8>(DIAGNOSIS_HISTORY_INDEX, NEWEST_MESSAGE)
|
||||
.await;
|
||||
let newest_message_index = match newest_message_index {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"get_most_recent_diagnosis_message: Failed to read newest_message_index {:?}",
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"get_most_recent_diagnosis_message: Reading newest message at index: {}",
|
||||
newest_message_index
|
||||
);
|
||||
let res = device
|
||||
.sdo_read::<[u8; DIAG_MESSAGE_LENGTH]>(DIAGNOSIS_HISTORY_INDEX, newest_message_index)
|
||||
.await;
|
||||
let message: [u8; DIAG_MESSAGE_LENGTH] = match res {
|
||||
Ok(res) => res,
|
||||
Err(_) => {
|
||||
tracing::error!("get_most_recent_diagnosis_message: Failed to read Diagnosis Message");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let message = convert_raw_diagnosis_bytes(message);
|
||||
let message = match message {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"get_most_recent_diagnosis_message: Failed to convert Diagnosis Message bytes to struct {:?}",
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"get_most_recent_diagnosis_message: {}",
|
||||
message.to_pretty_string()
|
||||
);
|
||||
return Some(message.to_pretty_string());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod diagnosis_history;
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::executor::block_on;
|
||||
use physical::{AnalogInputRange, AnalogInputValue};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
@@ -40,7 +39,7 @@ impl AnalogInput {
|
||||
let get_input = Box::new(move || -> AnalogInputInput {
|
||||
let device2 = Arc::clone(&device);
|
||||
let port_clone = port.clone();
|
||||
block_on(async move {
|
||||
smol::block_on(async move {
|
||||
let device = device2.read().await;
|
||||
device.get_input(port_clone)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
/// Digital Input (DI) device
|
||||
@@ -27,7 +26,7 @@ impl DigitalInput {
|
||||
let port2 = port;
|
||||
let device2 = device.clone();
|
||||
let get_input = Box::new(move || {
|
||||
let device = block_on(device2.read());
|
||||
let device = smol::block_on(device2.read());
|
||||
device.get_input(port2.clone())
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
/// Digital Output (DO) device
|
||||
@@ -29,7 +28,7 @@ impl DigitalOutput {
|
||||
let port1 = port.clone();
|
||||
let device1 = device.clone();
|
||||
let set_output = Box::new(move |value: DigitalOutputOutput| {
|
||||
let mut device = block_on(device1.write());
|
||||
let mut device = smol::block_on(device1.write());
|
||||
device.set_output(port1.clone(), value);
|
||||
});
|
||||
|
||||
@@ -37,7 +36,7 @@ impl DigitalOutput {
|
||||
let port2 = port;
|
||||
let device2 = device.clone();
|
||||
let get_output = Box::new(move || -> DigitalOutputOutput {
|
||||
let device = block_on(device2.read());
|
||||
let device = smol::block_on(device2.read());
|
||||
device.get_output(port2.clone())
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
/// Encoder Input device
|
||||
@@ -34,7 +33,7 @@ impl EncoderInput {
|
||||
let port_counter = port.clone();
|
||||
let device_counter = device.clone();
|
||||
let get_counter = Box::new(move || {
|
||||
let device = block_on(device_counter.read());
|
||||
let device = smol::block_on(device_counter.read());
|
||||
device.get_counter_value(port_counter.clone())
|
||||
});
|
||||
|
||||
@@ -42,7 +41,7 @@ impl EncoderInput {
|
||||
let port_frequency = port.clone();
|
||||
let device_frequency = device.clone();
|
||||
let get_frequency = Box::new(move || {
|
||||
let device = block_on(device_frequency.read());
|
||||
let device = smol::block_on(device_frequency.read());
|
||||
device.get_frequency(port_frequency.clone())
|
||||
});
|
||||
|
||||
@@ -50,7 +49,7 @@ impl EncoderInput {
|
||||
let port_period = port.clone();
|
||||
let device_period = device.clone();
|
||||
let get_period = Box::new(move || {
|
||||
let device = block_on(device_period.read());
|
||||
let device = smol::block_on(device_period.read());
|
||||
device.get_period(port_period.clone())
|
||||
});
|
||||
|
||||
@@ -58,7 +57,7 @@ impl EncoderInput {
|
||||
let port_set = port;
|
||||
let device_set = device;
|
||||
let set_counter = Box::new(move |value: u32| {
|
||||
let mut device = block_on(device_set.write());
|
||||
let mut device = smol::block_on(device_set.write());
|
||||
device.set_counter(port_set.clone(), value)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
/// Pulse Train Output (PTO) device
|
||||
@@ -29,21 +28,21 @@ impl<'device> PulseTrainOutput {
|
||||
// build sync write closure
|
||||
let device1 = device.clone();
|
||||
let set_output = Box::new(move |value: PulseTrainOutputOutput| {
|
||||
let mut device = block_on(device1.write());
|
||||
let mut device = smol::block_on(device1.write());
|
||||
device.set_output(port, value);
|
||||
});
|
||||
|
||||
// build sync get closure
|
||||
let device2 = device.clone();
|
||||
let get_output = Box::new(move || -> PulseTrainOutputOutput {
|
||||
let device = block_on(device2.read());
|
||||
let device = smol::block_on(device2.read());
|
||||
device.get_output(port)
|
||||
});
|
||||
|
||||
// build sync get closure
|
||||
let device2 = device;
|
||||
let get_input = Box::new(move || -> PulseTrainOutputInput {
|
||||
let device = block_on(device2.read());
|
||||
let device = smol::block_on(device2.read());
|
||||
device.get_input(port)
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use crate::helpers::el70xx_velocity_converter::EL70x1VelocityConverter;
|
||||
use anyhow::Error;
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
/// Pulse Train Output (PTO) device
|
||||
@@ -47,7 +46,7 @@ impl<'device> StepperVelocityEL70x1 {
|
||||
// build sync get closure
|
||||
let device2 = device.clone();
|
||||
let get_input = Box::new(move || -> Result<StepperVelocityEL70x1Input, Error> {
|
||||
block_on(async {
|
||||
smol::block_on(async {
|
||||
let device = device2.read().await;
|
||||
device.get_input(port)
|
||||
})
|
||||
@@ -55,7 +54,7 @@ impl<'device> StepperVelocityEL70x1 {
|
||||
|
||||
let device3 = device.clone();
|
||||
let get_output = Box::new(move || -> Result<StepperVelocityEL70x1Output, Error> {
|
||||
block_on(async {
|
||||
smol::block_on(async {
|
||||
let device = device3.read().await;
|
||||
device.get_output(port)
|
||||
})
|
||||
@@ -65,7 +64,7 @@ impl<'device> StepperVelocityEL70x1 {
|
||||
let device3 = device;
|
||||
let get_speed_range = Box::new(
|
||||
move || -> Result<crate::shared_config::el70x1::EL70x1SpeedRange, Error> {
|
||||
block_on(async {
|
||||
smol::block_on(async {
|
||||
let device = device3.read().await;
|
||||
Ok(device.get_speed_range(port))
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::pdo::basic::Limit;
|
||||
@@ -29,10 +28,9 @@ impl TemperatureInput {
|
||||
let get_input = Box::new(move || {
|
||||
let device2 = device2.clone();
|
||||
let port_clone = port2.clone();
|
||||
block_on(async move {
|
||||
let device = device2.read().await;
|
||||
device.get_input(port_clone)
|
||||
})
|
||||
|
||||
let device = device2.read_blocking();
|
||||
device.get_input(port_clone)
|
||||
});
|
||||
Self { get_input }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod coe;
|
||||
pub mod debugging;
|
||||
pub mod devices;
|
||||
pub mod helpers;
|
||||
pub mod io;
|
||||
|
||||
@@ -19,7 +19,6 @@ euclid = "0.22.11"
|
||||
|
||||
# concurrency
|
||||
smol = "2.0.2"
|
||||
futures = "0.3.31"
|
||||
|
||||
# web
|
||||
serde_json = "1.0.143"
|
||||
|
||||
@@ -45,12 +45,12 @@ impl MachineAct for AquaPathV1 {
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ impl MachineAct for BufferV1 {
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Macine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ impl MachineAct for ExtruderV2 {
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) => (),
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -251,8 +251,11 @@ 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);
|
||||
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_uses_rpm(uses_rpm);
|
||||
}
|
||||
|
||||
@@ -270,8 +273,11 @@ 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));
|
||||
self.screw_speed_controller.set_target_screw_rpm(
|
||||
AngularVelocity::new::<revolution_per_minute>(rpm),
|
||||
AngularVelocity::new::<revolution_per_minute>(1500.0),
|
||||
4,
|
||||
);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
|
||||
@@ -443,7 +443,6 @@ 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];
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use crate::machines::extruder1::mock::ExtruderV2;
|
||||
use control_core::machines::new::MachineAct;
|
||||
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();
|
||||
@@ -12,4 +20,26 @@ impl MachineAct for ExtruderV2 {
|
||||
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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::machines::extruder1::{HeatingType, api::Mutation, mock::ExtruderV2};
|
||||
use control_core::machines::api::MachineApi;
|
||||
use control_core::socketio::namespace::Namespace;
|
||||
use smol::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
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> {
|
||||
@@ -45,7 +45,13 @@ impl MachineApi for ExtruderV2 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::machines::extruder1::{
|
||||
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},
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use control_core::machines::identification::MachineIdentification;
|
||||
use smol::channel::Receiver;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use control_core::machines::identification::MachineIdentificationUnique;
|
||||
use smol::channel::Sender;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use control_core_derive::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::machines::{
|
||||
use crate::{
|
||||
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
|
||||
extruder1::{
|
||||
ExtruderV2Mode,
|
||||
@@ -31,9 +38,25 @@ pub mod mock_emit;
|
||||
pub mod new;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
#[derive(Debug, 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>,
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use crate::machines::extruder1::{
|
||||
ExtruderV2Mode,
|
||||
api::{
|
||||
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
|
||||
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
|
||||
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
|
||||
TemperaturePidStates,
|
||||
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,
|
||||
},
|
||||
mock::ExtruderV2,
|
||||
};
|
||||
|
||||
impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
|
||||
fn new(
|
||||
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, anyhow::Error>
|
||||
impl MachineNewTrait for ExtruderV2 {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -20,19 +21,23 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
|
||||
// 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 {
|
||||
control_core::machines::new::MachineNewHardware::Serial(_) => {
|
||||
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
|
||||
}
|
||||
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
|
||||
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(),
|
||||
|
||||
@@ -2,17 +2,22 @@
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use serde::{Deserialize, Serialize};
|
||||
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;
|
||||
|
||||
use crate::{AsyncThreadMessage, Machine};
|
||||
use units::f64::*;
|
||||
use units::thermodynamic_temperature::degree_celsius;
|
||||
|
||||
use crate::{AsyncThreadMessage, Machine};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{
|
||||
MACHINE_EXTRUDER_V1, MachineMessage, VENDOR_QITECH,
|
||||
@@ -94,6 +99,7 @@ pub struct ExtruderV2 {
|
||||
emitted_default_state: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl Machine for ExtruderV2 {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
|
||||
@@ -85,6 +85,8 @@ 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());
|
||||
|
||||
@@ -214,8 +216,13 @@ 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);
|
||||
let screw_speed_controller = ScrewSpeedController::new(
|
||||
inverter,
|
||||
target_pressure,
|
||||
target_rpm,
|
||||
pressure_sensor,
|
||||
FixedTransmission::new(1.0 / 34.0),
|
||||
);
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
let mut extruder: ExtruderV2 = Self {
|
||||
|
||||
@@ -6,7 +6,6 @@ use control_core::{
|
||||
transmission::{Transmission, fixed::FixedTransmission},
|
||||
};
|
||||
use ethercat_hal::io::analog_input::AnalogInput;
|
||||
use futures::executor::block_on;
|
||||
use units::angular_velocity::revolution_per_minute;
|
||||
use units::electric_current::milliampere;
|
||||
use units::f64::*;
|
||||
@@ -42,6 +41,7 @@ impl ScrewSpeedController {
|
||||
target_pressure: Pressure,
|
||||
target_rpm: AngularVelocity,
|
||||
pressure_sensor: AnalogInput,
|
||||
transmission: FixedTransmission,
|
||||
) -> Self {
|
||||
let now = Instant::now();
|
||||
Self {
|
||||
@@ -54,7 +54,8 @@ impl ScrewSpeedController {
|
||||
pressure_sensor,
|
||||
uses_rpm: true,
|
||||
forward_rotation: true,
|
||||
transmission: FixedTransmission::new(1.0 / 34.0),
|
||||
transmission: transmission,
|
||||
//FixedTransmission::new(1.0 / 34.0),
|
||||
motor_on: false,
|
||||
nozzle_pressure_limit: Pressure::new::<bar>(100.0),
|
||||
nozzle_pressure_limit_enabled: true,
|
||||
@@ -108,15 +109,22 @@ impl ScrewSpeedController {
|
||||
self.target_pressure = target_pressure;
|
||||
}
|
||||
|
||||
pub fn set_target_screw_rpm(&mut self, target_rpm: AngularVelocity) {
|
||||
pub fn set_target_screw_rpm(
|
||||
&mut self,
|
||||
target_rpm: AngularVelocity,
|
||||
_motor_rpm_rating: AngularVelocity,
|
||||
motor_poles: usize,
|
||||
) {
|
||||
// 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::new::<cycle_per_minute>(target_motor_rpm.get::<revolution_per_minute>());
|
||||
|
||||
let target_frequency: Frequency = Frequency::new::<hertz>(
|
||||
target_motor_rpm.get::<revolution_per_minute>() as f64 / 120.0 * motor_poles as f64,
|
||||
);
|
||||
|
||||
self.inverter.set_frequency_target(target_frequency);
|
||||
}
|
||||
@@ -206,15 +214,8 @@ impl ScrewSpeedController {
|
||||
|
||||
pub fn update(&mut self, now: Instant, is_extruding: bool) {
|
||||
// TODO: move this logic elsewhere or make non async
|
||||
block_on(self.inverter.act(now));
|
||||
|
||||
let wiring_error = self.get_wiring_error();
|
||||
if wiring_error {
|
||||
// emit in act
|
||||
}
|
||||
|
||||
smol::block_on(self.inverter.act(now));
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#[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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#[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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
#[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 ¶ms.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,12 @@ impl MachineAct for LaserMachine {
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Macine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ 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;
|
||||
@@ -34,6 +36,8 @@ 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;
|
||||
|
||||
@@ -48,12 +48,12 @@ impl MachineAct for MockMachine {
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Macine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
|
||||
use crate::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
|
||||
|
||||
use crate::{Machine, MachineNewParams, machine_identification::MachineIdentification};
|
||||
use crate::{
|
||||
Machine, MachineNewParams, extruder1::ExtruderV2, machine_identification::MachineIdentification,
|
||||
};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{
|
||||
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
|
||||
winder2::Winder2,
|
||||
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine,
|
||||
test_machine::TestMachine, winder2::Winder2,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
@@ -85,8 +87,11 @@ 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);
|
||||
|
||||
@@ -98,6 +103,7 @@ lazy_static! {
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
|
||||
mc.register::<TestMachine>(TestMachine::MACHINE_IDENTIFICATION);
|
||||
mc
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::{
|
||||
helpers::hashing::{byte_folding_u16, hash_djb2},
|
||||
machines::identification::{
|
||||
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::{
|
||||
MACHINE_EXTRUDER_V1, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
|
||||
machine_identification::{
|
||||
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
|
||||
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
|
||||
},
|
||||
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
|
||||
};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::machines::{MACHINE_EXTRUDER_V1, VENDOR_QITECH};
|
||||
|
||||
/// Mock serial device for testing MockMachine
|
||||
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::{
|
||||
helpers::hashing::{byte_folding_u16, hash_djb2},
|
||||
machines::identification::{
|
||||
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::{
|
||||
MACHINE_MOCK, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
|
||||
machine_identification::{
|
||||
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
|
||||
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
|
||||
},
|
||||
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
|
||||
};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
|
||||
|
||||
/// Mock serial device for testing MockMachine
|
||||
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::{
|
||||
helpers::hashing::{byte_folding_u16, hash_djb2},
|
||||
machines::identification::{
|
||||
use crate::{
|
||||
MACHINE_WINDER_V1, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
|
||||
machine_identification::{
|
||||
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
|
||||
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
|
||||
},
|
||||
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
|
||||
};
|
||||
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Mock serial device for testing MockMachine
|
||||
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use serialport::UsbPortInfo;
|
||||
use smol::Task;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use std::collections::HashMap;
|
||||
/*
|
||||
*@author: Alisher Darmenov
|
||||
*@company: QiTech
|
||||
@@ -75,38 +74,30 @@ impl SerialDetection {
|
||||
}
|
||||
}
|
||||
|
||||
// Async function that runs until at least one serial device is found.
|
||||
pub async fn find_serial() -> Option<HashMap<String, UsbPortInfo>> {
|
||||
smol::Timer::after(Duration::from_secs(1)).await;
|
||||
/*
|
||||
|
||||
let devices = SerialDetection::detect_devices();
|
||||
if !devices.is_empty() {
|
||||
return Some(devices);
|
||||
} else {
|
||||
return None;
|
||||
pub async fn start_serial_discovery(app_state: Arc<>) {
|
||||
loop {
|
||||
let devices = SerialDetection::detect_devices();
|
||||
|
||||
if !devices.is_empty() {
|
||||
handle_serial_device_hotplug(app_state.clone(), devices).await;
|
||||
}
|
||||
|
||||
smol::Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a smol::Task that resolves when atleast one device is found.
|
||||
pub fn start_serial_discovery() -> Task<Option<HashMap<String, UsbPortInfo>>> {
|
||||
smol::spawn(find_serial())
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
pub async fn handle_serial_device_hotplug(
|
||||
app_state: Arc<SharedState>,
|
||||
map: Option<HashMap<String, UsbPortInfo>>,
|
||||
map: HashMap<String, UsbPortInfo>,
|
||||
) {
|
||||
let laser_ident = SerialDeviceIdentification {
|
||||
vendor_id: 0x0403,
|
||||
product_id: 0x6001,
|
||||
};
|
||||
|
||||
let laser = match map {
|
||||
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
|
||||
None => None,
|
||||
};
|
||||
let laser = SerialDetection::get_path_by_id(laser_ident, map);
|
||||
|
||||
let mut unique_ident: Option<MachineIdentificationUnique> = None;
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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 ¶ms.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,22 @@
|
||||
mod winder2_imports {
|
||||
pub use super::super::Winder2Mode;
|
||||
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
|
||||
pub use serde::Deserialize;
|
||||
pub use serde::Serialize;
|
||||
|
||||
pub use std::{sync::Arc, time::Duration};
|
||||
|
||||
pub use control_core::{
|
||||
machines::{
|
||||
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
|
||||
},
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
pub use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
|
||||
pub use control_core_derive::BuildEvent;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use serde_json::Value;
|
||||
pub use smol::lock::Mutex;
|
||||
pub use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use tracing::instrument;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
use crate::machines::winder2::mock::Winder2;
|
||||
use control_core::machines::new::MachineAct;
|
||||
use super::Winder2;
|
||||
use crate::MachineAct;
|
||||
use crate::MachineMessage;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for Winder2 {
|
||||
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.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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use control_core::{machines::api::MachineApi, socketio::namespace::Namespace};
|
||||
use serde_json::Value;
|
||||
use smol::lock::Mutex;
|
||||
|
||||
use crate::machines::winder2::api::Mutation;
|
||||
|
||||
use super::Winder2;
|
||||
use crate::{MachineApi, winder2::api::Mutation};
|
||||
use serde_json::Value;
|
||||
use std::time::Instant;
|
||||
|
||||
impl MachineApi for Winder2 {
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
|
||||
@@ -61,7 +56,13 @@ impl MachineApi for Winder2 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::machines::winder2::Winder2Mode;
|
||||
use crate::machines::winder2::api::LiveValuesEvent;
|
||||
use crate::machines::winder2::api::{
|
||||
ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events,
|
||||
};
|
||||
use crate::machines::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
|
||||
use crate::machines::winder2::spool_speed_controller::SpoolSpeedControllerType;
|
||||
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
|
||||
use control_core::machines::identification::{MachineIdentification, MachineIdentificationUnique};
|
||||
use super::Winder2;
|
||||
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
|
||||
use crate::winder2::Winder2Mode;
|
||||
use crate::winder2::api::LiveValuesEvent;
|
||||
use crate::winder2::api::{ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events};
|
||||
use crate::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
|
||||
use crate::winder2::spool_speed_controller::SpoolSpeedControllerType;
|
||||
use crate::{MACHINE_WINDER_V1, VENDOR_QITECH};
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
|
||||
use super::Winder2;
|
||||
|
||||
impl Winder2 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_WINDER_V1,
|
||||
};
|
||||
|
||||
pub fn stop_or_pull_spool_reset(&mut self, now: Instant) {}
|
||||
pub fn stop_or_pull_spool_reset(&mut self, _now: Instant) {}
|
||||
/// Implement Mode
|
||||
pub fn set_mode(&mut self, mode: &Winder2Mode) {
|
||||
self.mode_state = ModeState {
|
||||
@@ -86,6 +83,20 @@ impl Winder2 {
|
||||
}
|
||||
|
||||
pub fn build_state_event(&mut self) -> StateEvent {
|
||||
use crate::MachineCrossConnectionState;
|
||||
|
||||
let connected_machine = self.connected_machines.get(0);
|
||||
|
||||
let ident = match connected_machine {
|
||||
Some(machine) => Some(machine.ident.clone()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let cross_conn = MachineCrossConnectionState {
|
||||
machine_identification_unique: ident,
|
||||
is_available: connected_machine.is_some(),
|
||||
};
|
||||
|
||||
StateEvent {
|
||||
is_default_state: self.is_default_state,
|
||||
traverse_state: self.traverse_state.clone(),
|
||||
@@ -94,7 +105,7 @@ impl Winder2 {
|
||||
mode_state: self.mode_state.clone(),
|
||||
tension_arm_state: self.tension_arm_state.clone(),
|
||||
spool_speed_controller_state: self.spool_speed_controller_state.clone(),
|
||||
connected_machine_state: self.connected_machine_state.clone(),
|
||||
connected_machine_state: cross_conn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +121,7 @@ impl Winder2 {
|
||||
/// It will set [`Self::spool_mode`]
|
||||
fn set_traverse_mode(&mut self, mode: &Winder2Mode) {
|
||||
self.mode_state = ModeState {
|
||||
mode: crate::machines::winder2::api::Mode::Standby,
|
||||
mode: crate::winder2::api::Mode::Standby,
|
||||
can_wind: true,
|
||||
};
|
||||
self.emit_state();
|
||||
@@ -227,7 +238,7 @@ impl Winder2 {
|
||||
/// set connected buffer
|
||||
pub fn set_connected_buffer(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
_machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
self.emit_state();
|
||||
}
|
||||
@@ -235,7 +246,7 @@ impl Winder2 {
|
||||
/// disconnect buffer
|
||||
pub fn disconnect_buffer(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
_machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::machines::{
|
||||
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
|
||||
};
|
||||
use control_core_derive::Machine;
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod mock_emit;
|
||||
pub mod new;
|
||||
use crate::machines::winder2::api::Winder2Namespace;
|
||||
|
||||
use super::api::{
|
||||
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState, TensionArmState,
|
||||
TraverseState,
|
||||
TraverseState, Winder2Namespace,
|
||||
};
|
||||
use crate::{
|
||||
AsyncThreadMessage, Machine, MachineConnection, MachineMessage,
|
||||
machine_identification::MachineIdentificationUnique,
|
||||
};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2 {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
namespace: Winder2Namespace,
|
||||
@@ -33,8 +33,18 @@ pub struct Winder2 {
|
||||
pub tension_arm_state: TensionArmState,
|
||||
/// spool speed controller state
|
||||
pub spool_speed_controller_state: SpoolSpeedControllerState,
|
||||
/// connected machine state
|
||||
pub connected_machine_state: MachineCrossConnectionState,
|
||||
|
||||
/// Receive from Api or MainThread
|
||||
api_receiver: Receiver<MachineMessage>,
|
||||
api_sender: Sender<MachineMessage>,
|
||||
|
||||
/// Communicate with main thread
|
||||
main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
|
||||
/// All currently "connected" Machines
|
||||
connected_machines: Vec<MachineConnection>,
|
||||
/// Defaults to limit of 2
|
||||
max_connected_machines: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Winder2 {
|
||||
@@ -42,3 +52,13 @@ impl std::fmt::Display for Winder2 {
|
||||
write!(f, "Winder2")
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for Winder2 {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
use crate::machines::winder2::{
|
||||
api::{
|
||||
use crate::{
|
||||
MachineNewParams, MachineNewTrait,
|
||||
winder2::api::{
|
||||
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
|
||||
TensionArmState, TraverseState, Winder2Namespace,
|
||||
},
|
||||
mock::Winder2,
|
||||
};
|
||||
use control_core::machines::connection::MachineCrossConnectionState;
|
||||
|
||||
impl control_core::machines::new::MachineNewTrait for Winder2 {
|
||||
fn new(
|
||||
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, anyhow::Error>
|
||||
use super::Winder2;
|
||||
|
||||
impl MachineNewTrait for Winder2 {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -18,11 +17,11 @@ impl control_core::machines::new::MachineNewTrait for Winder2 {
|
||||
// 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 {
|
||||
control_core::machines::new::MachineNewHardware::Serial(_) => {
|
||||
crate::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
|
||||
}
|
||||
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
|
||||
crate::MachineNewHardware::Ethercat(_) => {
|
||||
// For ethercat mode, we could potentially use the ethercat devices
|
||||
// but for a mock machine, we'll just note it and proceed
|
||||
}
|
||||
@@ -30,7 +29,12 @@ impl control_core::machines::new::MachineNewTrait for Winder2 {
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let mut extruder_mock_machine = Self {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut winder_mock_machine = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
max_connected_machines: 2,
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: Winder2Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
@@ -43,11 +47,11 @@ impl control_core::machines::new::MachineNewTrait for Winder2 {
|
||||
mode_state: ModeState::default(),
|
||||
tension_arm_state: TensionArmState::default(),
|
||||
spool_speed_controller_state: SpoolSpeedControllerState::default(),
|
||||
connected_machine_state: MachineCrossConnectionState::default(),
|
||||
connected_machines: vec![],
|
||||
};
|
||||
|
||||
extruder_mock_machine.emit_state();
|
||||
winder_mock_machine.emit_state();
|
||||
|
||||
Ok(extruder_mock_machine)
|
||||
Ok(winder_mock_machine)
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,9 @@ mod winder2_imports {
|
||||
pub use super::api::SpoolAutomaticActionMode;
|
||||
pub use std::time::Instant;
|
||||
pub use units::f64::Length;
|
||||
pub use units::length::meter;
|
||||
}
|
||||
|
||||
use units::Length;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mod winder2_imports {
|
||||
pub use super::api::SpoolAutomaticActionMode;
|
||||
@@ -36,19 +35,19 @@ mod winder2_imports {
|
||||
digital_input::DigitalInput, digital_output::DigitalOutput,
|
||||
stepper_velocity_el70x1::StepperVelocityEL70x1,
|
||||
};
|
||||
pub use smol::channel::{Receiver, Sender};
|
||||
pub use smol::lock::RwLock;
|
||||
pub use std::{fmt::Debug, sync::Weak, time::Instant};
|
||||
|
||||
pub use crate::buffer1::BufferV1;
|
||||
pub use crate::{AsyncThreadMessage, Machine};
|
||||
pub use units::ConstZero;
|
||||
pub use units::f64::Length;
|
||||
pub use units::{length::meter, length::millimeter, velocity::meter_per_second};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use smol::channel::{Receiver, Sender};
|
||||
pub use winder2_imports::*;
|
||||
|
||||
use crate::{AsyncThreadMessage, Machine};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{
|
||||
MACHINE_WINDER_V1, MachineConnection, MachineMessage, VENDOR_QITECH,
|
||||
@@ -74,6 +73,7 @@ impl Default for SpoolAutomaticAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl Machine for Winder2 {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
|
||||
@@ -23,7 +23,7 @@ bitvec = "1.0.1"
|
||||
# concurrency
|
||||
smol = "2.0.2"
|
||||
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
|
||||
futures = "0.3.31"
|
||||
regex = "1.11.3"
|
||||
|
||||
# web
|
||||
serde_json = "1.0.143"
|
||||
@@ -37,14 +37,12 @@ serialport = "4.7.3"
|
||||
#tracing
|
||||
tracing = { version = "0.1.41", features = ["attributes"] }
|
||||
tracing-journald = { version = "0.3.1", optional = true }
|
||||
tracing-futures = "0.2.5"
|
||||
tracing-subscriber = { version = "0.3.20", features = [
|
||||
"env-filter",
|
||||
"chrono",
|
||||
"registry",
|
||||
] }
|
||||
utils = { version = "0.1.0", path="../utils" }
|
||||
tonic = { version = "0.13.1", optional = true }
|
||||
spin_sleep = "1.3.3"
|
||||
dhat = { version = "0.3.3", optional = true }
|
||||
ctrlc = { version = "3.3", optional = true }
|
||||
@@ -71,6 +69,3 @@ heap-profile = ["dhat"]
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = "fat"
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tonic"]
|
||||
@@ -1,11 +0,0 @@
|
||||
# Server Structure
|
||||
|
||||
- `/server`: Is Backend part that makes connection between Beckhoff and Electron. Implements machine logic.
|
||||
|
||||
## EtherCAT
|
||||
|
||||
- `/ethercat`: Implements logic of EtherCAT
|
||||
|
||||
## Serial
|
||||
|
||||
- `/serial`: Implements logic of Serial, starting from serial device detection ending with serial device registration as machine
|
||||
@@ -142,7 +142,7 @@ impl SharedState {
|
||||
|
||||
pub async fn add_machines_if_not_exists(&self, machines: Vec<MachineObj>) {
|
||||
let mut current_machines = self.current_machines_meta.lock().await;
|
||||
|
||||
tracing::info!("add_machines_if_not_exists: {:?}", current_machines);
|
||||
// Track existing machine identifiers for quick lookup
|
||||
let existing_ids: HashSet<_> = current_machines
|
||||
.iter()
|
||||
|
||||