Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffdcf17bdb | ||
|
|
070f09e32b | ||
|
|
07bb10e668 | ||
|
|
b5ae86b19c | ||
|
|
35567b967f | ||
|
|
c45718e670 | ||
|
|
74dcd027e0 | ||
|
|
c793d3fa50 | ||
|
|
04fdc8a9d1 | ||
|
|
fd702ff0bc | ||
|
|
f25bf71753 | ||
|
|
4c33873095 | ||
|
|
8dbcb5efc3 | ||
|
|
36426aafe0 | ||
|
|
21cf9314be | ||
|
|
4f2af7b90d | ||
|
|
06795dcf78 | ||
|
|
1241d0a4a7 | ||
|
|
f36723f435 | ||
|
|
e96854a6bc | ||
|
|
04c5a673da | ||
|
|
40e17ef5f1 | ||
|
|
39200aac12 | ||
|
|
43a405eec4 | ||
|
|
0206a4ddca | ||
|
|
34da7178f8 | ||
|
|
2726c47280 | ||
|
|
63cf25b3d6 | ||
|
|
eae13207e4 | ||
|
|
f65d100cd5 | ||
|
|
13e4ea1ac7 | ||
|
|
49788246c8 | ||
|
|
d4e849d588 | ||
|
|
2689d19928 | ||
|
|
968e9327f6 | ||
|
|
3cbb74b3b0 | ||
|
|
50fcfd6e30 | ||
|
|
ce0f27787c | ||
|
|
08ab7fd404 | ||
|
|
f138b61a45 | ||
|
|
0ad0a58d46 | ||
|
|
3d13e6cee2 | ||
|
|
86915c2987 | ||
|
|
ada8d0f12c | ||
|
|
735456b6eb | ||
|
|
b46a528adb | ||
|
|
80f2343a60 | ||
|
|
e030fbe23e | ||
|
|
1ef0cb608c | ||
|
|
789a84e511 | ||
|
|
28620b6a23 | ||
|
|
01609513fb | ||
|
|
379a4ec417 | ||
|
|
28d898e780 | ||
|
|
daabb170f5 | ||
|
|
c4803e63dd | ||
|
|
0022d86035 | ||
|
|
dc711a820f | ||
|
|
59cf2cea2a | ||
|
|
da69ad1c3e | ||
|
|
6ff434fb47 | ||
|
|
a63c57a0b9 | ||
|
|
21c43adcb4 | ||
|
|
afa23666e2 | ||
|
|
245ad64426 | ||
|
|
9665971150 | ||
|
|
3b056c8f26 | ||
|
|
62b35e0237 | ||
|
|
11bc62f40e | ||
|
|
116c76d218 | ||
|
|
4dcedb537d | ||
|
|
09022c9c86 | ||
|
|
80cf98b11d | ||
|
|
644e750353 | ||
|
|
aecabfde43 | ||
|
|
686678a1fe | ||
|
|
7ac0b788c2 | ||
|
|
b8168c6b43 | ||
|
|
6026d2a658 | ||
|
|
a301f3e228 | ||
|
|
0b6bd68a24 | ||
|
|
e7fd390263 | ||
|
|
eab675795a | ||
|
|
8c2dc9a507 | ||
|
|
f6c0744efa | ||
|
|
8c7e810359 | ||
|
|
f0fd9e8ce6 | ||
|
|
c5579a4471 | ||
|
|
de7a65bf12 | ||
|
|
e4d0b3c5be | ||
|
|
dad029ab0d | ||
|
|
9c780799d1 | ||
|
|
27a33f9def | ||
|
|
f44cd029f9 | ||
|
|
9abb7629fb | ||
|
|
a161d85a63 | ||
|
|
9df9e6df4a | ||
|
|
29d465c13e | ||
|
|
622c94f2d9 | ||
|
|
f31a2789ca | ||
|
|
e7632e8d1b | ||
|
|
2f4f3de32a | ||
|
|
c9f49198c7 | ||
|
|
e2c12a0168 | ||
|
|
cb6e066512 | ||
|
|
1dd0421f96 | ||
|
|
64855f2e7a | ||
|
|
855ae7b3cf | ||
|
|
5f9a8605c9 | ||
|
|
567730a8d4 | ||
|
|
6b2164b88b | ||
|
|
930ea10409 | ||
|
|
24b703e262 | ||
|
|
152dd3efa0 | ||
|
|
ebc5bae55f | ||
|
|
88214c46a8 | ||
|
|
c9485a92e9 | ||
|
|
9df42c4d13 | ||
|
|
fc9a439ae8 | ||
|
|
e451153406 | ||
|
|
500e2fe481 | ||
|
|
a5884d93e7 | ||
|
|
d2e32cdd78 | ||
|
|
f66c6d4f65 |
No files matched your search
@@ -46,3 +46,6 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
- name: Mock Machine
|
||||
run: cargo build --features development-build --features mock-machine
|
||||
@@ -4,4 +4,7 @@ dist/
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
shell.nix
|
||||
node_modules/
|
||||
runtime_metrics.csv
|
||||
compile_metrics.csv
|
||||
@@ -204,7 +204,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -221,7 +221,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -291,7 +291,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -539,7 +539,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -682,7 +682,7 @@ dependencies = [
|
||||
"deluxe-macros",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -695,7 +695,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -710,7 +710,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -721,7 +721,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -962,7 +962,7 @@ dependencies = [
|
||||
"deluxe",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1014,7 +1014,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1908,7 +1908,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2221,7 +2221,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2251,7 +2251,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2323,6 +2323,7 @@ dependencies = [
|
||||
"dhat",
|
||||
"ethercat_hal",
|
||||
"ethercrab",
|
||||
"libc",
|
||||
"machines",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -2572,9 +2573,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2620,7 +2621,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2680,7 +2681,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2842,7 +2843,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3073,7 +3074,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3138,7 +3139,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3149,7 +3150,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3425,5 +3426,5 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
@@ -35,7 +35,7 @@ Other
|
||||
|
||||
## Backend
|
||||
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master software. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
|
||||
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
|
||||
|
||||
@@ -74,6 +74,15 @@ 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 | - |
|
||||
|
||||
# Minimal Hardware Examples
|
||||
|
||||
For complete step-by-step tutorials on setting up your first hardware, including wiring diagrams and software configuration, see the [Getting Started Guide](./docs/developer-docs/getting-started.md#minimal-hardware-examples).
|
||||
|
||||
Available examples:
|
||||
- **LED Control with EL2004** - Digital output control (simplest setup)
|
||||
- **Analog Input with EL3021** - Reading analog current measurements
|
||||
|
||||
|
||||
# More Docs
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_DIR="${BUILD_DIR:-target}"
|
||||
CSV_FILE="${CSV_FILE:-compile_metrics.csv}"
|
||||
DEFAULT_PKG="server"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- cargo build
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "cargo" ] && [ "${2:-}" = "build" ]; then
|
||||
cargo clean
|
||||
BUILD_CMD=(cargo rustc -p "$DEFAULT_PKG")
|
||||
shift 2
|
||||
BUILD_CMD+=("$@")
|
||||
BUILD_CMD+=(-- --emit=llvm-ir -C debuginfo=0)
|
||||
else
|
||||
BUILD_CMD=("$@")
|
||||
fi
|
||||
|
||||
compile_start_ns=$(date +%s%N)
|
||||
"${BUILD_CMD[@]}"
|
||||
compile_end_ns=$(date +%s%N)
|
||||
compile_ms=$(( (compile_end_ns - compile_start_ns) / 1000000 ))
|
||||
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
llvm_ir_lines=$(
|
||||
find "$BUILD_DIR" -type f -name '*.ll' -print0 \
|
||||
| xargs -0 cat \
|
||||
| wc -l
|
||||
)
|
||||
else
|
||||
llvm_ir_lines=0
|
||||
fi
|
||||
|
||||
bin_path=""
|
||||
|
||||
if [ -n "${BINARY_PATH:-}" ] && [ -f "$BINARY_PATH" ]; then
|
||||
bin_path="$BINARY_PATH"
|
||||
elif [ -f "./target/release/server" ]; then
|
||||
bin_path="./target/release/server"
|
||||
elif [ -f "./target/debug/server" ]; then
|
||||
bin_path="./target/debug/server"
|
||||
elif [ -f "./server/target/release/server" ]; then
|
||||
bin_path="./server/target/release/server"
|
||||
elif [ -f "./server/target/debug/server" ]; then
|
||||
bin_path="./server/target/debug/server"
|
||||
fi
|
||||
|
||||
if [ -n "$bin_path" ]; then
|
||||
binary_size_bytes=$(stat -c '%s' "$bin_path")
|
||||
else
|
||||
binary_size_bytes=0
|
||||
fi
|
||||
|
||||
finished_timestamp_ms=$(($(date +%s%N) / 1000000))
|
||||
finished_datetime=$(date -Iseconds)
|
||||
|
||||
if [ ! -f "$CSV_FILE" ]; then
|
||||
echo "datetime,finished_timestamp_ms,compile_ms,llvm_ir_lines,binary_size_bytes" > "$CSV_FILE"
|
||||
fi
|
||||
|
||||
echo "${finished_datetime},${finished_timestamp_ms},${compile_ms},${llvm_ir_lines},${binary_size_bytes}" \
|
||||
>> "$CSV_FILE"
|
||||
|
||||
echo "Metrics recorded in $CSV_FILE"
|
||||
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 557 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 212 KiB |
|
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 |
@@ -82,3 +82,12 @@ git push
|
||||
# Recommended Editor Setup
|
||||
|
||||
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
|
||||
|
||||
# Minimal Hardware Examples
|
||||
|
||||
To get started with actual hardware, check out these step-by-step tutorials:
|
||||
|
||||
- **[LED Control with EL2004](./minimal-example-el2004.md)** - Digital output control, the simplest possible hardware setup
|
||||
- **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements
|
||||
|
||||
These examples provide complete hardware wiring diagrams and software setup instructions.
|
||||
@@ -0,0 +1,224 @@
|
||||
# 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="../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="../assets/el2004.jpeg" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Final Assembled Setup
|
||||
|
||||
#### **Figure 3 — EK1100 + EL2004 Connected**
|
||||
<img src="../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="../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="../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.
|
||||
@@ -0,0 +1,178 @@
|
||||
# Minimal Example — Analog Input EL3021
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
## 1. Introduction
|
||||
The EL3021 Analog Input example demonstrates how to measure analog current on a **Beckhoff EL3021 EtherCAT terminal** using the QiTech machine framework.
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- Beckhoff **EL3021 EtherCAT Terminal** (1-channel analog input)
|
||||
- Beckhoff **EK1100 EtherCAT Coupler**
|
||||
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
|
||||
- **Adjustable DC power supply** (or any other means of producing 4-20mA DC @ <10V )
|
||||
- Jumper / bridge wires (0.5–1.5 mm² recommended)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Schematic
|
||||

|
||||
The 24V supply powers the EL2004, while the adjustable power supply generates the analog input signal for the EL3021.
|
||||
|
||||
#### ⚠️ 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="../assets/ek1100.jpeg" width="400">
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3.3 EL3021 Integration
|
||||
|
||||
Slide the EL3021 onto the **right side of the EK1100** until it locks.
|
||||
The EtherCAT E-Bus and power contacts connect automatically.
|
||||
Then you need to wire the adjustable power supply to the EL3021.
|
||||
|
||||
#### **Figure 2 — EL3021 Terminal**
|
||||
<img src="../assets/EL3021.jpg" width="400">
|
||||
|
||||
1. Wire ⊕ to **Terminal 1**
|
||||
2. Wire ⊖ to **Terminal 2**
|
||||
---
|
||||
|
||||
### 3.4 Final Assembled Setup
|
||||
|
||||
#### **Figure 3 — EK1100 + EL3021 Connected**
|
||||
<img src="../assets/EK1100+EL3021.jpg" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 3.5 Power & Ethernet
|
||||
|
||||
### 3.5.1 Power
|
||||
Connect the 24 V adapter to the hollow plug used earlier.
|
||||
|
||||
#### Example AC/DC Adapter (Figure 4):
|
||||
|
||||
<img src="../assets/adapter.jpeg" width="400">
|
||||
|
||||
|
||||
### 3.5.2 Ethernet
|
||||
Use a standard LAN cable to connect your PC → EK1100.
|
||||
The final powered up and connected setup should look like this:
|
||||
|
||||
<img src="../assets/EL3021_power+eth.jpg" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
### 4.2 Running the Backend
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
This script:
|
||||
|
||||
- Builds the backend
|
||||
|
||||
- Grants required system capabilities (raw sockets)
|
||||
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the 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 both backend and frontend are running, the Setup -> EtherCat page should look something like this:
|
||||

|
||||
Listing the EK1100 and the EL3021.
|
||||
|
||||
To assign the two devices to a machine, click "Assign" on both devices. This should open the following pop-up modal:
|
||||
|
||||
**EK1100**
|
||||

|
||||
1. Select "AnalogTest V1" under "Machine"
|
||||
2. Choose a serial number to uniquely identify the machine (1 in this example)
|
||||
3. Under "Device Role" select "0 - Bus Coupler"
|
||||
|
||||
**EL3021**
|
||||

|
||||
1. Select "AnalogTest V1" under "Machine"
|
||||
2. Use the same serial number you chose for the EK1100
|
||||
3. Under "Device Role" select "1 - EL3021"
|
||||
---
|
||||
The machine is now configured.
|
||||
A new Tab "AnalogTest" should appear in the right sidebar. Clicking that tab should show the machine control interface:
|
||||

|
||||
Results should show the current measured by the analog input - the current set on the adjustable power supply.
|
||||
|
||||
|
||||
## 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 EL3021 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el30xx_de.pdf)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Machine Documentation
|
||||
|
||||
This directory contains documentation for QiTech machines.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
docs/machines/
|
||||
├── manuals/ # Machine operation manuals (markdown files)
|
||||
│ ├── extruder.md
|
||||
│ ├── winder.md
|
||||
│ ├── laser.md
|
||||
│ └── mock.md
|
||||
├── extruder/ # Machine-specific documentation
|
||||
├── winder/
|
||||
├── laser/
|
||||
└── mock/
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
All manual images are stored in: **`electron/public/images/manuals/[machine-name]/`**
|
||||
|
||||
For example:
|
||||
- Winder images: `electron/public/images/manuals/winder/`
|
||||
- Extruder images: `electron/public/images/manuals/extruder/`
|
||||
- Laser images: `electron/public/images/manuals/laser/`
|
||||
|
||||
### Image References in Manuals
|
||||
|
||||
When referencing images in manual markdown files (located in `docs/machines/manuals/`), use relative paths:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
## Adding Images
|
||||
|
||||
1. Place images in: `electron/public/images/manuals/[machine-name]/`
|
||||
2. Reference in markdown: `../../../../electron/public/images/manuals/[machine-name]/your-image.png`
|
||||
@@ -1,4 +1,65 @@
|
||||

|
||||

|
||||
|
||||
<!-- Datasheet: Added below Titelseite. Placeholder images to be replaced with final exports. -->
|
||||
|
||||
# QiTech Pro Extruder – Datasheet
|
||||
|
||||

|
||||
|
||||
## Included Assemblies
|
||||
Electrical cabinet · Temperature controller · Frequency inverter · Cable routing + connection to QiTech Control (digital control)
|
||||
|
||||
## Motor
|
||||
| Specification | Value |
|
||||
| ------------- | ----- |
|
||||
| Motor power | 1.5 kW |
|
||||
| Output speed | 41 rpm |
|
||||
| Output torque | 336 Nm |
|
||||
| Transmission ratio | 34:2 |
|
||||
| Service factor cG | 1.4 |
|
||||
| Voltage / Frequency | 400 V / 50 Hz |
|
||||
| Efficiency class | IE3 (Premium Efficiency) |
|
||||
| Efficiency @ 100% rated load / % rated current (std. voltage) | 5.98 / 3.45 A |
|
||||
| Protection class | IP55, Insulation class F |
|
||||
| Construction design | Flange design, terminal box top |
|
||||
| Shaft end (d×L) | 40 × 80 mm |
|
||||
|
||||
## Melt Pressure Sensor (DSI1-1/2-500-F-15)
|
||||
| Specification | Value |
|
||||
| ------------- | ----- |
|
||||
| Output signal | 4–20 mA (2-conductor) |
|
||||
| Sensor system | Mercury-free |
|
||||
| Sensor | DMS Vollbrückentechnik |
|
||||
| Measuring accuracy | 0.5 % |
|
||||
| Calibration signal | 80 % |
|
||||
| Supply voltage | 15–32 VDC |
|
||||
| Measuring range | 0–500 bar |
|
||||
| Temperature range (membrane) | max. 410 °C |
|
||||
| Temperature range (sensor head) | max. 85 °C |
|
||||
| Screw-in thread | 1/2"–20 UNF |
|
||||
|
||||
## Extruder Barrel & Screw
|
||||
Nitrided extruder barrel (Ø 65 mm). Screw hardened from high‑alloyed steel (Ø 25 mm).
|
||||
|
||||
## Heating Elements & Temperature Sensors
|
||||
3 heating zones (max. 350 °C), Heater 415 V, 3 × 750 W. 3× Type K temperature sensors, 1/8" NPT.
|
||||
|
||||
## Hopper
|
||||
Volume: 4 L · Material: Aluminum · Easily detachable.
|
||||
|
||||
## Dimensions
|
||||
|
||||

|
||||
|
||||
Overall length 1365 mm · Width 220 mm · Height 384 mm.
|
||||
|
||||
Fully wired and assembled (raised on 2 aluminum profiles).
|
||||
|
||||
Delivery time after completion: 2–5 working days.
|
||||
|
||||
Warranty: 24 months after delivery (excluding wearing parts).
|
||||
|
||||
<br>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -55,7 +116,7 @@ Darmstadt, 10/06/2025
|
||||
|
||||
64392 Darmstadt,
|
||||
|
||||
<img src="images/manuals/extruder/signature.jpeg" width="300" height="60"/>
|
||||
<img src="/images/manuals/extruder/signature.jpeg" width="300" height="60"/>
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
@@ -101,7 +162,7 @@ Operating instructions
|
||||
|
||||
# 1 Machine description
|
||||
|
||||

|
||||

|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -168,7 +229,7 @@ Accuracy class: 2
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -176,17 +237,17 @@ Accuracy class: 2
|
||||
|
||||
| Pictogram | Description of pictogram | Pictogram | Pictogram Description |
|
||||
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -194,15 +255,15 @@ Accuracy class: 2
|
||||
|
||||
| Prohibition signs Pictogram | Pictogram description | International pictograms / other Pictogram | Pictogram Description |
|
||||
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/extruder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="images/manuals/extruder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="images/manuals/extruder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="images/manuals/extruder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="images/manuals/extruder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="images/manuals/extruder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="images/manuals/extruder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="images/manuals/extruder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="images/manuals/extruder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="images/manuals/extruder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="images/manuals/extruder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="images/manuals/extruder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="images/manuals/extruder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="images/manuals/extruder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="images/manuals/extruder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="images/manuals/extruder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
| <img src="/images/manuals/extruder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="/images/manuals/extruder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="/images/manuals/extruder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="/images/manuals/extruder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="/images/manuals/extruder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="/images/manuals/extruder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="/images/manuals/extruder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="/images/manuals/extruder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="/images/manuals/extruder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="/images/manuals/extruder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="/images/manuals/extruder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="/images/manuals/extruder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="/images/manuals/extruder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="/images/manuals/extruder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="/images/manuals/extruder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="/images/manuals/extruder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -275,7 +336,7 @@ The machine has a main switch that disconnects/connects the mains supply. The ON
|
||||
|
||||
The emergency stop switch is used to quickly stop the machine in the event of danger. This is designed with redundant emergency stop contactors. An emergency stop switching device also prevents the machine from being switched on again after a power failure. By activating the emergency stop device, the entire machine is shut down and disconnected from the mains supply.
|
||||
|
||||

|
||||

|
||||
|
||||
Hazardous movements are brought to a controlled standstill as quickly as possible. The machine can only be restarted by physically resetting the emergency stop switch and then starting one of the operating modes in the software.
|
||||
|
||||
@@ -357,11 +418,11 @@ Depending on the activity or the duration of the activity, the operator must det
|
||||
<br><br>
|
||||
| Picture | Danger |
|
||||
|---------|--------|
|
||||
| |**Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves.
|
||||
| |**Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves.
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to incorrect installation and commissioning!** <brErrors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|  | **Danger to life due to incorrect installation and commissioning!** <brErrors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -390,7 +451,7 @@ The CEE plug of the extruder must be plugged into a suitable three-phase socket
|
||||
|
||||
The machines are delivered with the newest version of the software installed. Updates, as well as previous updates and the software version can be viewed under Setup-> Updates. If a new version of the software is available, it will be displayed here. To perform the update an internet connection is required. In the next step, click the red "Select" button next to the chosen update. The display will now shut down, and when it is turned back on, the latest version of the software will be installed.
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -431,7 +492,7 @@ Then start the machine as follows:
|
||||
- Set the “target temperature” of the individual heating zones according to the melting material in the 3 input fields
|
||||
- Set the “target temperature” of the nozzle
|
||||
- After reaching the target temperature, wait 15 minutes until the machine has heated through and the plastic in the screw barrel has completely melted.
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -443,7 +504,7 @@ Then start the machine as follows:
|
||||
- Press the "Extrude" button in the “mode” box
|
||||
- Wait until old melt has been "flushed" out of the extruder, i.e. no more discolouration of the filament is no longer discoloured and a constant material flow has been achieved
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -471,7 +532,7 @@ Then start the machine as follows:
|
||||
- Go to "Config" in the funktion bar
|
||||
- To set the screw direction click on "Forward" or "Backward" in the "Invert settings" box
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -495,7 +556,7 @@ Then start the machine as follows:
|
||||
|
||||
| Picture | DANGER! |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | **Danger to life due to operation of the machine in a faulty condition.** <br> If safety-relevant changes or defects occur on the machine, shut it down immediately, secure it against being switched on again and report the incident to the responsible office/person! |
|
||||
|  | **Danger to life due to operation of the machine in a faulty condition.** <br> If safety-relevant changes or defects occur on the machine, shut it down immediately, secure it against being switched on again and report the incident to the responsible office/person! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -533,11 +594,11 @@ If further work is necessary, such as replacing the extruder screw, please conta
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
|  | **Observe the instructions for maintenance work when servicing the machine!** |
|
||||
|  | **Observe the instructions for maintenance work when servicing the machine!** |
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Risk of screw breakage if disassembled while hot!** <br> Screw connections may only be loosened when the system has cooled down. |
|
||||
|  | **Risk of screw breakage if disassembled while hot!** <br> Screw connections may only be loosened when the system has cooled down. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -620,15 +681,15 @@ Appliances with this logo on the packaging or on the appliance must be disposed
|
||||
|
||||
| Picture | Danger! |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The extruder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the extruder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="images/manuals/extruder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="images/manuals/extruder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
| <img src="/images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="/images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The extruder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="/images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="/images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the extruder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="/images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="/images/manuals/extruder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="/images/manuals/extruder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="/images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="/images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -674,7 +735,7 @@ The following persons must not work on the machine:
|
||||
|
||||
| Picture | DANGER! |
|
||||
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Risk of injury due to insufficient qualification!** Improper handling can lead to considerable personal injury and damage to property. |
|
||||
|  | **Risk of injury due to insufficient qualification!** Improper handling can lead to considerable personal injury and damage to property. |
|
||||
| - Special activities may only be carried out by the persons specified in the respective chapters of these instructions. |
|
||||
|
||||
<br>
|
||||
@@ -703,24 +764,24 @@ The following persons must not work on the machine:
|
||||
|
||||
## Electrical Diagrams
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

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

|
||||

|
||||
@@ -1,4 +1,88 @@
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
# Data sheet: QiTech Pro Winder
|
||||
|
||||

|
||||
|
||||
Short description:
|
||||
|
||||
The QiTech Pro Winder is a machine for winding and rewinding 3D printing filaments and other strand materials. It is an improvement upon the proven QiTech Winder with support for multiple filament diameters, spools up to 5 kg, angular speeds up to 20 m/min and automatic adjustment of the winding speed depending on the filament tension.
|
||||
|
||||
It consists of a pulling unit, a tension arm, a filament guiding traverse and the spool axis. It is controlled by a computer using the QiTech Control software. The traverse movement ensures a clean wind of the filament on the spool. Automatic load adjustment allows for tight winding without having to manually readjust the winding speed each layer. A laser in the traverse allows for easy calibration of the spool width.
|
||||
|
||||
<br>
|
||||
|
||||
## Scope of delivery
|
||||
|
||||
| # | Item |
|
||||
| - | ----------------------------------------- |
|
||||
| 1 | QiTech Pro Winder |
|
||||
| 2 | 2x Spool Carrier-Pins |
|
||||
| 3 | 4x Weights for tension arm |
|
||||
| 4 | 230V/110V to 24V Power Supply |
|
||||
| 5 | USB C to USB A cable |
|
||||
| 6 | USB Drive with QiTech Control software |
|
||||
| 7 | Operating manual |
|
||||
| 8 | optional: wheeled frame |
|
||||
|
||||
<br>
|
||||
|
||||
## Dimensions and Weight
|
||||
|
||||

|
||||

|
||||
|
||||
| | no frame | incl. frame |
|
||||
| -------------- | -------- | ----------- |
|
||||
| Weight | 20 kg | 35 kg |
|
||||
| Length (1) | 91 cm | 91 cm |
|
||||
| Width (2) | 50 cm | 50 cm |
|
||||
| Height (3) | 42 cm | 134 cm |
|
||||
|
||||
<br>
|
||||
|
||||
## Requirements of the Spool
|
||||
|
||||
The QiTech Pro Winder is compatible with almost all spools. The spool is rotated by a Carrier-Pin and secured against lateral slipping by a quick release mechanism. The maximum spool weight is 5 kg.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
## Winding Speed
|
||||
|
||||
How many meters of filament can be wound per minute at maximum speed depends on the inner diameter of the spool/how much filament has already been wound. The resulting path speed determines how much filament can be wound up in a given time. We take a usual spool with an inner diameter of 100 mm to calculate an expected maximum speed. If the inner diameter is larger, the maximum speed shifts upwards.
|
||||
|
||||
Winding Speed:
|
||||
|
||||
- Min.: 0.5 Meters/Minute
|
||||
- Max.: 20 Meters/Minute
|
||||
|
||||
<br>
|
||||
|
||||
## Friction of the puller rolls
|
||||
|
||||
Since the diameter of the extruded filament depends primarily on the pulling speed of the Pro Winder, it is important that it remains constant. This is ensured on the one hand by the use of a stepper motor as drive. At the same time, a vulcanized PU coating of the rolls and a spring-increased contact pressure make the filament only slip through from approx. 30 N tension.
|
||||
|
||||
<br>
|
||||
|
||||
## Auto-Stop function
|
||||
|
||||
When extruding or rewinding, it is often the case that a specific amount of meters must be wound onto a spool. The QiTech Pro Winder has an integrated filament length measurement. For example, after 100 m, it can automatically switch from "wind" to "pull" mode. In addition, the operator is alerted acoustically that the spool has finished winding.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
## Electronics / Power consumption
|
||||
|
||||
| Specification | Value |
|
||||
| --------------------- | -------------------------- |
|
||||
| Mains connection | 230V/50Hz or 110V/60Hz |
|
||||
| Ø Load in operation | 3A at 24V |
|
||||
| Ø Consumption in use | 72 Wh |
|
||||
|
||||
<br>
|
||||
|
||||
@@ -55,7 +139,7 @@ Darmstadt, 10/06/2025
|
||||
|
||||
64392 Darmstadt,
|
||||
|
||||
<img src="images/manuals/winder/signature.jpeg" width="300" height="60"/>
|
||||
<img src="/images/manuals/winder/signature.jpeg" width="300" height="60"/>
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
@@ -101,7 +185,7 @@ Operating instructions
|
||||
|
||||
# 1 Machine description
|
||||
|
||||

|
||||

|
||||
|
||||
| Item number | Item number Description |
|
||||
| ----------- | ----------------------------------------- |
|
||||
@@ -186,7 +270,7 @@ Accuracy class: 2
|
||||
|
||||
| Picture | Danger! |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -194,17 +278,17 @@ Accuracy class: 2
|
||||
|
||||
| Pictogram | Description of pictogram | Pictogram | Pictogram Description |
|
||||
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -212,15 +296,15 @@ Accuracy class: 2
|
||||
|
||||
| Prohibition signs Pictogram | Pictogram description | International pictograms / other Pictogram | Pictogram Description |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="images/manuals/winder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="images/manuals/winder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="images/manuals/winder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="images/manuals/winder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="images/manuals/winder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="images/manuals/winder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="images/manuals/winder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="images/manuals/winder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="images/manuals/winder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="images/manuals/winder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="images/manuals/winder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="images/manuals/winder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="images/manuals/winder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="images/manuals/winder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="images/manuals/winder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
| <img src="/images/manuals/winder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="/images/manuals/winder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="/images/manuals/winder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="/images/manuals/winder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="/images/manuals/winder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="/images/manuals/winder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="/images/manuals/winder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="/images/manuals/winder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="/images/manuals/winder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="/images/manuals/winder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="/images/manuals/winder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="/images/manuals/winder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="/images/manuals/winder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="/images/manuals/winder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="/images/manuals/winder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="/images/manuals/winder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -258,7 +342,7 @@ In addition, the following procedures must be regarded as misuse:
|
||||
|
||||
<br><br><br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -286,7 +370,7 @@ Hazardous movements are brought to a controlled standstill as quickly as possibl
|
||||
|
||||
<br>
|
||||
|
||||
<img src="images/manuals/winder/emergency_stop.jpeg" width="300" height="300"/>
|
||||
<img src="/images/manuals/winder/emergency_stop.jpeg" width="300" height="300"/>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -343,8 +427,8 @@ Depending on the activity or the duration of the activity, the operator must det
|
||||
<br><br>
|
||||
| Picture | Danger |
|
||||
|---------|--------|
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to incorrect installation and commissioning!** <br> Errors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves. |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to incorrect installation and commissioning!** <br> Errors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -364,7 +448,7 @@ The machines are delivered with the newest version of the software installed. Up
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -412,7 +496,7 @@ Then start the machine as follows:
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -435,7 +519,7 @@ The **padding** parameter creates a safety margin that prevents the filament fro
|
||||
|
||||
The default padding is 0.88mm, but you can adjust this value in the Control settings based on your filament diameter and winding requirements. Larger filaments may require more padding, while smaller filaments can use less padding for maximum spool utilization.
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -448,7 +532,7 @@ The default padding is 0.88mm, but you can adjust this value in the Control sett
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -514,7 +598,7 @@ All settings such as the position of the spool sides, the winding speed or the f
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to operation of the machine in faulty condition** <br> If safety-relevant changes or defects occur on the machine, shut down immediately, secure against restarting and report the process to the responsible office/person! |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to operation of the machine in faulty condition** <br> If safety-relevant changes or defects occur on the machine, shut down immediately, secure against restarting and report the process to the responsible office/person! |
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -558,14 +642,14 @@ The polyurethane coating of the puller rollers will inevitably wear out over tim
|
||||
|
||||
Now remove the black plastic caps and simply pull out the puller rollers. To install the new rollers back on, slide them on the axes, put the plastic cap back on and screw the screws.
|
||||
|
||||
<img src="images/manuals/winder/swapping_out_rollers.jpeg" width="300" height="400"/>
|
||||
<img src="/images/manuals/winder/swapping_out_rollers.jpeg" width="300" height="400"/>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | Observe the instructions for maintenance work when servicing the machine! |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | Observe the instructions for maintenance work when servicing the machine! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -630,7 +714,7 @@ The machine consists largely of aluminum and plastic and can be easily separated
|
||||
|
||||
| Picture | Disposal Instructions |
|
||||
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/trashcan.jpeg" width="150" height="150"/> | Appliances with this logo on the packaging or on the appliance must be disposed of separately. These devices must not be disposed of with normal household waste. You are responsible for ensuring that any electrical or electronic waste is disposed of at the appropriate locations, e.g., recycling centers. |
|
||||
| <img src="/images/manuals/winder/trashcan.jpeg" width="150" height="150"/> | Appliances with this logo on the packaging or on the appliance must be disposed of separately. These devices must not be disposed of with normal household waste. You are responsible for ensuring that any electrical or electronic waste is disposed of at the appropriate locations, e.g., recycling centers. |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -638,15 +722,15 @@ The machine consists largely of aluminum and plastic and can be easily separated
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The winder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the winder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="images/manuals/winder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="images/manuals/winder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The winder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="/images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the winder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="/images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="/images/manuals/winder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="/images/manuals/winder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="/images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="/images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
|
||||
# 4 Glossary and Definitions
|
||||
|
||||
@@ -687,7 +771,7 @@ The following persons may not work on the machine:
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury if insufficiently qualified!** <br> Improper handling can lead to considerable personal injury and damage to property. Special activities may only be carried out by the persons specified in the relevant sections of these instructions. |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury if insufficiently qualified!** <br> Improper handling can lead to considerable personal injury and damage to property. Special activities may only be carried out by the persons specified in the relevant sections of these instructions. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -716,12 +800,12 @@ The following persons may not work on the machine:
|
||||
|
||||
## Electrical Diagrams
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
@@ -73,7 +73,7 @@ typings/
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
# Serverless directoriesProcessMetrics
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
@@ -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: 72 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 387 KiB |
@@ -9,6 +9,7 @@ import { RouterProvider } from "@tanstack/react-router";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { enableMapSet } from "immer";
|
||||
import { useGlobalLogStreaming } from "./hooks/useGlobalLogStreaming";
|
||||
import { GlobalLaserToastManager } from "./setup/GlobalLaserToastManager";
|
||||
|
||||
export default function App() {
|
||||
const { i18n } = useTranslation();
|
||||
@@ -21,7 +22,12 @@ export default function App() {
|
||||
updateAppLanguage(i18n);
|
||||
}, [i18n]);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
return (
|
||||
<>
|
||||
<GlobalLaserToastManager />
|
||||
<RouterProvider router={router} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// enable immer MapSet plugin
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { Markdown } from "./Markdown";
|
||||
|
||||
type ManualMarkdownProps = {
|
||||
/**
|
||||
* The markdown content to render
|
||||
*/
|
||||
markdownContent: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A specialized Markdown component for rendering machine manuals.
|
||||
*
|
||||
* This component transforms image paths in markdown to work correctly in the Electron app.
|
||||
* Manual markdown files are stored in `docs/machines/manuals/` with relative image paths
|
||||
* pointing to `../../../../electron/public/images/manuals/[machine-name]/...`
|
||||
*
|
||||
* This component transforms those paths to absolute paths `/images/manuals/[machine-name]/...`
|
||||
* that work correctly in the Electron app's context.
|
||||
*
|
||||
* Path transformation examples:
|
||||
* - `../../../../electron/public/images/manuals/winder/image.png` → `/images/manuals/winder/image.png`
|
||||
* - `images/manuals/winder/image.png` → `/images/manuals/winder/image.png`
|
||||
*/
|
||||
export function ManualMarkdown({ markdownContent }: ManualMarkdownProps) {
|
||||
const transformedContent = React.useMemo(() => {
|
||||
// Transform image paths to work in Electron context
|
||||
// Pattern 1: ../../../../electron/public/images/manuals/... → /images/manuals/...
|
||||
let content = markdownContent.replace(
|
||||
/!\[([^\]]*)\]\(\.\.\/\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^)]+)\)/g,
|
||||
"",
|
||||
);
|
||||
|
||||
// Pattern 2: ../../../electron/public/images/manuals/... → /images/manuals/... (backward compatibility)
|
||||
content = content.replace(
|
||||
/!\[([^\]]*)\]\(\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^)]+)\)/g,
|
||||
"",
|
||||
);
|
||||
|
||||
// Pattern 3: images/manuals/... → /images/manuals/... (for backward compatibility)
|
||||
content = content.replace(
|
||||
/!\[([^\]]*)\]\(images\/manuals\/([^)]+)\)/g,
|
||||
"",
|
||||
);
|
||||
|
||||
// Pattern 4: Handle <img> tags with src attribute (new path)
|
||||
content = content.replace(
|
||||
/<img\s+([^>]*)src="\.\.\/\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^"]+)"/g,
|
||||
'<img $1src="/$2"',
|
||||
);
|
||||
|
||||
// Pattern 5: Handle <img> tags with src attribute (old path)
|
||||
content = content.replace(
|
||||
/<img\s+([^>]*)src="\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^"]+)"/g,
|
||||
'<img $1src="/$2"',
|
||||
);
|
||||
|
||||
content = content.replace(
|
||||
/<img\s+([^>]*)src="images\/manuals\/([^"]+)"/g,
|
||||
'<img $1src="/images/manuals/$2"',
|
||||
);
|
||||
|
||||
return content;
|
||||
}, [markdownContent]);
|
||||
|
||||
return <Markdown text={transformedContent} />;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Markdown } from "./Markdown";
|
||||
import { ManualMarkdown } from "./ManualMarkdown";
|
||||
import { MarkdownToc } from "./MarkdownToc";
|
||||
|
||||
type MarkdownWithTocProps = {
|
||||
@@ -26,6 +27,11 @@ type MarkdownWithTocProps = {
|
||||
* @default "top-4"
|
||||
*/
|
||||
stickyTop?: string;
|
||||
/**
|
||||
* Whether this is a machine manual (enables image path transformation)
|
||||
* @default false
|
||||
*/
|
||||
isManual?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -44,12 +50,19 @@ export function MarkdownWithToc({
|
||||
tocWidth = "w-64",
|
||||
gap = "gap-6",
|
||||
stickyTop = "top-6",
|
||||
isManual = false,
|
||||
}: MarkdownWithTocProps) {
|
||||
const MarkdownComponent = isManual ? ManualMarkdown : Markdown;
|
||||
|
||||
return (
|
||||
<div className={`flex ${gap}`}>
|
||||
{/* Main Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Markdown text={markdownContent} />
|
||||
{isManual ? (
|
||||
<ManualMarkdown markdownContent={markdownContent} />
|
||||
) : (
|
||||
<Markdown text={markdownContent} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table of Contents Sidebar */}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { OutsideCorner } from "@/components/OutsideCorner";
|
||||
import { useMachines } from "@/client/useMachines";
|
||||
import { useOnSubpath } from "@/lib/useOnSubpath";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from "@tanstack/react-router";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { Icon, IconName } from "./Icon";
|
||||
import { useMainNamespace } from "@/client/mainNamespace";
|
||||
|
||||
type SidebarItemContent = {
|
||||
link: string;
|
||||
@@ -57,7 +63,51 @@ export function useSidebarlessWidth() {
|
||||
|
||||
export function SidebarLayout() {
|
||||
const machines = useMachines();
|
||||
const router = useRouter();
|
||||
const routerState = useRouterState();
|
||||
const { machines: machinesEvent } = useMainNamespace();
|
||||
const [contentWidth, setContentWidth] = React.useState<number>(0);
|
||||
|
||||
// Machine connection guard: redirect to setup if selected machine disconnects
|
||||
useEffect(() => {
|
||||
// Wait for machines data to be loaded
|
||||
if (!machinesEvent?.data) return;
|
||||
|
||||
// Extract serial from current path (e.g., /machines/laser1/12345/control)
|
||||
const pathMatch = routerState.location.pathname.match(
|
||||
/\/machines\/[^/]+\/(\d+)/,
|
||||
);
|
||||
if (!pathMatch) return; // Not on a machine page
|
||||
|
||||
// Safety check: ensure pathMatch has at least 2 elements (full match + capture group)
|
||||
if (pathMatch.length < 2) {
|
||||
console.warn("[ConnectionGuard] Invalid pathMatch structure:", pathMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse serial number with error handling
|
||||
const serialString = pathMatch[1];
|
||||
const serialNumber = parseInt(serialString, 10);
|
||||
|
||||
// Check if parseInt succeeded (NaN means parsing failed)
|
||||
if (isNaN(serialNumber)) {
|
||||
console.warn(
|
||||
`[ConnectionGuard] Failed to parse serial number from "${serialString}"`,
|
||||
{ pathname: routerState.location.pathname },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if machine still exists in the raw machines data
|
||||
const machineExists = machinesEvent.data.machines.some(
|
||||
(m) => m.machine_identification_unique.serial === serialNumber,
|
||||
);
|
||||
|
||||
if (!machineExists) {
|
||||
router.navigate({ to: "/_sidebar/setup/machines" });
|
||||
}
|
||||
}, [machines, machinesEvent, routerState.location.pathname, router]);
|
||||
|
||||
const items: SidebarItemContent[] = [
|
||||
...machines.map((machine) => ({
|
||||
link: `/_sidebar/machines/${machine.slug}/${machine.machine_identification_unique.serial}/control`,
|
||||
|
||||
@@ -63,7 +63,11 @@ export type DataSeries = SeriesData | SeriesData[];
|
||||
export type BigGraphProps = {
|
||||
newData: DataSeries;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
renderValue?: (
|
||||
value: number,
|
||||
seriesIndex?: number,
|
||||
seriesTitle?: string,
|
||||
) => string;
|
||||
config: GraphConfig;
|
||||
graphId: string;
|
||||
syncGraph?: PropGraphSync;
|
||||
|
||||
@@ -25,6 +25,8 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
return "lu:Gauge";
|
||||
case "mHz":
|
||||
return "lu:AudioWaveform";
|
||||
case "Hz":
|
||||
return "lu:AudioWaveform";
|
||||
case "W":
|
||||
return "lu:Zap";
|
||||
case "V":
|
||||
@@ -35,6 +37,18 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
return "lu:BatteryFull";
|
||||
case "l/min":
|
||||
return "lu:Waves";
|
||||
case "%":
|
||||
return "lu:ChartNoAxesColumn";
|
||||
case "µs":
|
||||
return "lu:RefreshCcw";
|
||||
case "s":
|
||||
return "lu:Clock";
|
||||
case "MiB":
|
||||
return "lu:MemoryStick";
|
||||
case "Mbit/s":
|
||||
return "lu:Network";
|
||||
case "/s":
|
||||
return "lu:Gauge";
|
||||
default:
|
||||
return "lu:ChartNoAxesColumn";
|
||||
}
|
||||
@@ -62,6 +76,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "m/min";
|
||||
case "mHz":
|
||||
return "mHz";
|
||||
case "Hz":
|
||||
return "Hz";
|
||||
case "W":
|
||||
return "W";
|
||||
case "V":
|
||||
@@ -72,6 +88,18 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "kWh";
|
||||
case "l/min":
|
||||
return "l/min";
|
||||
case "%":
|
||||
return "%";
|
||||
case "µs":
|
||||
return "µs";
|
||||
case "s":
|
||||
return "s";
|
||||
case "MiB":
|
||||
return "MiB";
|
||||
case "Mbit/s":
|
||||
return "Mbit/s";
|
||||
case "/s":
|
||||
return "/s";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -121,6 +149,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "meters/minute";
|
||||
case "mHz":
|
||||
return "millihertz";
|
||||
case "Hz":
|
||||
return "hertz";
|
||||
case "W":
|
||||
return "watts";
|
||||
case "V":
|
||||
@@ -131,6 +161,18 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "kilowatt-hours";
|
||||
case "l/min":
|
||||
return "liters/minute";
|
||||
case "%":
|
||||
return "percent";
|
||||
case "µs":
|
||||
return "microseconds";
|
||||
case "s":
|
||||
return "seconds";
|
||||
case "MiB":
|
||||
return "mebibytes";
|
||||
case "Mbit/s":
|
||||
return "megabits/second";
|
||||
case "/s":
|
||||
return "per second";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -147,11 +189,18 @@ export const units = [
|
||||
"bar",
|
||||
"m/min",
|
||||
"mHz",
|
||||
"Hz",
|
||||
"W",
|
||||
"V",
|
||||
"A",
|
||||
"kWh",
|
||||
"l/min",
|
||||
"%",
|
||||
"µs",
|
||||
"s",
|
||||
"MiB",
|
||||
"Mbit/s",
|
||||
"/s",
|
||||
] as const;
|
||||
|
||||
export type Unit = (typeof units)[number];
|
||||
|
||||
@@ -38,6 +38,28 @@ export interface TimeSeriesWithInsert {
|
||||
insert: (series: TimeSeries, valueObj: TimeSeriesValue) => TimeSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for creating a time series
|
||||
*/
|
||||
export interface TimeSeriesConfig {
|
||||
sampleIntervalShort: number;
|
||||
sampleIntervalLong: number;
|
||||
retentionDurationShort: number;
|
||||
retentionDurationLong: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration values
|
||||
* Short: 20ms sample, 5s retention
|
||||
* Long: 1s sample, 1h retention
|
||||
*/
|
||||
export const DEFAULT_TIMESERIES_CONFIG: TimeSeriesConfig = {
|
||||
sampleIntervalShort: 20, // 20ms
|
||||
sampleIntervalLong: 1000, // 1s
|
||||
retentionDurationShort: 5000, // 5s
|
||||
retentionDurationLong: 60 * 60 * 1000, // 1h
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract data with time window filtering in chronological order
|
||||
*/
|
||||
@@ -177,13 +199,18 @@ export function seriesToUPlotData(
|
||||
|
||||
/**
|
||||
* Factory function to create a new time series with circular buffers
|
||||
* @param config Optional configuration object. If omitted or partial, defaults from DEFAULT_TIMESERIES_CONFIG are used.
|
||||
*/
|
||||
export const createTimeSeries = (
|
||||
sampleIntervalShort: number,
|
||||
sampleIntervalLong: number,
|
||||
retentionDurationShort: number,
|
||||
retentionDurationLong: number,
|
||||
config: Partial<TimeSeriesConfig> = {},
|
||||
): TimeSeriesWithInsert => {
|
||||
const {
|
||||
sampleIntervalShort,
|
||||
sampleIntervalLong,
|
||||
retentionDurationShort,
|
||||
retentionDurationLong,
|
||||
} = { ...DEFAULT_TIMESERIES_CONFIG, ...config };
|
||||
|
||||
const shortSize = Math.ceil(retentionDurationShort / sampleIntervalShort);
|
||||
const longSize = Math.ceil(retentionDurationLong / sampleIntervalLong);
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MiniGraph } from "@/components/graph/MiniGraph";
|
||||
import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
|
||||
import React from "react";
|
||||
import { useAnalogInputTestMachine } from "./useAnalogInputTestMachine";
|
||||
|
||||
export function AnalogInputTestMachineControl(): React.JSX.Element {
|
||||
const { seriesData, state, updateMeasurementRate } =
|
||||
useAnalogInputTestMachine();
|
||||
|
||||
if (!seriesData) return <div>Initializing Sensor...</div>;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Measurement Control">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div>Measurement Rate (Hz)</div>
|
||||
<input
|
||||
type="number"
|
||||
style={{
|
||||
border: "2px solid transparent",
|
||||
borderRadius: "4px",
|
||||
padding: "3px",
|
||||
}}
|
||||
className={"bg-neutral-100 shadow"}
|
||||
value={state.measurementRateHz}
|
||||
onChange={(v) => {
|
||||
const val = Number(v.currentTarget.value);
|
||||
updateMeasurementRate(val);
|
||||
}}
|
||||
min={1}
|
||||
></input>
|
||||
</div>
|
||||
</ControlCard>
|
||||
<ControlCard title="Results">
|
||||
<div className="flex flex-row">
|
||||
<MiniGraph
|
||||
newData={seriesData}
|
||||
width={500}
|
||||
renderValue={(v) => v.toFixed(2)}
|
||||
></MiniGraph>
|
||||
<div className="flex flex-col justify-center">
|
||||
{seriesData.current?.value.toFixed(2)}mA
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { analogInputTestMachineSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function AnalogInputTestMachine(): React.JSX.Element {
|
||||
const { serial } = analogInputTestMachineSerialRoute.useParams();
|
||||
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/analoginputtestmachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import {
|
||||
createTimeSeries,
|
||||
TimeSeries,
|
||||
TimeSeriesValue,
|
||||
TimeSeriesWithInsert,
|
||||
} from "@/lib/timeseries";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import z from "zod";
|
||||
import {
|
||||
StateEvent,
|
||||
useAnalogInputTestMachineNamespace,
|
||||
} from "./useAnalogInputTestMachineNamespace";
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { MachineIdentificationUnique } from "../types";
|
||||
import { analogInputTestMachineSerialRoute } from "@/routes/routes";
|
||||
import { analogInputTestMachine } from "../properties";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { produce } from "immer";
|
||||
|
||||
export function useAnalogInputTestMachine() {
|
||||
const { serial: serialString } =
|
||||
analogInputTestMachineSerialRoute.useParams();
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString);
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
return {
|
||||
machine_identification: { vendor: 0, machine: 0 },
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
machine_identification: analogInputTestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
const state = useAnalogInputTestMachineNamespace({
|
||||
type: "machine",
|
||||
machine_identification_unique: machineIdentification,
|
||||
});
|
||||
|
||||
const mutateMachine = useMachineMutate(
|
||||
z.object({
|
||||
measurement_rate_hz: z.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest?: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState)
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
serverRequest?.();
|
||||
};
|
||||
|
||||
const updateMeasurementRate = (val: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.measurementRateHz = val;
|
||||
},
|
||||
() => {
|
||||
mutateMachine.request({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { measurement_rate_hz: val },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const timeSeries: TimeSeriesWithInsert = createTimeSeries({
|
||||
sampleIntervalShort: 20,
|
||||
sampleIntervalLong: 1000,
|
||||
retentionDurationShort: 5000,
|
||||
retentionDurationLong: 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
const [seriesData, setSeriesData] = useState<TimeSeries | null>(
|
||||
timeSeries.initialTimeSeries,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const measurement = state.currentMeasurement;
|
||||
|
||||
if (measurement && seriesData) {
|
||||
const dataPoint: TimeSeriesValue = {
|
||||
value: measurement[0] * 1000,
|
||||
timestamp: Number(measurement[1]),
|
||||
};
|
||||
setSeriesData(timeSeries.insert(seriesData, dataPoint));
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
seriesData,
|
||||
state,
|
||||
updateMeasurementRate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
createNamespaceHookImplementation,
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
} from "@/client/socketioStore";
|
||||
import z from "zod";
|
||||
import { create, StoreApi } from "zustand";
|
||||
|
||||
// ========== Event Schema ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
measurementRateHz: z.number().optional(),
|
||||
currentMeasurement: z.tuple([z.number(), z.string()]).optional(),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type AnalogInputTestMachineNamespaceStore = z.infer<
|
||||
typeof stateEventDataSchema
|
||||
>;
|
||||
|
||||
const createMachineStore = (): StoreApi<AnalogInputTestMachineNamespaceStore> =>
|
||||
create<AnalogInputTestMachineNamespaceStore>(() => ({
|
||||
measurementRateHz: 1,
|
||||
currentMeasurement: undefined,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
function analogInputTestMachineMessageHandler(
|
||||
store: StoreApi<AnalogInputTestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event) => {
|
||||
const oldState = store.getState();
|
||||
const newMeasurementDataHz = event.data["MeasurementRateHz"];
|
||||
const newMeasurement: [number, string] = event.data["Measurement"];
|
||||
|
||||
switch (event.name) {
|
||||
case "MeasurementRateHz":
|
||||
if (
|
||||
newMeasurementDataHz &&
|
||||
newMeasurementDataHz !== oldState.measurementRateHz
|
||||
)
|
||||
store.setState({
|
||||
...oldState,
|
||||
measurementRateHz: newMeasurementDataHz,
|
||||
});
|
||||
break;
|
||||
case "Measurement":
|
||||
if (newMeasurement && newMeasurement !== oldState.currentMeasurement)
|
||||
store.setState({
|
||||
...oldState,
|
||||
currentMeasurement: newMeasurement,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Namespace Hook ==========
|
||||
export const useAnalogInputTestMachineNamespace =
|
||||
createNamespaceHookImplementation({
|
||||
createStore: () => createMachineStore(),
|
||||
createEventHandler: analogInputTestMachineMessageHandler,
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export function Aquapath1ControlPage() {
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front Flow"
|
||||
label="Flow 1"
|
||||
unit="l/min"
|
||||
timeseries={front_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
@@ -48,7 +48,7 @@ export function Aquapath1ControlPage() {
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back Flow"
|
||||
label="Flow 2"
|
||||
unit="l/min"
|
||||
timeseries={back_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
@@ -57,7 +57,7 @@ export function Aquapath1ControlPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Label label="Front Pump">
|
||||
<Label label="Pump 1">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={frontTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
@@ -86,7 +86,7 @@ export function Aquapath1ControlPage() {
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Back Pump">
|
||||
<Label label="Pump 2">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={backTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
@@ -121,22 +121,19 @@ export function Aquapath1ControlPage() {
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front in"
|
||||
label="Reservoir 1"
|
||||
unit="C"
|
||||
timeseries={front_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front Reservoir"
|
||||
unit="C"
|
||||
timeseries={front_temp_reservoir}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Front Target Temperature">
|
||||
<Label label="Set Reservoir 1 Target Temperature">
|
||||
<EditValue
|
||||
title="Set Front Target Temperature"
|
||||
title="Set Reservoir 1 Target Temperature"
|
||||
value={frontTargetTemperature}
|
||||
unit="C"
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
max={80.0}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontTemperature(val);
|
||||
@@ -150,22 +147,19 @@ export function Aquapath1ControlPage() {
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back in"
|
||||
label="Reservoir 2"
|
||||
unit="C"
|
||||
timeseries={back_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back Reservoir"
|
||||
unit="C"
|
||||
timeseries={back_temp_reservoir}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Back Target Temperature">
|
||||
<Label label="Set Reservoir 2 Target Temperature">
|
||||
<EditValue
|
||||
title="Set Back Target Temperature"
|
||||
title="Set Reservoir 2 Target Temperature"
|
||||
value={backTargetTemperature}
|
||||
unit="C"
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
max={80.0}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackTemperature(val);
|
||||
|
||||
@@ -98,37 +98,17 @@ export type Aquapath1NamespaceStore = {
|
||||
back_temp_reservoir: 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: front_temperature, insert: addTemperature1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_temperature, insert: addTemperature2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_temp_reservoir, insert: addTempReserv1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries();
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries();
|
||||
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
/**
|
||||
* Factory function to create a new Aquapath namespace store
|
||||
* @returns A new Zustand store instance for Aquapath namespace
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import extruderManualContent from "@/assets/markdown/extruder/manual.md?raw";
|
||||
import extruderManualContent from "@root/docs/machines/manuals/extruder.md?raw";
|
||||
|
||||
export function ExtruderV2ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={extruderManualContent} />
|
||||
<MarkdownWithToc
|
||||
markdownContent={extruderManualContent}
|
||||
isManual={true}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -223,64 +223,35 @@ export type Extruder2NamespaceStore = {
|
||||
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: pressure, insert: addPressure } = createTimeSeries();
|
||||
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontPower, insert: addFrontPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
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,
|
||||
);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: backPower, insert: addBackPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorPower, insert: addMotorPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
|
||||
export function extruder2MessageHandler(
|
||||
store: StoreApi<Extruder2NamespaceStore>,
|
||||
|
||||
@@ -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 "@root/docs/machines/manuals/extruder.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,404 @@
|
||||
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;
|
||||
};
|
||||
|
||||
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
|
||||
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontPower, insert: addFrontPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middlePower, insert: addMiddlePower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: backPower, insert: addBackPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorPower, insert: addMotorPower } =
|
||||
createTimeSeries();
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Label } from "@/control/Label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useIP20TestMachine } from "./useIP20TestMachine";
|
||||
|
||||
export function IP20TestMachineControlPage() {
|
||||
const { state, liveValues, setOutput, setAllOutputs } = useIP20TestMachine();
|
||||
|
||||
const safeState = state ?? {
|
||||
outputs: [false, false, false, false, false, false, false, false],
|
||||
};
|
||||
const safeLiveValues = liveValues ?? {
|
||||
inputs: [false, false, false, false, false, false, false, false],
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
{/* Digital Output Controls */}
|
||||
<ControlCard title="Digital Outputs">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{safeState.outputs.map((output, index) => (
|
||||
<Label key={index} label={`Output ${index + 1}`}>
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={output ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-red-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => setOutput(index, value === "On")}
|
||||
/>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
{/* Digital Input Display */}
|
||||
<ControlCard title="Digital Inputs">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{safeLiveValues.inputs.map((input, index) => (
|
||||
<Label key={index} label={`Input ${index + 1}`}>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Badge
|
||||
className={`text-md ${input ? "bg-green-600" : "bg-gray-400"}`}
|
||||
>
|
||||
{input ? "HIGH" : "LOW"}
|
||||
</Badge>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
{/* Master Output Control */}
|
||||
<ControlCard title="Master Output Control">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={safeState.outputs.every(Boolean) ? "On" : "Off"}
|
||||
orientation="horizontal"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Turn All Outputs Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-red-600",
|
||||
},
|
||||
On: {
|
||||
children: "Turn All Outputs On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => setAllOutputs(value === "On")}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
{/* Input Summary */}
|
||||
<ControlCard title="Input Summary">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Active Inputs:</span>
|
||||
<Badge className="bg-blue-500">
|
||||
{safeLiveValues.inputs.filter(Boolean).length} / 8
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Inactive Inputs:</span>
|
||||
<Badge className="bg-gray-500">
|
||||
{safeLiveValues.inputs.filter((i) => !i).length} / 8
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { ip20TestMachineSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function IP20TestMachinePage(): React.JSX.Element {
|
||||
const { serial } = ip20TestMachineSerialRoute.useParams();
|
||||
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/ip20testmachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:ToggleLeft",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "@/client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
|
||||
// ========== Event Schema ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
outputs: z.array(z.boolean()).length(8),
|
||||
});
|
||||
|
||||
export const liveValuesEventDataSchema = z.object({
|
||||
inputs: z.array(z.boolean()).length(8),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
export type LiveValuesEvent = z.infer<typeof liveValuesEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type IP20TestMachineNamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
liveValues: LiveValuesEvent | null;
|
||||
};
|
||||
|
||||
export const createIP20TestMachineNamespaceStore =
|
||||
(): StoreApi<IP20TestMachineNamespaceStore> =>
|
||||
create<IP20TestMachineNamespaceStore>(() => ({
|
||||
state: null,
|
||||
liveValues: null,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
export function ip20TestMachineMessageHandler(
|
||||
store: StoreApi<IP20TestMachineNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<IP20TestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const updateStore = (
|
||||
updater: (
|
||||
state: IP20TestMachineNamespaceStore,
|
||||
) => IP20TestMachineNamespaceStore,
|
||||
) => throttledUpdater.updateWith(updater);
|
||||
|
||||
try {
|
||||
if (event.name === "StateEvent") {
|
||||
const parsed = stateEventSchema.parse(event);
|
||||
updateStore((current) => ({ ...current, state: parsed.data }));
|
||||
} else if (event.name === "LiveValuesEvent") {
|
||||
const parsed = liveValuesEventSchema.parse(event);
|
||||
updateStore((current) => ({ ...current, liveValues: parsed.data }));
|
||||
} else {
|
||||
handleUnhandledEventError(event.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${event.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Namespace Hook ==========
|
||||
const useIP20TestMachineNamespaceImplementation =
|
||||
createNamespaceHookImplementation<IP20TestMachineNamespaceStore>({
|
||||
createStore: createIP20TestMachineNamespaceStore,
|
||||
createEventHandler: ip20TestMachineMessageHandler,
|
||||
});
|
||||
|
||||
export function useIP20TestMachineNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): IP20TestMachineNamespaceStore {
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
return useIP20TestMachineNamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { ip20TestMachineSerialRoute } from "@/routes/routes";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
useIP20TestMachineNamespace,
|
||||
StateEvent,
|
||||
LiveValuesEvent,
|
||||
} from "./ip20TestMachineNamespace";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { ip20TestMachine } from "@/machines/properties";
|
||||
import { z } from "zod";
|
||||
|
||||
export function useIP20TestMachine() {
|
||||
const { serial: serialString } = ip20TestMachineSerialRoute.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: ip20TestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Namespace state from backend
|
||||
const { state, liveValues } = useIP20TestMachineNamespace(
|
||||
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 setOutput = (index: number, on: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.outputs[index] = on;
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetOutput", value: { index, on } },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setAllOutputs = (on: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.outputs = [on, on, on, on, on, on, on, on];
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetAllOutputs", value: { on } },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value,
|
||||
liveValues,
|
||||
setOutput,
|
||||
setAllOutputs,
|
||||
};
|
||||
}
|
||||
@@ -79,8 +79,9 @@ export function Laser1ControlPage() {
|
||||
<div className="flex flex-row items-center gap-6">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Roundness"
|
||||
unit="%"
|
||||
timeseries={roundness}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
renderValue={(value) => (value * 100).toFixed(2)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -99,8 +100,9 @@ export function Laser1ControlPage() {
|
||||
{roundness?.current && (
|
||||
<MinMaxValue
|
||||
label="Roundness Range"
|
||||
unit="%"
|
||||
timeseries={roundness}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
renderValue={(value) => (value * 100).toFixed(2)}
|
||||
timeframe={timeframe}
|
||||
hideSelector
|
||||
/>
|
||||
|
||||
@@ -12,128 +12,143 @@ import { useLaser1 } from "./useLaser1";
|
||||
export function Laser1GraphsPage() {
|
||||
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
|
||||
|
||||
const syncHook = useGraphSync("diameter-group");
|
||||
const syncHook = useGraphSync("diameter-roundness-group");
|
||||
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
|
||||
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
|
||||
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
|
||||
|
||||
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
|
||||
|
||||
const diameterColor = "#3b82f6";
|
||||
const xDiameterColor = "#ef4444";
|
||||
const yDiameterColor = "#22c55e";
|
||||
const roundnessColor = "#eab308";
|
||||
// Convert roundness from ratio (0-1) to percentage (0-100)
|
||||
const roundnessPercent = React.useMemo(() => {
|
||||
if (!roundness) return null;
|
||||
|
||||
const config: GraphConfig = {
|
||||
title: "Diameter",
|
||||
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
|
||||
exportFilename: "diameter_data",
|
||||
const transformValue = (
|
||||
val: { value: number; timestamp: number } | null,
|
||||
) => {
|
||||
if (!val) return null;
|
||||
return { value: val.value * 100, timestamp: val.timestamp };
|
||||
};
|
||||
|
||||
const transformSeries = (series: any) => {
|
||||
if (!series || !series.values) return series;
|
||||
return {
|
||||
...series,
|
||||
values: series.values.map(transformValue),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
current: roundness.current ? transformValue(roundness.current) : null,
|
||||
long: transformSeries(roundness.long),
|
||||
short: transformSeries(roundness.short),
|
||||
};
|
||||
}, [roundness]);
|
||||
|
||||
const diameterColor = "#3b82f6"; // blue
|
||||
const xDiameterColor = "#ef4444"; // red
|
||||
const yDiameterColor = "#22c55e"; // green
|
||||
const roundnessColor = "#eab308"; // yellow
|
||||
|
||||
// config for all graphs
|
||||
const baseGraphConfig: GraphConfig = {
|
||||
title: "",
|
||||
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes Standard
|
||||
colors: {
|
||||
primary: diameterColor,
|
||||
grid: "#e2e8f0",
|
||||
axis: "#64748b",
|
||||
background: "#ffffff",
|
||||
},
|
||||
};
|
||||
if (!isTwoAxis) {
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
// Diameter-Graph (Diameter, X-Diameter, Y-Diameter)
|
||||
const diameterGraphData = [
|
||||
{
|
||||
newData: diameter,
|
||||
color: diameterColor,
|
||||
title: "Diameter",
|
||||
lines: [
|
||||
// Tolerance for diameter
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetDiameter + higherTolerance,
|
||||
label: "Upper Tolerance",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetDiameter - lowerTolerance,
|
||||
label: "Lower Tolerance",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "target" as const,
|
||||
value: targetDiameter,
|
||||
label: "Target",
|
||||
color: diameterColor,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
...(isTwoAxis && x_diameter
|
||||
? [{ newData: x_diameter, color: xDiameterColor, title: "X-Diameter" }]
|
||||
: []),
|
||||
...(isTwoAxis && y_diameter
|
||||
? [{ newData: y_diameter, color: yDiameterColor, title: "Y-Diameter" }]
|
||||
: []),
|
||||
];
|
||||
|
||||
// Roundness-Graph
|
||||
const roundnessGraphData = {
|
||||
newData: roundnessPercent,
|
||||
color: roundnessColor,
|
||||
title: "Roundness (%)",
|
||||
};
|
||||
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Graph 1: Diameter */}
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={diameterGraphData}
|
||||
unit="mm"
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
config={{
|
||||
...baseGraphConfig,
|
||||
title: "Diameter (mm)",
|
||||
exportFilename: "diameter_data",
|
||||
colors: {
|
||||
...baseGraphConfig.colors,
|
||||
primary: diameterColor,
|
||||
},
|
||||
}}
|
||||
graphId="diameter-graph"
|
||||
/>
|
||||
|
||||
{/* Graph 2: Roundness */}
|
||||
{isTwoAxis && roundnessPercent && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: diameter,
|
||||
color: diameterColor,
|
||||
lines: [
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter + higherTolerance,
|
||||
label: "Upper Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter - lowerTolerance,
|
||||
label: "Lower Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "target",
|
||||
value: targetDiameter,
|
||||
label: "Target",
|
||||
color: diameterColor,
|
||||
},
|
||||
],
|
||||
newData={roundnessGraphData}
|
||||
unit="%"
|
||||
renderValue={(value) => value.toFixed(2)}
|
||||
config={{
|
||||
...baseGraphConfig,
|
||||
title: "Roundness (%)",
|
||||
exportFilename: "roundness_data",
|
||||
colors: {
|
||||
...baseGraphConfig.colors,
|
||||
primary: roundnessColor,
|
||||
},
|
||||
}}
|
||||
unit="mm"
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
config={config}
|
||||
graphId="diameter-graph"
|
||||
graphId="roundness-graph"
|
||||
/>
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={[
|
||||
{
|
||||
newData: diameter,
|
||||
color: diameterColor,
|
||||
title: "Diameter",
|
||||
lines: [
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter + higherTolerance,
|
||||
label: "Upper Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter - lowerTolerance,
|
||||
label: "Lower Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "target",
|
||||
value: targetDiameter,
|
||||
label: "Target",
|
||||
color: diameterColor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
newData: x_diameter,
|
||||
color: xDiameterColor,
|
||||
title: "X-Diameter",
|
||||
},
|
||||
{
|
||||
newData: y_diameter,
|
||||
color: yDiameterColor,
|
||||
title: "Y-Diameter",
|
||||
},
|
||||
{
|
||||
newData: roundness,
|
||||
color: roundnessColor,
|
||||
title: "Roundness",
|
||||
},
|
||||
]}
|
||||
unit="mm"
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
config={config}
|
||||
graphId="diameter-graph"
|
||||
/>
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import laserManualContent from "@/assets/markdown/laser/manual.md?raw";
|
||||
import laserManualContent from "@root/docs/machines/manuals/laser.md?raw";
|
||||
|
||||
export function Laser1ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={laserManualContent} />
|
||||
<MarkdownWithToc markdownContent={laserManualContent} isManual={true} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,12 @@ export function Laser1Page() {
|
||||
icon: "lu:ChartSpline",
|
||||
},
|
||||
// {
|
||||
// link: "settings",
|
||||
// activeLink: "settings",
|
||||
// title: "Config",
|
||||
// icon: "lu:Settings",
|
||||
// },
|
||||
// {
|
||||
// link: "manual",
|
||||
// activeLink: "manual",
|
||||
// title: "Manual",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const stateEventDataSchema = z.object({
|
||||
higher_tolerance: z.number(),
|
||||
lower_tolerance: z.number(),
|
||||
target_diameter: z.number(),
|
||||
in_tolerance: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -66,28 +67,17 @@ export type Laser1NamespaceStore = {
|
||||
};
|
||||
|
||||
// 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: diameter, insert: addDiameter } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const HALF_SECOND = 500;
|
||||
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries({
|
||||
sampleIntervalLong: HALF_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
{ sampleIntervalLong: HALF_SECOND },
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -102,7 +92,7 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
|
||||
diameter: diameter,
|
||||
x_diameter,
|
||||
y_diameter,
|
||||
roundness: roundness,
|
||||
roundness,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import winderManualContent from "@/assets/markdown/winder/manual.md?raw";
|
||||
import mockManualContent from "@root/docs/machines/manuals/mock.md?raw";
|
||||
|
||||
export function Mock1ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={winderManualContent} />
|
||||
<MarkdownWithToc markdownContent={mockManualContent} isManual={true} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -78,18 +78,7 @@ export type Mock1NamespaceStore = {
|
||||
sineWave3: 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: sineWave, insert: addSineWave } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries();
|
||||
|
||||
/**
|
||||
* Factory function to create a new Mock1 namespace store
|
||||
|
||||
@@ -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",
|
||||
@@ -340,13 +428,111 @@ 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 analogInputTestMachine: MachineProperties = {
|
||||
name: "AnalogTest",
|
||||
version: "V1",
|
||||
slug: "analogInputTestMachine",
|
||||
icon: "lu:Clock",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0035,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "Bus Coupler",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x44c2c52,
|
||||
revision: 0x120000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 1,
|
||||
role_label: "EL3021",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xbcd3052,
|
||||
revision: 0x140000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ip20TestMachine: MachineProperties = {
|
||||
name: "IP20 Test",
|
||||
version: "V1",
|
||||
slug: "ip20testmachine",
|
||||
icon: "lu:ToggleLeft",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0034,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "IP20-EC-DI8-DO8",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 0x741,
|
||||
product_id: 0x117b6722,
|
||||
revision: 0x1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const machineProperties: MachineProperties[] = [
|
||||
winder2,
|
||||
extruder3,
|
||||
extruder2,
|
||||
laser1,
|
||||
mock1,
|
||||
buffer1,
|
||||
aquapath1,
|
||||
testmachine,
|
||||
analogInputTestMachine,
|
||||
ip20TestMachine,
|
||||
];
|
||||
|
||||
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,12 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import winderManualContent from "@/assets/markdown/winder/manual.md?raw";
|
||||
import winderManualContent from "@root/docs/machines/manuals/winder.md?raw";
|
||||
|
||||
export function Winder2ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={winderManualContent} />
|
||||
<MarkdownWithToc markdownContent={winderManualContent} isManual={true} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -218,26 +218,16 @@ export type Winder2NamespaceStore = {
|
||||
spoolProgress: 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;
|
||||
|
||||
//Store Factory and Message Handler -> no param, so default values
|
||||
const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries();
|
||||
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
|
||||
/**
|
||||
* Factory function to create a new Winder2 namespace store
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from "react";
|
||||
import { Page } from "@/components/Page";
|
||||
import { useRuntimeMetrics } from "./useRuntimeMetrics";
|
||||
import { SectionTitle } from "@/components/SectionTitle";
|
||||
import {
|
||||
renderValueToReactNode,
|
||||
renderUnitSymbol,
|
||||
getUnitIcon,
|
||||
type Unit,
|
||||
} from "@/control/units";
|
||||
import { Icon } from "@/components/Icon";
|
||||
|
||||
type MetricCardProps = {
|
||||
title: string;
|
||||
value: React.ReactNode;
|
||||
unit?: Unit;
|
||||
};
|
||||
|
||||
function renderMetricWithUnit(
|
||||
value: number | null | undefined,
|
||||
unit: Unit,
|
||||
digits = 1,
|
||||
) {
|
||||
if (value == null) {
|
||||
return "—";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderValueToReactNode(value, unit, (v) => v.toFixed(digits))}
|
||||
<span className="text-muted-foreground ml-1 text-xs">
|
||||
{renderUnitSymbol(unit)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({ title, value, unit }: MetricCardProps) {
|
||||
const iconName = unit ? getUnitIcon(unit) : "lu:StickyNote";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{iconName && (
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 text-slate-500">
|
||||
<Icon name={iconName} className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-sm font-medium text-slate-700">{title}</h2>
|
||||
</div>
|
||||
<span className="text-base font-semibold text-slate-900">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetricsControlPage() {
|
||||
// Global poller: immediate first fetch, then every 5 seconds
|
||||
const { sample, cpuPercent, preemptionRate } = useRuntimeMetrics(true, 5000);
|
||||
|
||||
const rssMb = sample != null ? sample.rss_bytes / (1024 * 1024) : null;
|
||||
|
||||
// jitter_*_ns is nanoseconds; divide by 1e3 to get microseconds (µs)
|
||||
const jitterMaxUs = sample != null ? sample.jitter_max_ns / 1e3 : null;
|
||||
|
||||
const rxMbit =
|
||||
sample != null ? (sample.rx_rate_bytes_per_sec * 8.0) / 1_000_000.0 : null;
|
||||
const txMbit =
|
||||
sample != null ? (sample.tx_rate_bytes_per_sec * 8.0) / 1_000_000.0 : null;
|
||||
|
||||
const cpuTimeSec = sample != null ? sample.cpu_time_seconds : null;
|
||||
const minorFaults = sample != null ? sample.minor_faults : null;
|
||||
const majorFaults = sample != null ? sample.major_faults : null;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="space-y-6">
|
||||
<SectionTitle title="Backend Metrics" />
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Loop jitter: max, in µs */}
|
||||
<MetricCard
|
||||
title="Max Loop Jitter"
|
||||
unit={"µs"}
|
||||
value={renderMetricWithUnit(jitterMaxUs, "µs")}
|
||||
/>
|
||||
|
||||
{/* CPU usage */}
|
||||
<MetricCard
|
||||
title="CPU Usage"
|
||||
unit={"%"}
|
||||
value={renderMetricWithUnit(cpuPercent, "%")}
|
||||
/>
|
||||
|
||||
{/* CPU time */}
|
||||
<MetricCard
|
||||
title="CPU Time"
|
||||
unit={"s"}
|
||||
value={renderMetricWithUnit(cpuTimeSec, "s")}
|
||||
/>
|
||||
|
||||
{/* Memory usage */}
|
||||
<MetricCard
|
||||
title="Memory Usage"
|
||||
unit={"MiB"}
|
||||
value={renderMetricWithUnit(rssMb, "MiB")}
|
||||
/>
|
||||
|
||||
{/* Total page faults (composite, no single unit) */}
|
||||
<MetricCard
|
||||
title="Total Page Faults"
|
||||
value={
|
||||
minorFaults != null || majorFaults != null
|
||||
? `${minorFaults ?? "–"} minor | ${majorFaults ?? "–"} major`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
|
||||
{/* EtherCAT throughput (RX | TX) */}
|
||||
<MetricCard
|
||||
title="EtherCAT Throughput (RX | TX)"
|
||||
unit={"Mbit/s"}
|
||||
value={
|
||||
rxMbit != null && txMbit != null ? (
|
||||
<span className="flex items-center gap-1">
|
||||
{renderMetricWithUnit(rxMbit, "Mbit/s")}
|
||||
<span className="text-muted-foreground mx-1 text-xs">|</span>
|
||||
{renderMetricWithUnit(txMbit, "Mbit/s")}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Preemptions per second */}
|
||||
<MetricCard
|
||||
title="Preemptions"
|
||||
unit={"/s"}
|
||||
value={
|
||||
preemptionRate != null
|
||||
? renderMetricWithUnit(preemptionRate, "/s")
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import React from "react";
|
||||
import { Page } from "@/components/Page";
|
||||
import {
|
||||
AutoSyncedBigGraph,
|
||||
SyncedFloatingControlPanel,
|
||||
useGraphSync,
|
||||
type GraphConfig,
|
||||
} from "@/components/graph";
|
||||
import { useRuntimeMetrics } from "./useRuntimeMetrics";
|
||||
import { useRuntimeMetricSeries } from "./runtimeSeries";
|
||||
import { getUnitIcon, type Unit } from "@/control/units";
|
||||
|
||||
export function MetricsGraphsPage() {
|
||||
// Start / keep the global poller alive
|
||||
useRuntimeMetrics(true, 5000);
|
||||
|
||||
// Shared series (fed by global poller)
|
||||
const series = useRuntimeMetricSeries();
|
||||
|
||||
const syncHook = useGraphSync("runtime-metrics");
|
||||
|
||||
const jitterUnit: Unit = "µs";
|
||||
const cpuUnit: Unit = "%";
|
||||
const memUnit: Unit = "MiB";
|
||||
const ioUnit: Unit = "Mbit/s";
|
||||
const preemptUnit: Unit = "/s";
|
||||
|
||||
const jitterConfig: GraphConfig = {
|
||||
title: "Average Loop Jitter",
|
||||
icon: getUnitIcon(jitterUnit),
|
||||
colors: {
|
||||
primary: "#f97316",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "runtime_jitter",
|
||||
};
|
||||
|
||||
const cpuConfig: GraphConfig = {
|
||||
title: "CPU Usage",
|
||||
icon: getUnitIcon(cpuUnit),
|
||||
colors: {
|
||||
primary: "#3b82f6",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "runtime_cpu",
|
||||
};
|
||||
|
||||
const memConfig: GraphConfig = {
|
||||
title: "Memory Usage",
|
||||
icon: getUnitIcon(memUnit),
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "runtime_memory",
|
||||
};
|
||||
|
||||
const ioConfig: GraphConfig = {
|
||||
title: "EtherCAT Throughput",
|
||||
icon: getUnitIcon(ioUnit),
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "runtime_io",
|
||||
showLegend: true,
|
||||
};
|
||||
|
||||
const preemptConfig: GraphConfig = {
|
||||
title: "Preemptions Per Second",
|
||||
icon: getUnitIcon(preemptUnit),
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "runtime_preemptions",
|
||||
};
|
||||
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
{series.jitter && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: series.jitter,
|
||||
color: "#f97316",
|
||||
}}
|
||||
unit={jitterUnit}
|
||||
renderValue={(v) => v.toFixed(1)}
|
||||
config={jitterConfig}
|
||||
graphId="runtime_jitter"
|
||||
/>
|
||||
)}
|
||||
|
||||
{series.cpu && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: series.cpu,
|
||||
color: "#3b82f6",
|
||||
}}
|
||||
unit={cpuUnit}
|
||||
renderValue={(v) => v.toFixed(1)}
|
||||
config={cpuConfig}
|
||||
graphId="runtime_cpu"
|
||||
/>
|
||||
)}
|
||||
|
||||
{series.mem && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: series.mem,
|
||||
color: "#10b981",
|
||||
}}
|
||||
unit={memUnit}
|
||||
renderValue={(v) => v.toFixed(1)}
|
||||
config={memConfig}
|
||||
graphId="runtime_memory"
|
||||
/>
|
||||
)}
|
||||
|
||||
{series.rx && series.tx && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={[
|
||||
{
|
||||
newData: series.rx,
|
||||
title: "RX",
|
||||
color: "#10b981",
|
||||
},
|
||||
{
|
||||
newData: series.tx,
|
||||
title: "TX",
|
||||
color: "#ef4444",
|
||||
},
|
||||
]}
|
||||
unit={ioUnit}
|
||||
renderValue={(v) => v.toFixed(2)}
|
||||
config={ioConfig}
|
||||
graphId="runtime_io"
|
||||
/>
|
||||
)}
|
||||
|
||||
{series.preempt && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: series.preempt,
|
||||
color: "#6366f1",
|
||||
}}
|
||||
unit={preemptUnit}
|
||||
renderValue={(v) => v.toFixed(1)}
|
||||
config={preemptConfig}
|
||||
graphId="runtime_preemptions"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
|
||||
import type { RuntimeMetricsSample } from "./useRuntimeMetrics";
|
||||
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
|
||||
// 5 s window, 1 s bucket, 1 h history
|
||||
const { initialTimeSeries: jitterInitial, insert: insertJitter } =
|
||||
createTimeSeries({ sampleIntervalShort: FIVE_SECOND });
|
||||
const { initialTimeSeries: cpuInitial, insert: insertCpu } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: memInitial, insert: insertMem } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: rxInitial, insert: insertRx } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: txInitial, insert: insertTx } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: preemptInitial, insert: insertPreempt } =
|
||||
createTimeSeries({ sampleIntervalShort: FIVE_SECOND });
|
||||
|
||||
export type RuntimeSeriesState = {
|
||||
jitter: TimeSeries | null;
|
||||
cpu: TimeSeries | null;
|
||||
mem: TimeSeries | null;
|
||||
rx: TimeSeries | null;
|
||||
tx: TimeSeries | null;
|
||||
preempt: TimeSeries | null;
|
||||
};
|
||||
|
||||
let baseTimestampMs: number | null = null;
|
||||
|
||||
let currentSeries: RuntimeSeriesState = {
|
||||
jitter: jitterInitial,
|
||||
cpu: cpuInitial,
|
||||
mem: memInitial,
|
||||
rx: rxInitial,
|
||||
tx: txInitial,
|
||||
preempt: preemptInitial,
|
||||
};
|
||||
|
||||
const subscribers = new Set<(s: RuntimeSeriesState) => void>();
|
||||
|
||||
function notifySubscribers() {
|
||||
for (const cb of subscribers) cb(currentSeries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push one sample into the shared time-series.
|
||||
* Always records; UI toggles only control what is displayed.
|
||||
*/
|
||||
export function pushRuntimeSample(
|
||||
sample: RuntimeMetricsSample,
|
||||
cpuPercent: number | null,
|
||||
preemptionRate: number | null,
|
||||
) {
|
||||
if (baseTimestampMs === null) {
|
||||
baseTimestampMs = sample.timestamp_ms;
|
||||
}
|
||||
const t = sample.timestamp_ms - baseTimestampMs; // ms since first sample
|
||||
|
||||
let { jitter, cpu, mem, rx, tx, preempt } = currentSeries;
|
||||
|
||||
// avg jitter in µs
|
||||
jitter = insertJitter(jitter ?? jitterInitial, {
|
||||
value: sample.jitter_avg_ns / 1e3,
|
||||
timestamp: t,
|
||||
});
|
||||
|
||||
if (cpuPercent != null) {
|
||||
cpu = insertCpu(cpu ?? cpuInitial, {
|
||||
value: cpuPercent,
|
||||
timestamp: t,
|
||||
});
|
||||
}
|
||||
|
||||
const rssMb = sample.rss_bytes / (1024 * 1024);
|
||||
mem = insertMem(mem ?? memInitial, {
|
||||
value: rssMb,
|
||||
timestamp: t,
|
||||
});
|
||||
|
||||
const rxMbit = (sample.rx_rate_bytes_per_sec * 8.0) / 1_000_000.0;
|
||||
const txMbit = (sample.tx_rate_bytes_per_sec * 8.0) / 1_000_000.0;
|
||||
|
||||
rx = insertRx(rx ?? rxInitial, {
|
||||
value: rxMbit,
|
||||
timestamp: t,
|
||||
});
|
||||
tx = insertTx(tx ?? txInitial, {
|
||||
value: txMbit,
|
||||
timestamp: t,
|
||||
});
|
||||
|
||||
if (preemptionRate != null) {
|
||||
preempt = insertPreempt(preempt ?? preemptInitial, {
|
||||
value: preemptionRate,
|
||||
timestamp: t,
|
||||
});
|
||||
}
|
||||
|
||||
currentSeries = { jitter, cpu, mem, rx, tx, preempt };
|
||||
notifySubscribers();
|
||||
}
|
||||
/**
|
||||
* Read shared time-series; history survives unmount/remount.
|
||||
*/
|
||||
export function useRuntimeMetricSeries(): RuntimeSeriesState {
|
||||
const [state, setState] = useState<RuntimeSeriesState>(currentSeries);
|
||||
|
||||
useEffect(() => {
|
||||
const cb = (s: RuntimeSeriesState) => setState(s);
|
||||
subscribers.add(cb);
|
||||
cb(currentSeries);
|
||||
return () => {
|
||||
subscribers.delete(cb);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { pushRuntimeSample } from "./runtimeSeries";
|
||||
|
||||
// Talk directly to the Rust API on port 3001.
|
||||
// If you later proxy via Vite/Electron, make this configurable.
|
||||
const API_BASE = "http://127.0.0.1:3001";
|
||||
const RUNTIME_URL = `${API_BASE}/api/v1/metrics/runtime/latest`;
|
||||
|
||||
export type RuntimeMetricsSample = {
|
||||
timestamp_ms: number;
|
||||
rss_bytes: number;
|
||||
cpu_time_seconds: number;
|
||||
minor_faults: number;
|
||||
major_faults: number;
|
||||
jitter_min_ns: number;
|
||||
jitter_avg_ns: number;
|
||||
jitter_max_ns: number;
|
||||
rx_rate_bytes_per_sec: number;
|
||||
tx_rate_bytes_per_sec: number;
|
||||
rt_loop_cpu_time_seconds?: number | null;
|
||||
rt_nr_switches: number | null;
|
||||
rt_nr_voluntary_switches: number | null;
|
||||
rt_nr_involuntary_switches: number | null;
|
||||
};
|
||||
|
||||
type MetricsState = {
|
||||
sample: RuntimeMetricsSample | null;
|
||||
cpuPercent: number | null;
|
||||
preemptionRate: number | null;
|
||||
};
|
||||
|
||||
// ---- shared module-level state (global poller) ----
|
||||
|
||||
let currentState: MetricsState = {
|
||||
sample: null,
|
||||
cpuPercent: null,
|
||||
preemptionRate: null,
|
||||
};
|
||||
|
||||
let lastSample: RuntimeMetricsSample | null = null;
|
||||
let pollIntervalMs = 5000;
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const subscribers = new Set<(s: MetricsState) => void>();
|
||||
|
||||
function notifySubscribers() {
|
||||
for (const cb of subscribers) cb(currentState);
|
||||
}
|
||||
|
||||
async function fetchOnce() {
|
||||
try {
|
||||
const res = await fetch(RUNTIME_URL);
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Runtime metrics fetch failed:",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const json = (await res.json()) as RuntimeMetricsSample | null;
|
||||
if (!json) {
|
||||
// backend returned null (no sample yet)
|
||||
return;
|
||||
}
|
||||
|
||||
const current = json;
|
||||
const last = lastSample;
|
||||
|
||||
let cpuPercent: number | null = null;
|
||||
let preemptionRate: number | null = null;
|
||||
|
||||
if (last) {
|
||||
const dtWallSec = (current.timestamp_ms - last.timestamp_ms) / 1000.0;
|
||||
if (dtWallSec > 0) {
|
||||
const dtCpu = current.cpu_time_seconds - last.cpu_time_seconds;
|
||||
cpuPercent = (dtCpu / dtWallSec) * 100.0;
|
||||
|
||||
if (
|
||||
current.rt_nr_involuntary_switches != null &&
|
||||
last.rt_nr_involuntary_switches != null
|
||||
) {
|
||||
const dtPreempt =
|
||||
current.rt_nr_involuntary_switches -
|
||||
last.rt_nr_involuntary_switches;
|
||||
preemptionRate = dtPreempt / dtWallSec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastSample = current;
|
||||
|
||||
currentState = {
|
||||
sample: current,
|
||||
cpuPercent,
|
||||
preemptionRate,
|
||||
};
|
||||
|
||||
// feed into shared time-series for graphs
|
||||
pushRuntimeSample(current, cpuPercent, preemptionRate);
|
||||
|
||||
notifySubscribers();
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch runtime metrics:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer != null) return;
|
||||
void fetchOnce();
|
||||
pollTimer = setInterval(fetchOnce, pollIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared runtime metrics hook.
|
||||
*
|
||||
* - One global poller per app (module-level).
|
||||
* - Polling continues even if no components are mounted.
|
||||
* - Components just subscribe to the latest state.
|
||||
*/
|
||||
export function useRuntimeMetrics(
|
||||
enabled: boolean,
|
||||
intervalMs: number = 5000,
|
||||
): MetricsState {
|
||||
const [state, setState] = useState<MetricsState>(currentState);
|
||||
|
||||
// Subscribe to shared state
|
||||
useEffect(() => {
|
||||
const cb = (s: MetricsState) => setState(s);
|
||||
subscribers.add(cb);
|
||||
cb(currentState);
|
||||
|
||||
return () => {
|
||||
subscribers.delete(cb);
|
||||
// do NOT stop polling here; recording continues in background
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Control poller configuration
|
||||
useEffect(() => {
|
||||
pollIntervalMs = intervalMs;
|
||||
|
||||
if (enabled) {
|
||||
if (pollTimer != null) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
startPolling();
|
||||
}
|
||||
}, [enabled, intervalMs]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default useRuntimeMetrics;
|
||||
@@ -1,46 +1,69 @@
|
||||
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";
|
||||
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
|
||||
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
|
||||
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
|
||||
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
|
||||
|
||||
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
|
||||
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
|
||||
|
||||
// make a route tree like this
|
||||
// _mainNavigation/machines/winder2/$serial/control
|
||||
// _mainNavigation/configuration/a
|
||||
@@ -48,6 +71,42 @@ 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 analogInputTestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "analogInputTestMachine/$serial",
|
||||
component: () => <AnalogInputTestMachine />,
|
||||
});
|
||||
|
||||
export const analogInputTestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => analogInputTestMachineSerialRoute,
|
||||
path: "control",
|
||||
component: () => <AnalogInputTestMachineControl />,
|
||||
});
|
||||
|
||||
export const ip20TestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "ip20testmachine/$serial",
|
||||
component: () => <IP20TestMachinePage />,
|
||||
});
|
||||
|
||||
export const ip20TestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => ip20TestMachineSerialRoute,
|
||||
path: "control",
|
||||
component: () => <IP20TestMachineControlPage />,
|
||||
});
|
||||
|
||||
export const sidebarRoute = createRoute({
|
||||
getParentRoute: () => RootRoute,
|
||||
@@ -96,6 +155,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",
|
||||
@@ -257,6 +352,16 @@ export const updateChooseVersionRoute = createRoute({
|
||||
path: "choose-version",
|
||||
component: () => <ChooseVersionPage />,
|
||||
});
|
||||
export const metricsRoute = createRoute({
|
||||
getParentRoute: () => setupRoute,
|
||||
path: "metrics",
|
||||
component: () => (
|
||||
<>
|
||||
<MetricsControlPage />
|
||||
<MetricsGraphsPage />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
export const versionSearchSchema = z
|
||||
.object({
|
||||
@@ -305,14 +410,21 @@ export const rootTree = RootRoute.addChildren([
|
||||
updateExecuteRoute,
|
||||
]),
|
||||
troubleshootRoute,
|
||||
metricsRoute,
|
||||
]),
|
||||
|
||||
machinesRoute.addChildren([
|
||||
laser1SerialRoute.addChildren([
|
||||
laser1ControlRoute,
|
||||
laser1GraphsRoute,
|
||||
laser1PresetsRoute,
|
||||
]),
|
||||
testMachineSerialRoute.addChildren([testMachineControlRoute]),
|
||||
|
||||
analogInputTestMachineSerialRoute.addChildren([
|
||||
analogInputTestMachineControlRoute,
|
||||
]),
|
||||
|
||||
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
aquapath1ControlRoute,
|
||||
@@ -335,6 +447,14 @@ export const rootTree = RootRoute.addChildren([
|
||||
extruder2PresetsRoute,
|
||||
]),
|
||||
|
||||
extruder3Route.addChildren([
|
||||
extruder3ControlRoute,
|
||||
extruder3SettingsRoute,
|
||||
extruder3ManualRoute,
|
||||
extruder3GraphsRoute,
|
||||
extruder3PresetsRoute,
|
||||
]),
|
||||
|
||||
mock1SerialRoute.addChildren([
|
||||
mock1ControlRoute,
|
||||
mock1GraphRoute,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
machineProperties,
|
||||
VENDOR_QITECH,
|
||||
} from "@/machines/properties";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -45,6 +45,7 @@ import { Icon } from "@/components/Icon";
|
||||
import { toast } from "sonner";
|
||||
import { Toast } from "@/components/Toast";
|
||||
import { EthercatDevicesEventData } from "@/client/mainNamespace";
|
||||
import { TouchNumpad } from "@/components/touch/TouchNumpad";
|
||||
|
||||
type Device = NonNullable<EthercatDevicesEventData["Done"]>["devices"][number];
|
||||
|
||||
@@ -63,15 +64,22 @@ type FormSchema = z.infer<typeof formSchema>;
|
||||
export function DeviceEepromDialog({ device }: Props) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const key = useMemo(() => Math.random(), [open]);
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
// Prevent closing via Escape to keep numpad open while interacting
|
||||
modal
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Icon name="lu:Pencil" />
|
||||
Assign
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DeviceEeepromDialogContent device={device} key={key} setOpen={setOpen} />
|
||||
<DeviceEeepromDialogContent device={device} key={key} setOpen={onClose} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -79,11 +87,17 @@ export function DeviceEepromDialog({ device }: Props) {
|
||||
// (Empty comment block removed)
|
||||
type ContentProps = {
|
||||
device: Device;
|
||||
setOpen: (open: boolean) => void;
|
||||
setOpen: () => void;
|
||||
};
|
||||
|
||||
export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
const client = useClient();
|
||||
const serialInputRef = useRef<HTMLInputElement>(null);
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const numpadRef = useRef<HTMLDivElement>(null);
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadPosition, setNumpadPosition] = useState({ left: 0, top: 0 });
|
||||
const serialContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const form = useForm<FormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -126,7 +140,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
Machine assignment written successfully.
|
||||
</Toast>,
|
||||
);
|
||||
setOpen(false);
|
||||
setOpen();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -166,104 +180,328 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
}
|
||||
}, [filteredAllowedDevices]);
|
||||
|
||||
// Position numpad once when it opens
|
||||
useEffect(() => {
|
||||
if (!numpadOpen || !dialogRef.current) return;
|
||||
const rect = dialogRef.current.getBoundingClientRect();
|
||||
setNumpadPosition({
|
||||
left: rect.right + 20,
|
||||
top: rect.top + rect.height / 2,
|
||||
});
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Close numpad when clicking outside input/numpad
|
||||
useEffect(() => {
|
||||
if (!numpadOpen) return;
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
const insideSerial = serialContainerRef.current?.contains(target);
|
||||
const insideNumpad = numpadRef.current?.contains(target);
|
||||
if (!insideSerial && !insideNumpad) {
|
||||
setNumpadOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
};
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Keep focus on input field when numpad is opened
|
||||
useEffect(() => {
|
||||
if (numpadOpen && serialInputRef.current) {
|
||||
// Use setTimeout to ensure this runs after any other focus changes
|
||||
setTimeout(() => {
|
||||
if (serialInputRef.current) {
|
||||
serialInputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Numpad handlers for serial input
|
||||
const numpadHandlers = useMemo(() => {
|
||||
const ensureFocus = () => {
|
||||
if (
|
||||
serialInputRef.current &&
|
||||
document.activeElement !== serialInputRef.current
|
||||
) {
|
||||
serialInputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const updateCursorPosition = (position: number) => {
|
||||
setTimeout(() => {
|
||||
if (serialInputRef.current) {
|
||||
serialInputRef.current.setSelectionRange(position, position);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getCurrentValue = () => {
|
||||
return form.getValues("serial") || "";
|
||||
};
|
||||
|
||||
return {
|
||||
appendDigit: (digit: string) => {
|
||||
if (!serialInputRef.current) return;
|
||||
|
||||
ensureFocus();
|
||||
const input = serialInputRef.current;
|
||||
const start = input.selectionStart || 0;
|
||||
const end = input.selectionEnd || 0;
|
||||
const currentValue = getCurrentValue();
|
||||
const newValue =
|
||||
currentValue.slice(0, start) + digit + currentValue.slice(end);
|
||||
|
||||
form.setValue("serial", newValue, { shouldValidate: true });
|
||||
updateCursorPosition(start + 1);
|
||||
},
|
||||
|
||||
addDecimal: () => {
|
||||
// Not needed for serial (U16 integer), but keeping for consistency
|
||||
// Could be used for other numeric inputs if needed
|
||||
},
|
||||
|
||||
deleteChar: () => {
|
||||
if (!serialInputRef.current) return;
|
||||
|
||||
ensureFocus();
|
||||
const input = serialInputRef.current;
|
||||
const start = input.selectionStart || 0;
|
||||
const end = input.selectionEnd || 0;
|
||||
const currentValue = getCurrentValue();
|
||||
|
||||
let newValue: string;
|
||||
let newPosition: number;
|
||||
|
||||
if (start !== end) {
|
||||
// Delete selection
|
||||
newValue = currentValue.slice(0, start) + currentValue.slice(end);
|
||||
newPosition = start;
|
||||
} else if (start > 0) {
|
||||
// Backspace
|
||||
newValue =
|
||||
currentValue.slice(0, start - 1) + currentValue.slice(start);
|
||||
newPosition = start - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue("serial", newValue, { shouldValidate: true });
|
||||
updateCursorPosition(newPosition);
|
||||
},
|
||||
|
||||
toggleSign: () => {
|
||||
// Not needed for U16 (unsigned), but keeping for consistency
|
||||
},
|
||||
|
||||
moveCursorLeft: () => {
|
||||
if (!serialInputRef.current) return;
|
||||
|
||||
ensureFocus();
|
||||
const currentPos = serialInputRef.current.selectionStart || 0;
|
||||
if (currentPos > 0) {
|
||||
serialInputRef.current.setSelectionRange(
|
||||
currentPos - 1,
|
||||
currentPos - 1,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
moveCursorRight: () => {
|
||||
if (!serialInputRef.current) return;
|
||||
|
||||
ensureFocus();
|
||||
const currentPos = serialInputRef.current.selectionStart || 0;
|
||||
const currentValue = getCurrentValue();
|
||||
if (currentPos < currentValue.length) {
|
||||
serialInputRef.current.setSelectionRange(
|
||||
currentPos + 1,
|
||||
currentPos + 1,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Machine Assignment</DialogTitle>
|
||||
<p>
|
||||
for {device.name}
|
||||
<Hex value={device.configured_address} />
|
||||
</p>
|
||||
<DialogDescription>
|
||||
To assign the device to a machine, select the machine, serial number &
|
||||
device role.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Separator />
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* machine type dropdown */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="machine"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Maschine</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-min">
|
||||
<SelectValue placeholder="Machine" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{machineProperties.map((machine) => (
|
||||
<SelectItem
|
||||
key={machine.machine_identification.machine}
|
||||
value={machine.machine_identification.machine.toString()}
|
||||
>
|
||||
{machine.name} {machine.version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<>
|
||||
<DialogContent
|
||||
ref={dialogRef}
|
||||
// Keep dialog open on any outside interaction; closing is manual via controls
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Machine Assignment</DialogTitle>
|
||||
<p>
|
||||
for {device.name}
|
||||
<Hex value={device.configured_address} />
|
||||
</p>
|
||||
<DialogDescription>
|
||||
To assign the device to a machine, select the machine, serial number
|
||||
& device role.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Separator />
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* machine type dropdown */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="machine"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Maschine</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-min">
|
||||
<SelectValue placeholder="Machine" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{machineProperties.map((machine) => (
|
||||
<SelectItem
|
||||
key={machine.machine_identification.machine}
|
||||
value={machine.machine_identification.machine.toString()}
|
||||
>
|
||||
{machine.name} {machine.version}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Serial Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serial"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Serial</FormLabel>
|
||||
<FormControl>
|
||||
<div
|
||||
ref={serialContainerRef}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
ref={(e) => {
|
||||
field.ref(e);
|
||||
serialInputRef.current = e;
|
||||
}}
|
||||
placeholder="1234"
|
||||
onFocus={() => setNumpadOpen(true)}
|
||||
onClick={() => setNumpadOpen(true)}
|
||||
onBlur={(event) => {
|
||||
const next = event.relatedTarget as Node | null;
|
||||
if (
|
||||
serialContainerRef.current?.contains(next) ||
|
||||
numpadRef.current?.contains(next)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setNumpadOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Serial number of the machine.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Device Role */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
disabled={!machinePreset}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Device Role</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field}>
|
||||
<SelectTrigger className="w-min">
|
||||
<SelectValue placeholder="Device Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{machinePreset?.device_roles.map((device_role, i) => (
|
||||
<SelectItem
|
||||
key={device_role.role}
|
||||
value={device_role.role.toString()}
|
||||
disabled={!filteredAllowedDevices[i]}
|
||||
>
|
||||
<DeviceRoleComponent device_role={device_role} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<Button type="submit" disabled={!form.formState.isValid}>
|
||||
<Icon name="lu:Save" /> Write
|
||||
</Button>
|
||||
<Alert title="Restart mandatory" variant="info">
|
||||
The device must be restarted for the changes to take effect
|
||||
</Alert>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
{/* Numpad as separate window right of dialog */}
|
||||
{numpadOpen && (
|
||||
<div
|
||||
ref={numpadRef}
|
||||
data-numpad
|
||||
className="fixed z-[100] w-auto rounded-md border border-neutral-200 bg-white p-4 shadow-md dark:border-neutral-800 dark:bg-neutral-950"
|
||||
style={{
|
||||
left: `${numpadPosition.left}px`,
|
||||
top: `${numpadPosition.top}px`,
|
||||
transform: "translateY(-50%)",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
tabIndex={-1}
|
||||
onMouseDown={(e) => {
|
||||
// Prevent clicks on numpad from closing the dialog and stealing focus from input
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Ensure input field keeps focus
|
||||
if (serialInputRef.current) {
|
||||
serialInputRef.current.focus();
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Prevent clicks on numpad from closing the dialog
|
||||
e.stopPropagation();
|
||||
// Ensure input field keeps focus
|
||||
if (serialInputRef.current) {
|
||||
serialInputRef.current.focus();
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent Escape or other keys from bubbling and closing the dialog
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<TouchNumpad
|
||||
onDigit={numpadHandlers.appendDigit}
|
||||
onDelete={numpadHandlers.deleteChar}
|
||||
onCursorLeft={numpadHandlers.moveCursorLeft}
|
||||
onCursorRight={numpadHandlers.moveCursorRight}
|
||||
/>
|
||||
{/* Serial Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serial"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Serial</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="1234" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Serial number of the machine.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Device Role */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
disabled={!machinePreset}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Device Role</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field}>
|
||||
<SelectTrigger className="w-min">
|
||||
<SelectValue placeholder="Device Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{machinePreset?.device_roles.map((device_role, i) => (
|
||||
<SelectItem
|
||||
key={device_role.role}
|
||||
value={device_role.role.toString()}
|
||||
disabled={!filteredAllowedDevices[i]}
|
||||
>
|
||||
<DeviceRoleComponent device_role={device_role} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<Button type="submit" disabled={!form.formState.isValid}>
|
||||
<Icon name="lu:Save" /> Write
|
||||
</Button>
|
||||
<Alert title="Restart mandatory" variant="info">
|
||||
The device must be restarted for the changes to take effect
|
||||
</Alert>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useMainNamespace } from "@/client/mainNamespace";
|
||||
import { useLaser1Namespace } from "@/machines/laser/laser1/laser1Namespace";
|
||||
import { laser1 } from "@/machines/properties";
|
||||
import type { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { Icon } from "@/components/Icon";
|
||||
|
||||
/**
|
||||
* Global manager that toasts laser StateEvent changes.
|
||||
* If machineIdentification is omitted it will attempt to discover the laser via the main namespace.
|
||||
*
|
||||
*/
|
||||
export function GlobalLaserToastManager({
|
||||
machineIdentification,
|
||||
}: {
|
||||
machineIdentification?: MachineIdentificationUnique;
|
||||
}) {
|
||||
const main = useMainNamespace();
|
||||
const [discovered, setDiscovered] =
|
||||
useState<MachineIdentificationUnique | null>(null);
|
||||
|
||||
// Prefer explicit prop, otherwise use discovered id
|
||||
const effectiveId = machineIdentification ?? discovered;
|
||||
|
||||
// Attempt to discover the single laser machine from the main namespace
|
||||
useEffect(() => {
|
||||
if (machineIdentification) return;
|
||||
const machinesEvent = (main as any)?.machines;
|
||||
if (!machinesEvent) {
|
||||
setDiscovered(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Defensive shapes: try common structures
|
||||
let list: any[] = [];
|
||||
if (Array.isArray(machinesEvent?.data?.machines))
|
||||
list = machinesEvent.data.machines;
|
||||
else if (Array.isArray(machinesEvent?.data)) list = machinesEvent.data;
|
||||
else if (Array.isArray(machinesEvent?.machines))
|
||||
list = machinesEvent.machines;
|
||||
else if (machinesEvent && typeof machinesEvent === "object") {
|
||||
list = Object.values(machinesEvent).filter(
|
||||
(v) => v && typeof v === "object",
|
||||
);
|
||||
}
|
||||
|
||||
const match = list.find((entry) => {
|
||||
const mi =
|
||||
entry?.machine_identification ||
|
||||
entry?.machine_identification_unique?.machine_identification ||
|
||||
entry?.device_machine_identification?.machine_identification;
|
||||
if (!mi) return false;
|
||||
return (
|
||||
mi.vendor === (laser1.machine_identification as any).vendor &&
|
||||
mi.machine === (laser1.machine_identification as any).machine
|
||||
);
|
||||
});
|
||||
|
||||
if (match) {
|
||||
const serial =
|
||||
match?.serial ??
|
||||
match?.machine_identification_unique?.serial ??
|
||||
match?.device_machine_identification?.machine_identification_unique
|
||||
?.serial ??
|
||||
null;
|
||||
if (serial != null && !Number.isNaN(Number(serial))) {
|
||||
setDiscovered({
|
||||
machine_identification: laser1.machine_identification,
|
||||
serial: Number(serial),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setDiscovered(null);
|
||||
}, [main, machineIdentification]);
|
||||
|
||||
return effectiveId ? (
|
||||
<LaserToastWatcher machineIdentification={effectiveId} />
|
||||
) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches StateEvent for the laser namespace and shows toasts for non-default states.
|
||||
* Component is non-visual.
|
||||
*/
|
||||
function LaserToastWatcher({
|
||||
machineIdentification,
|
||||
}: {
|
||||
machineIdentification: MachineIdentificationUnique;
|
||||
}) {
|
||||
const { state } = useLaser1Namespace(machineIdentification);
|
||||
|
||||
// Deduplicate toasts by event timestamp
|
||||
const lastToastTs = useRef<number | string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state) return;
|
||||
|
||||
const eventTs = (state as any)?.ts ?? null;
|
||||
const inTolerance = (state as any)?.data?.laser_state.in_tolerance;
|
||||
const isDefault = !!(state as any)?.data?.is_default_state;
|
||||
const toastId = eventTs?.toString() ?? String(Date.now());
|
||||
|
||||
if (isDefault) return;
|
||||
|
||||
// Only show toast if laser is out of tolerance and not already shown for this timestamp
|
||||
if (!inTolerance && lastToastTs.current !== eventTs) {
|
||||
lastToastTs.current = eventTs;
|
||||
// Sonner toast call
|
||||
toast(
|
||||
<div className="flex w-100 flex-col gap-3 rounded-xl border border-red-400 bg-red-600 p-4 text-white shadow-xl backdrop-blur-sm transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon
|
||||
name="lu:TriangleAlert"
|
||||
className="h-5 w-5 text-yellow-200"
|
||||
/>
|
||||
<strong className="text-lg font-semibold tracking-wide">
|
||||
Warning
|
||||
</strong>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-md p-1 text-2xl font-bold text-white/80 hover:bg-red-500 hover:text-white focus:ring-2 focus:ring-white/30 focus:outline-none"
|
||||
onClick={() => {
|
||||
toast.dismiss(toastId);
|
||||
lastToastTs.current = null;
|
||||
}}
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-base leading-snug text-red-50">
|
||||
Laser diameter is <strong>out of tolerance</strong>.<br />
|
||||
Please check the filament immediately.
|
||||
</p>
|
||||
</div>,
|
||||
{
|
||||
id: toastId,
|
||||
duration: Infinity,
|
||||
position: "top-center",
|
||||
style: {
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
boxShadow: "none",
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
if (inTolerance) {
|
||||
lastToastTs.current = null;
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -30,6 +30,12 @@ export function SetupPage() {
|
||||
title: "Troubleshoot",
|
||||
icon: "lu:MessageSquareWarning",
|
||||
},
|
||||
{
|
||||
link: "metrics",
|
||||
activeLink: "metrics",
|
||||
title: "Metrics",
|
||||
icon: "lu:Activity",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ export function TroubleshootPage() {
|
||||
// Get backend logs for display
|
||||
const backendLogs = getLogsBySource("qitech-control-server");
|
||||
const logLines = backendLogs.map((log) => log.raw);
|
||||
console.log(backendLogs);
|
||||
const handleRebootHmi = async () => {
|
||||
setIsRebootLoading(true);
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@root/*": ["../*"]
|
||||
},
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -30,6 +30,7 @@ export default defineConfig((env) => {
|
||||
preserveSymlinks: true,
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@root": path.resolve(__dirname, ".."),
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
|
||||
@@ -9,11 +9,11 @@ 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();
|
||||
|
||||
|
||||
@@ -251,6 +251,27 @@ pub fn ethercat_device_derive(input: TokenStream) -> TokenStream {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
|
||||
fn is_module(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
|
||||
fn get_module(&self) -> Option<crate::devices::Module> {
|
||||
None
|
||||
}
|
||||
|
||||
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
|
||||
fn set_module(&mut self,module : crate::devices::Module){
|
||||
()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl crate::devices::EthercatDeviceUsed for #name {
|
||||
|
||||
@@ -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;
|
||||