Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
850b2d411d | ||
|
|
008efe6ba1 | ||
|
|
3bd248b9fe | ||
|
|
d73cfe7a02 | ||
|
|
6d2279d44b | ||
|
|
728dd6eb26 | ||
|
|
742fd35d5f | ||
|
|
5e1126e896 | ||
|
|
e91719f902 | ||
|
|
0efa9bcb07 | ||
|
|
37d897d240 | ||
|
|
7488e1d97a | ||
|
|
0a87e45901 | ||
|
|
6205189b14 | ||
|
|
9d7da4f0af | ||
|
|
241b24be43 | ||
|
|
aa77d12879 | ||
|
|
78f183a8e5 | ||
|
|
9f6fc8184e | ||
|
|
ec3bdcb1df | ||
|
|
30292650bc | ||
|
|
1e9bcf9b19 | ||
|
|
1e88979ced | ||
|
|
1112fbaae2 | ||
|
|
1abe0bbbdb | ||
|
|
3644d16cb5 | ||
|
|
cbd82ee7c3 | ||
|
|
06ea89d500 | ||
|
|
0868a90513 | ||
|
|
39fb295b02 | ||
|
|
85dbae7458 | ||
|
|
6ce2174e25 | ||
|
|
3ccaa1cd26 | ||
|
|
72fec6dc9e | ||
|
|
547e4ac2bd | ||
|
|
502c6d010e | ||
|
|
e864a91caa | ||
|
|
2d2415a801 | ||
|
|
b4cce1dba4 | ||
|
|
f80cdb05d5 | ||
|
|
f24f31259b | ||
|
|
6411f9ff80 | ||
|
|
8b462d5a1d | ||
|
|
d8a0039db3 | ||
|
|
dec8ea481c | ||
|
|
0c8b8388d0 | ||
|
|
d3a56710d6 | ||
|
|
f9f67d3309 | ||
|
|
66122ad957 | ||
|
|
95feaf46bf | ||
|
|
88f14e0329 | ||
|
|
5bf19af844 | ||
|
|
19e2a337cf | ||
|
|
0fd47cfcb1 | ||
|
|
e0548d9a65 | ||
|
|
259ed30dd5 | ||
|
|
2e028f0c0f | ||
|
|
1328c6ce03 | ||
|
|
adc06623cd | ||
|
|
172ad0ab28 | ||
|
|
bfabb5aa13 | ||
|
|
94d5a844cf | ||
|
|
75c528a430 | ||
|
|
36045455db | ||
|
|
bb12f96723 | ||
|
|
8151c99944 | ||
|
|
6408010aff | ||
|
|
c96c96217a | ||
|
|
93d179a81d | ||
|
|
0f6f2240fc | ||
|
|
74505c6c22 | ||
|
|
3dfb879101 | ||
|
|
44d4c650cc | ||
|
|
3121679205 | ||
|
|
9fe5e582b0 | ||
|
|
3e6b838757 | ||
|
|
3d7bcfecca | ||
|
|
63a472a1ba | ||
|
|
df6b9e72c0 | ||
|
|
6ad4b9c8bb | ||
|
|
4fcc8dfe12 | ||
|
|
1da64c9cea | ||
|
|
9b6912be8d | ||
|
|
3beed9478c | ||
|
|
3715728fc6 | ||
|
|
5beee78cad | ||
|
|
32fe774e62 | ||
|
|
dead77b49c | ||
|
|
d7aa976eb1 | ||
|
|
6f9e89d616 | ||
|
|
bb3f6c0d5a | ||
|
|
620974ac59 | ||
|
|
6c78ff95e4 | ||
|
|
a814236bfa | ||
|
|
2bee030410 | ||
|
|
285ad59b08 | ||
|
|
f8b861cb39 | ||
|
|
f6a3aa6458 | ||
|
|
dd5f3e8e73 | ||
|
|
f50e32af02 | ||
|
|
345e083edd | ||
|
|
5660ddd05f | ||
|
|
c4360850af | ||
|
|
ce2123c88c | ||
|
|
520f33bf29 | ||
|
|
c620f1db7f | ||
|
|
d352e88d6f | ||
|
|
cc009b31f3 | ||
|
|
b5c189ab49 | ||
|
|
64f57277f2 | ||
|
|
af8ee60fe8 | ||
|
|
beb2e3cc2d | ||
|
|
0e2bcfd7bc | ||
|
|
417ffb23c2 | ||
|
|
a57d4cdf36 |
No files matched your search
@@ -1,3 +1,15 @@
|
||||
# `2.14.1`
|
||||
_26.01.2026_
|
||||
|
||||
## Laser
|
||||
-[#1079](https://github.com/qitechgmbh/control/pull/1079) Fixed Overflowing of buffer for Graphs causing crashes
|
||||
|
||||
# `2.14.0`
|
||||
_22.01.2026_
|
||||
|
||||
## General
|
||||
- [#1040](https://github.com/qitechgmbh/control/pull/1040) Added API for reading Machine Data
|
||||
|
||||
# `2.13.0`
|
||||
_07.01.2026_
|
||||
|
||||
@@ -5,7 +17,6 @@ _07.01.2026_
|
||||
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
|
||||
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
|
||||
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
|
||||
- []
|
||||
|
||||
## Extruder V3
|
||||
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
|
||||
|
||||
@@ -17,22 +17,22 @@ QiTech Control combines the modularity and reliability of standard EtherCAT term
|
||||
|
||||
# Repo Structure
|
||||
|
||||
Frontend
|
||||
**Frontend**
|
||||
|
||||
- `/electron`: Frontend code for the control software built with React and Electron.
|
||||
|
||||
Backend
|
||||
**Backend**
|
||||
|
||||
- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
|
||||
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
|
||||
- `/ethercat-hal-derive`: Macros for `ethercat-hal`
|
||||
- `/control-core`: Core control logic for the server.
|
||||
|
||||
Operating System
|
||||
**Operating System**
|
||||
|
||||
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
|
||||
|
||||
Other
|
||||
**Other**
|
||||
|
||||
- `/docs`: Documentation for the project.
|
||||
|
||||
@@ -54,115 +54,57 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
|
||||
[Developer Documentation](./docs/developer-docs/)
|
||||
|
||||
## Backend
|
||||
[API Documentation](https://github.com/qitechgmbh/control/blob/master/docs/rest-api.md)
|
||||
|
||||
-> Backend
|
||||
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
|
||||
- `rust-analyzer` extension for VSCode
|
||||
- Set your interface in `server/src/ethercat/init.rs` like `en10`
|
||||
- Connect a Beckhoff EK1100 to your interface
|
||||
- run `cd server && cargo run` to start the server (localhost:3001)
|
||||
|
||||
## Frontend
|
||||
|
||||
-> Frontend
|
||||
- nodejs and npm installed
|
||||
- run `cd electron && npm i && npm run start` to start the frontend
|
||||
|
||||
# Machines
|
||||
|
||||
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
|
||||
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
|
||||
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
|
||||
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
|
||||
|
||||
# Minimal Hardware Examples
|
||||
## 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
|
||||
| Example | Vendor | Hardware | Description | How-To Video | Docs |
|
||||
| :----------------- | :------- | :------------ | :-------------------------------------- | :----------- | :---------------------------- |
|
||||
| **Digital Input** | WAGO | 750-402 | 4-Channel Digital Input | [ ] | - |
|
||||
| **Digital Output** | WAGO | 750-753 | 8-Channel Digital Output | [ ] | - |
|
||||
| **Analog Input** | WAGO | 750-455 | 4-Channel Analog Input | [ ] | - |
|
||||
| **Analog Output** | WAGO | 750-553 | 4-Channel Analog Output | [ ] | - |
|
||||
| **Serial Comms** | WAGO | 750-652 | RS-485 Modbus Module | [ ] | - |
|
||||
| **Stepper Drive** | WAGO | 750-67x | Stepper Controller | [ ] | - |
|
||||
| **Power Supply** | WAGO | 2787-214 | Power Supply Unit | [ ] | - |
|
||||
| **LED Control** | Beckhoff | EL2004 | Digital output control (simplest setup) | [ ] | [](./docs/hardware/el2004.md) |
|
||||
| **Analog Input** | Beckhoff | EL3021 | Reading analog current measurements | [ ] | [](./docs/hardware/el3021.md) |
|
||||
|
||||
# QiTech Machines
|
||||
|
||||
# More Docs
|
||||
| Machine Name | Description | Machine ID | Implemented | Video | Docs |
|
||||
| :---------------- | :-------------------------- | :--------- | :---------- | :-------------------------------------------------------------- | :------------------------------ |
|
||||
| Winder V1 | Winding Filaments & Similar | 1 | Legacy | [Video](https://youtu.be/4aE4cFhioKA?si=Xdj_LVnFrAYWnLm6) | - |
|
||||
| Winder V2 | Winding Filaments & Similar | 2 | Yes | [Video](https://youtu.be/f2kzh6kpQWE?si=HEYcLOaC9gWSp2Wo) | [](./docs/machines/winder-1.md) |
|
||||
| XL Winder V1 | Large Scale Winder | 3 | Yes | [Video](https://youtu.be/ynI6ioWIQQY?si=4EDVKXbgIqrvGGtY) | - |
|
||||
| Buffer V1 | Filament buffering system | 4 | In Progress | [Video](https://youtu.be/VR5mEdZDPA0?si=eip7dQXU4zaXK7Ev) | - |
|
||||
| Extruder V1 | Single Screw Extruder | 5 | Legacy | [Video](https://youtu.be/gchoG-yGczI?si=bEM4daf5eOVaU5_2) | - |
|
||||
| Extruder V2 | Single Screw Extruder | 6 | Yes | [Video](https://youtu.be/mexRYGDNWa4?si=h7JBz_XKMwKLPenG) | |
|
||||
| Waterway V1 | Filament Water Cooling | 7 | In Progress | [Video](https://youtu.be/_T5z1J8bl_k?si=uOC1hIQ1EAP0cHIF) | |
|
||||
| Laser V1 | Diameter Measuring Laser | 8 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | |
|
||||
| 2-Axis-Laser V1 | Dual Axis Laser | 9 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | - |
|
||||
| Mock | Mock Machine for Testing | 10 | Yes | | - |
|
||||
| Extruder V3 | Single Screw Extruder | 11 | Yes | [Video](https://youtu.be/ipHHuPzCvn4?si=hkZ1b93rVuwDxhFD) | |
|
||||
| Mini Schredder V1 | mini plastic crusher | 12 | Legacy | [Video](https://youtu.be/m8NplNqdu2Q?si=x6zjDktJtpbSMSmu) | - |
|
||||
| Pro Schredder V1 | large plastic crusher | 13 | Legacy | [Video](https://youtu.be/pSaVMqp06pU?si=y61enutRpxKscBm4) | - |
|
||||
| Dryer V1 | polymer dryer | 14 | Reserved | [Video](https://youtu.be/6hdmUUAdZp0?si=eKOW1WlgkqTpnfdS) | - |
|
||||
| Pelletizier V1 | Filament chopper | 15 | Reserved | | - |
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
# Current Restructuring Efforts
|
||||
|
||||
- [x] Example Winder V2
|
||||
|
||||
- [ ] Electron
|
||||
|
||||
- Folder Structure
|
||||
- Routing with TanStack Router
|
||||
- Design with Tailwind & Shadcn
|
||||
- ...
|
||||
|
||||
- [ ] Interfacing with Electron/Server
|
||||
|
||||
- [ ] SocketIO
|
||||
- Machine Namespace
|
||||
- Main Namespace
|
||||
- [ ] REST
|
||||
- Machine Mutations
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
- Maindevice
|
||||
- Group
|
||||
- Extracting Device Identifications
|
||||
- Identifying Groups
|
||||
- Validating Machines
|
||||
- Run Control Loop
|
||||
- [x] [Control Loop](./docs/control-loop.md)
|
||||
- [x] [Machine/Device Identification](./docs/identification.md)
|
||||
- [ ] Machines
|
||||
- When to create a new Machine?
|
||||
- Versioning
|
||||
- Code sharing
|
||||
- Creating/Validating a Machine
|
||||
- Validation
|
||||
- Configuration
|
||||
- [ ] Machine Implementation Guide
|
||||
- Link: How to create a Device
|
||||
- Link: How to create an Actor
|
||||
- Link: How to create a Machine
|
||||
- API (SocketIO + REST)
|
||||
- Creation/Validation Logic
|
||||
- Optional/Mandatory Devices
|
||||
- Validate Devices
|
||||
- Business Logic
|
||||
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
|
||||
- Forward `act` in winder.
|
||||
|
||||
- [ ] Control Core
|
||||
|
||||
- [x] [Actors](./docs/actors.md)
|
||||
- [ ] SocketIO
|
||||
- Namespaces & Caching
|
||||
- Joining leaving namespaces
|
||||
- NamespaceId
|
||||
- Caching
|
||||
- Serverside Caching
|
||||
- Clientside Caching
|
||||
- [ ] REST
|
||||
|
||||
- [x] Ethercat HAL
|
||||
|
||||
- [x] [Devices](./docs/devices.md)
|
||||
- [x] [Configuration (CoE)](./docs/coe.md)
|
||||
- [x] [IO](./docs/io.md)
|
||||
- [x] [PDO](./docs/pdo.md)
|
||||
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
[](https://www.youtube.com/live/UlbnSVIhfLI?si=ZTotC5B7gd87tUim)
|
||||
*Video: Live stream of software dev meeting*
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 6.6 MiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -91,3 +91,83 @@ To get started with actual hardware, check out these step-by-step tutorials:
|
||||
- **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements
|
||||
|
||||
These examples provide complete hardware wiring diagrams and software setup instructions.
|
||||
|
||||
|
||||
# More Docs
|
||||
|
||||
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
|
||||
|
||||
- [x] Example Winder V2
|
||||
|
||||
- [ ] Electron
|
||||
|
||||
- Folder Structure
|
||||
- Routing with TanStack Router
|
||||
- Design with Tailwind & Shadcn
|
||||
- ...
|
||||
|
||||
- [ ] Interfacing with Electron/Server
|
||||
|
||||
- [ ] SocketIO
|
||||
- Machine Namespace
|
||||
- Main Namespace
|
||||
- [ ] REST
|
||||
- Machine Mutations
|
||||
- Write Device Identification
|
||||
|
||||
- Server
|
||||
|
||||
- [x] [Threading](./docs/control-loop.md)
|
||||
- [x] [Logging](./docs/logging.md)
|
||||
- [ ] Control Loop Setup
|
||||
- Control Loop Thread
|
||||
- [ ] realtime
|
||||
- Maindevice
|
||||
- Group
|
||||
- Extracting Device Identifications
|
||||
- Identifying Groups
|
||||
- Validating Machines
|
||||
- Run Control Loop
|
||||
- [x] [Control Loop](./docs/control-loop.md)
|
||||
- [x] [Machine/Device Identification](./docs/identification.md)
|
||||
- [ ] Machines
|
||||
- When to create a new Machine?
|
||||
- Versioning
|
||||
- Code sharing
|
||||
- Creating/Validating a Machine
|
||||
- Validation
|
||||
- Configuration
|
||||
- [ ] Machine Implementation Guide
|
||||
- Link: How to create a Device
|
||||
- Link: How to create an Actor
|
||||
- Link: How to create a Machine
|
||||
- API (SocketIO + REST)
|
||||
- Creation/Validation Logic
|
||||
- Optional/Mandatory Devices
|
||||
- Validate Devices
|
||||
- Business Logic
|
||||
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
|
||||
- Forward `act` in winder.
|
||||
|
||||
- [ ] Control Core
|
||||
|
||||
- [x] [Actors](./docs/actors.md)
|
||||
- [ ] SocketIO
|
||||
- Namespaces & Caching
|
||||
- Joining leaving namespaces
|
||||
- NamespaceId
|
||||
- Caching
|
||||
- Serverside Caching
|
||||
- Clientside Caching
|
||||
- [ ] REST
|
||||
|
||||
- [x] Ethercat HAL
|
||||
|
||||
- [x] [Devices](./docs/devices.md)
|
||||
- [x] [Configuration (CoE)](./docs/coe.md)
|
||||
- [x] [IO](./docs/io.md)
|
||||
- [x] [PDO](./docs/pdo.md)
|
||||
|
||||
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
|
||||
|
||||
- [x] [NixOS Operating System](./docs/nixos/README.md)
|
||||
@@ -0,0 +1,196 @@
|
||||
# Minimal Example — Analog Input Wago 750-455
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
## 1. Introduction
|
||||
The Wago 750-455 Analog Input example demonstrates how to measure analog current on a **Wago 750-455 EtherCAT module** using the QiTech machine framework. This module provides 4 independent analog input channels that can read 4-20mA signals.
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- **Wago 750-354 EtherCAT Coupler**
|
||||
- **Wago 750-602 Power Supply** (24V VDC)
|
||||
- **Wago 750-455 EtherCAT Module** (4-channel analog input, 4-20mA)
|
||||
- **Wago 750-600 End Module**
|
||||
- **24 V DC power supply** for the Wago system
|
||||
- **Adjustable DC power supply** (or any other means of producing 4-20mA DC @ <10V)
|
||||
- Jumper / bridge wires (0.5–1.5 mm² recommended)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
### Software
|
||||
*(Installation steps in Section 4)*
|
||||
- Rust toolchain
|
||||
- Node.js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL (included inside repo)
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Overview
|
||||
The Wago 750-455 is a 4-channel analog input module that measures current signals in the 4-20mA range. Each channel has its own input terminals and can operate independently.
|
||||
|
||||
#### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT modules can cause serious damage or electrical shock.
|
||||
|
||||
### 3.2 Wiring Procedure
|
||||
|
||||
#### 3.2.1 Safe Wiring (Wago Spring Clamp)
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 3.3 Wago 750-455 Module Wiring
|
||||
|
||||
The Wago 750-455 has 4 analog input channels. Each channel requires two terminals:
|
||||
|
||||
**Channel 1 (AI1):**
|
||||
- Terminal 1: ⊕ (Positive input)
|
||||
- Terminal 2: ⊖ (Negative input/Ground)
|
||||
|
||||
**Channel 2 (AI2):**
|
||||
- Terminal 3: ⊕ (Positive input)
|
||||
- Terminal 4: ⊖ (Negative input/Ground)
|
||||
|
||||
**Channel 3 (AI3):**
|
||||
- Terminal 5: ⊕ (Positive input)
|
||||
- Terminal 6: ⊖ (Negative input/Ground)
|
||||
|
||||
**Channel 4 (AI4):**
|
||||
- Terminal 7: ⊕ (Positive input)
|
||||
- Terminal 8: ⊖ (Negative input/Ground)
|
||||
|
||||
For this minimal example, we'll connect at least one channel to an adjustable power supply:
|
||||
|
||||
1. Wire adjustable power supply ⊕ to **Terminal 1** (Channel 1 positive)
|
||||
2. Wire adjustable power supply ⊖ to **Terminal 2** (Channel 1 negative)
|
||||
|
||||
*Repeat for additional channels as needed.*
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Wago I/O System Integration
|
||||
|
||||
The Wago 750-455 module plugs into a Wago fieldbus system. Consult your specific Wago controller/coupler documentation for:
|
||||
- Power supply requirements
|
||||
- Network configuration
|
||||
- Module placement in the I/O rack
|
||||
|
||||
The module automatically connects to the backplane for EtherCAT communication and power.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Power & Ethernet
|
||||
|
||||
#### Power
|
||||
Ensure your Wago system has adequate 24V DC power connected per manufacturer specifications.
|
||||
|
||||
#### Ethernet
|
||||
Connect your PC to the Wago controller/coupler using a standard Ethernet cable.
|
||||
|
||||
---
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4.2 Running the Backend
|
||||
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
- Builds the backend
|
||||
- Grants required system capabilities (raw sockets)
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the Wago controller is connected and powered.
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
|
||||
```bash
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
|
||||
Once both backend and frontend are running:
|
||||
|
||||
1. Navigate to **Setup → EtherCAT** page
|
||||
2. You should see your Wago controller and the 750-455 module listed
|
||||
3. Select the Device called 750-354 and assign the Wago AI Test V1 Machine, set the Serial to 1 and set the device role to 0 (Wago Bus Coupler)
|
||||
4. Press write thn restart the backend to load the changes
|
||||
|
||||
### 5.2 Testing Analog Input
|
||||
|
||||
1. Set your adjustable power supply to output a current in the 4-20mA range
|
||||
2. In the dashboard, navigate to the monitoring view
|
||||
3. Observe the analog input value for the connected channel(s)
|
||||
|
||||
### 5.3 Channel Specifications
|
||||
|
||||
The Wago 750-455 provides:
|
||||
- **Input Range:** 4-20mA
|
||||
- **Wiring Error Detection:** Built-in detection for open/short circuit conditions
|
||||
- **4 Independent Channels:** Each can be read separately
|
||||
|
||||
### 5.4 Error Detection
|
||||
|
||||
The module includes wiring error detection. If you see a wiring error flag:
|
||||
- Check that your current source is within the 4-20mA range
|
||||
- Verify all wire connections are secure
|
||||
- Ensure the current loop is properly closed
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
### Wago Documentation
|
||||
- [Wago 750-455 Product Page](https://www.wago.com/de-en/io-systems/4-channel-analog-input/p/750-455)
|
||||
- Wago I/O System manual
|
||||
- EtherCAT configuration guide for Wago systems
|
||||
|
||||
### QiTech Control
|
||||
- [Architecture Overview](../architecture-overview.md)
|
||||
- [EtherCAT Basics](../ethercat-basics.md)
|
||||
- [Device Documentation](../devices.md)
|
||||
- [Getting Started Guide](getting-started.md)
|
||||
|
||||
### Related Examples
|
||||
- [Minimal Example — EL3021](minimal-example-el3021.md) (Single channel analog input)
|
||||
- [Minimal Example — EL2004](minimal-example-el2004.md) (Digital output)
|
||||
@@ -0,0 +1,240 @@
|
||||
# Minimal Example — LED Control on the WAGO750-402
|
||||
A complete hardware + software walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Requirements](#2-requirements)
|
||||
3. [Hardware Setup](#3-hardware-setup)
|
||||
4. [Software Setup](#4-software-setup)
|
||||
5. [Demo](#5-demo)
|
||||
6. [References](#6-references)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
The WAGO 750-402 Display Example is a minimal demonstation showing how to display the digital inputs on a WAGO 750-402 EtherCAT terminal using the QiTech machine framework.
|
||||
It represents the simplest possible hardware interaction in the system:
|
||||
Displaying the Digital Inputs using the QiTech Control Dashboard.
|
||||
## 2. Requirements
|
||||
|
||||
### Hardware
|
||||
- WAGO **2787-2144 EtherCAT**
|
||||
- WAGO **750-354 EtherCAT Coupler**
|
||||
- WAGO **750-602 EtherCAT Power Terminal**
|
||||
- WAGO **750-402 EtherCAT Terminal** (4.channle digital input)
|
||||
- WAGO **750-600 EtherCAT Endmodule**
|
||||
- WAGO **852-1322 Ethernet-Switch**
|
||||
- **24V DC Lab power supply**(AC/DC adapter)
|
||||
- A **Linux PC** (Ubuntu/Debian recommended)
|
||||
- Standard Ethernet cable
|
||||
- Flat screwdriver
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Software
|
||||
*(Installation steps in Section 4)*
|
||||
|
||||
- Rust tollchain
|
||||
- Node-js + npm
|
||||
- Git
|
||||
- QiTech Control repository
|
||||
- EtherCAT HAL *(include inside repo)*
|
||||
|
||||
## 3. Hardware Setup
|
||||
|
||||
### 3.1 Schematic
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ 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 to the other devices by using the **750-602 EtherCAT Power Terminal**
|
||||
|
||||
Perform the following wiring on the Ethernet-Switch:
|
||||
|
||||
1. Red wire (+24V) → PWR+
|
||||
2. Black wire (0V) → RPS-
|
||||
|
||||
#### **Figure 1 —**
|
||||
<img src="../assets/WAGO750_402_switch.JPG" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.3 WAGO Wiring the Coupler
|
||||
|
||||
Wire the **750-354 EtherCAT Coupler** to the power supply
|
||||
1. red wire (+24V)
|
||||
2. yellow (0V)
|
||||
|
||||
#### **Figure 2 —**
|
||||
<img src="../assets/WAGO750_402_coupler.JPG" width="400">
|
||||
|
||||
---
|
||||
|
||||
### 3.4 750-602 EtherCAT Power Terminal Integration
|
||||
|
||||
Slide the **750-602 EtherCAT Power Terminal** onto the **right side of the 750-354 Coupler** until it locks.
|
||||
|
||||
Next We need to wire the EtherCat Power Terminal
|
||||
|
||||
Perform the next wiring Steps:
|
||||
|
||||
1. Green wire (Ground) → **Terminal4**
|
||||
2. Red wire (+24V) from **750-602 EtherCAT Power Terminal** → **Terminal 2**
|
||||
3. Black wire(0V) from **750-602 EtherCAT Power Terminal** → **Terminal 3**
|
||||
4. Red wire (+24V) from **750-354 EtherCAT Coupler** → **Terminal 6**
|
||||
5. Yellow wire (0V) from **750-354 EtherCAT Coupler** → **Terminal 7**
|
||||
|
||||
#### **Figure 3 — Wiring**
|
||||
<img src="../assets/WAGO750_402_power_terminal.JPG" width="400">
|
||||
|
||||
---
|
||||
|
||||
## 3.5 750-402 EtherCAT Terminal Integration
|
||||
|
||||
Slide the **750-402 EtherCAT Terminal** onto to **right side of the 750-602 EtherCAT Power Terminal** until it locks.
|
||||
|
||||
Wire from the **24V DC Lab Adapter** → **Terminal 1**
|
||||
|
||||
#### **Figure 4 — Wiring**
|
||||
<img src="../assets/WAGO750_402_terminal.JPG" width="400">
|
||||
|
||||
## 3.6 750-600 Final Assemly
|
||||
|
||||
Slide the **750-600 EtherCAT Endmodule** onto to **right side of the 7750-402 EtherCAT Terminal** until it locks.
|
||||
|
||||
No wiring is required
|
||||
|
||||
#### **Figure 5 — 750-402 + 750-600 Connected**
|
||||
<img src="../assets/WAGO750_402_end_modul.JPG" width="400">
|
||||
|
||||
## 3.7 Power & Etherneet
|
||||
|
||||
### Power
|
||||
|
||||
Connect the 2787-2144 power supply to the power outlet
|
||||
|
||||
### Ehternet
|
||||
|
||||
Use a standard LAN cable to connect to your PC → **852-1322 Ethernet-Switch**
|
||||
|
||||
And another from **852-1322 Ethernet-Switch** → **750-354 EtherCAT Coupler**
|
||||
|
||||
#### **Figure 6 — Ethernet**
|
||||
<img src="../assets/WAGO750_402_ethernet.JPG" width="400">
|
||||
|
||||
## 4. Software Setup
|
||||
|
||||
### 4.1 Installing on Ubuntu/Debian
|
||||
Paste this into your terminal:
|
||||
|
||||
```bash
|
||||
# Press Enter when prompted
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y npm nodejs git
|
||||
|
||||
git clone git@github.com:qitechgmbh/control.git
|
||||
cd control/electron
|
||||
npm install
|
||||
```
|
||||
### 4.2 Running the Backend
|
||||
```bash
|
||||
./cargo_run_linux.sh
|
||||
```
|
||||
This script:
|
||||
|
||||
- Builds the backend
|
||||
|
||||
- Grants required system capabilities (raw sockets)
|
||||
|
||||
- Starts EtherCAT communication
|
||||
|
||||
Ensure the WAGO750-402 is connected.
|
||||
|
||||
### 4.3 Running the Frontend
|
||||
```bash
|
||||
cd electron
|
||||
npm run start
|
||||
```
|
||||
|
||||
This launches the QiTech Control dashboard.
|
||||
|
||||
## 5. Demo
|
||||
|
||||
### 5.1 Assigning Devices in the Dashboard
|
||||
|
||||
Once the backend + frontend are running, you should see:
|
||||
|
||||
- WAGO750-354 (Coupler)
|
||||
|
||||
- wago module (Digital Input Terminal)
|
||||
|
||||

|
||||
|
||||
Steps:
|
||||
|
||||
- Click Assign on the wago module
|
||||
|
||||
- Select Digital Input Machine V1
|
||||
|
||||

|
||||
|
||||
|
||||
- Enter a serial number except for 0
|
||||
|
||||

|
||||
|
||||
- Click Write
|
||||
|
||||
### 5.2 Diyplaying LED
|
||||
Navigate to:
|
||||
|
||||
Machines → DigitalInputMachine
|
||||
|
||||

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

|
||||
|
||||
Now you can apply a Voltage of the Range of 15V-30V (+24V recommended) from the DC lab power supply to the Terminal.
|
||||
The Terminal can read the digital input, which is signaled by the LED on the Terminal and should be displayed in the Control software.
|
||||
|
||||
In our example it lights up the first LED
|
||||
|
||||

|
||||
|
||||
## 6. References
|
||||
This guide incorporates information from official WAGO documentation.
|
||||
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
|
||||
|
||||
Referenced Manuals
|
||||
|
||||
[WAGO 750-402 Documentation](https://www.wago.com/de/io-systeme/4-kanal-digitaleingang/p/750-402#downloads)
|
||||
|
||||
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/feldbuskoppler-ethercat/p/750-354#details)
|
||||
|
||||
[WAGO 750-602 Documentation](https://www.wago.com/de/io-systeme/potentialeinspeisung/p/750-602#details)
|
||||
|
||||
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/endmodul/p/750-600#details)
|
||||
@@ -10,9 +10,15 @@ The Qitech Control Panel exposes a small HTTP interface for discovering machines
|
||||
|
||||
## Authentication
|
||||
|
||||
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks.
|
||||
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. It is therefore suggested to run the entire production line in isolated operation.
|
||||
|
||||
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity (if configured). If you need authentication or access from outside the isolated subnet, place a router/device in **client/bridge mode** on the panel network and expose the panel through a **reverse proxy** where you can add authentication, logging, rate limiting, etc.
|
||||
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity. To let the outside world communicate with the production line, a router should be placed at the network bondary bridging the production line's subnet and the rest of the network. The router can run in **client/bridge mode** on the line's subnet and expose the panel's API via **port forwading** or through a **reverse proxy** where one can add authentication, logging, rate limiting, etc.
|
||||
|
||||
The following graphic depicts the suggested network topology for a single production line. Many lines could potentially share the same router.
|
||||
|
||||
<p align="center">
|
||||
<img width="769" height="528" alt="Suggested network topology" src="./assets/subnet.png" />
|
||||
</p>
|
||||
|
||||
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -659,7 +658,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -683,7 +681,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -3283,7 +3280,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
|
||||
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.141.0",
|
||||
"@tanstack/react-store": "^0.8.0",
|
||||
@@ -3374,7 +3370,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
|
||||
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.141.0",
|
||||
"@tanstack/store": "^0.8.0",
|
||||
@@ -3595,7 +3590,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -3761,7 +3757,6 @@
|
||||
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -3778,7 +3773,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3789,7 +3783,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -3877,7 +3870,6 @@
|
||||
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
@@ -4236,7 +4228,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4300,6 +4291,7 @@
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -4614,7 +4606,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4997,8 +4988,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
@@ -5271,7 +5261,8 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
@@ -5817,7 +5808,6 @@
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5878,7 +5868,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -5980,7 +5969,6 @@
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -6939,7 +6927,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -6973,7 +6960,6 @@
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -7576,7 +7562,6 @@
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -8110,6 +8095,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -9529,7 +9515,6 @@
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -9646,6 +9631,7 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -9661,6 +9647,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -9750,7 +9737,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9760,7 +9746,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -9773,7 +9758,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
|
||||
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -9816,7 +9800,8 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
@@ -10326,11 +10311,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz",
|
||||
"integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
|
||||
"integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -10585,6 +10569,7 @@
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
|
||||
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.3.0",
|
||||
@@ -10606,6 +10591,7 @@
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz",
|
||||
"integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -10930,8 +10916,7 @@
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
@@ -11020,7 +11005,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11284,7 +11268,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11621,7 +11604,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -11734,8 +11716,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
|
||||
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
@@ -11773,7 +11754,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -12208,7 +12188,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { useAquapath1 } from "./useAquapath";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Label } from "@/control/Label";
|
||||
import { HeatingZone } from "../../extruder/HeatingZone";
|
||||
|
||||
export function Aquapath1ControlPage() {
|
||||
const {
|
||||
@@ -16,8 +18,12 @@ export function Aquapath1ControlPage() {
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
front_revolutions,
|
||||
back_revolutions,
|
||||
front_power,
|
||||
back_power,
|
||||
front_total_energy,
|
||||
back_total_energy,
|
||||
setAquapathMode,
|
||||
setFrontTemperature,
|
||||
setBackTemperature,
|
||||
@@ -29,111 +35,55 @@ export function Aquapath1ControlPage() {
|
||||
const backTargetTemperature =
|
||||
state?.temperature_states?.back.target_temperature ?? 0;
|
||||
|
||||
const frontCurrentTemperature =
|
||||
state?.temperature_states?.front.temperature ?? 0;
|
||||
const backCurrentTemperature =
|
||||
state?.temperature_states?.back.temperature ?? 0;
|
||||
|
||||
const frontCoolingBoundary =
|
||||
frontTargetTemperature + (state?.tolerance_states.front.cooling ?? 0);
|
||||
const frontHeatingBoundary =
|
||||
frontTargetTemperature - (state?.tolerance_states.front.heating ?? 0);
|
||||
const backCoolingBoundary =
|
||||
backTargetTemperature + (state?.tolerance_states.back.cooling ?? 0);
|
||||
const backHeatingBoundary =
|
||||
backTargetTemperature - (state?.tolerance_states.back.heating ?? 0);
|
||||
|
||||
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
|
||||
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Flow Measurements">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<ControlCard title="Reservoir 1">
|
||||
<div className="grid grid-rows-5 gap-4">
|
||||
<div className="flex flex-row">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Flow 1"
|
||||
label="Flow"
|
||||
unit="l/min"
|
||||
timeseries={front_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-row">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Flow 2"
|
||||
unit="l/min"
|
||||
timeseries={back_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Label label="Pump 1">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={frontTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (value == "On") {
|
||||
setFrontFlow(true);
|
||||
} else {
|
||||
setFrontFlow(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Pump 2">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={backTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (value == "On") {
|
||||
setBackFlow(true);
|
||||
} else {
|
||||
setBackFlow(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Temperature Measurements">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Reservoir 1"
|
||||
label="Temperature"
|
||||
unit="C"
|
||||
timeseries={front_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Reservoir 1 Target Temperature">
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<Label label="Set Target Temperature">
|
||||
<EditValue
|
||||
title="Set Reservoir 1 Target Temperature"
|
||||
title="Set Target Temperature"
|
||||
min={0}
|
||||
value={frontTargetTemperature}
|
||||
max={80}
|
||||
unit="C"
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
max={80.0}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontTemperature(val);
|
||||
@@ -143,23 +93,88 @@ export function Aquapath1ControlPage() {
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{frontCurrentTemperature < frontHeatingBoundary &&
|
||||
(state?.flow_states.front.flow ?? 0) > 0 && (
|
||||
<Badge variant="default">
|
||||
Power: {front_power.current?.value} W<br />
|
||||
Total: {front_total_energy.current?.value} kWh
|
||||
</Badge>
|
||||
)}
|
||||
{frontCurrentTemperature > frontCoolingBoundary &&
|
||||
frontTargetFlow &&
|
||||
(state?.flow_states.front.flow ?? 0) && (
|
||||
<Badge variant="secondary">Cooling</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-row">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Reservoir 2"
|
||||
label="Revolution Speed"
|
||||
unit="%"
|
||||
timeseries={front_revolutions}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<Label label="Pump">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={frontTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
setFrontFlow(value == "On");
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Reservoir 2">
|
||||
<div className="grid grid-rows-4 gap-4">
|
||||
<div className="flex flex-row">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Flow"
|
||||
unit="l/min"
|
||||
timeseries={back_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Temperature"
|
||||
unit="C"
|
||||
timeseries={back_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Reservoir 2 Target Temperature">
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<Label label="Set Target Temperature">
|
||||
<EditValue
|
||||
title="Set Reservoir 2 Target Temperature"
|
||||
title="Set Target Temperature"
|
||||
min={0}
|
||||
value={backTargetTemperature}
|
||||
max={80}
|
||||
unit="C"
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
max={80.0}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackTemperature(val);
|
||||
@@ -169,6 +184,56 @@ export function Aquapath1ControlPage() {
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{backCurrentTemperature < backHeatingBoundary &&
|
||||
backTargetFlow &&
|
||||
(state?.flow_states.front.flow ?? 0) && (
|
||||
<Badge variant="default">
|
||||
Power: {back_power.current?.value} W<br />
|
||||
Total: {back_total_energy.current?.value} kWh
|
||||
</Badge>
|
||||
)}
|
||||
{backCurrentTemperature > backCoolingBoundary &&
|
||||
frontTargetFlow &&
|
||||
(state?.flow_states.front.flow ?? 0) && (
|
||||
<Badge variant="secondary">Cooling</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Revolution Speed"
|
||||
unit="%"
|
||||
timeseries={back_revolutions}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<Label label="Pump">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={backTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
setBackFlow(value == "On");
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
@@ -20,6 +20,12 @@ export function Aquapath1Page() {
|
||||
icon: "lu:ChartSpline",
|
||||
activeLink: "graphs",
|
||||
},
|
||||
{
|
||||
link: "settings",
|
||||
title: "Config",
|
||||
icon: "lu:Settings",
|
||||
activeLink: "settings",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Label } from "@/control/Label";
|
||||
import { useAquapath1 } from "./useAquapath";
|
||||
import React from "react";
|
||||
|
||||
export function Aquapath1SettingsPage() {
|
||||
const {
|
||||
state,
|
||||
setFrontRevolutions,
|
||||
setBackRevolutions,
|
||||
setFrontHeatingTolerance,
|
||||
setFrontCoolingTolerance,
|
||||
setBackHeatingTolerance,
|
||||
setBackCoolingTolerance,
|
||||
} = useAquapath1();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Front Fan Revolutions">
|
||||
<Label label="Set Max Revolution Speed">
|
||||
<EditValue
|
||||
title="Set Max Revolution Speed"
|
||||
min={0}
|
||||
value={state?.fan_states.front.revolutions}
|
||||
max={100}
|
||||
unit="%"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontRevolutions(val);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Back Fan Revolutions">
|
||||
<Label label="Set Max Revolution Speed">
|
||||
<EditValue
|
||||
title="Set Max Revolution Speed"
|
||||
min={0}
|
||||
value={state?.fan_states.back.revolutions}
|
||||
max={100}
|
||||
unit="%"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackRevolutions(val);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Front Temperature Tolerances">
|
||||
<Label label="Set Heating Tolerance">
|
||||
<EditValue
|
||||
title="Set Heating Tolerance"
|
||||
min={0}
|
||||
value={state?.tolerance_states.front.heating}
|
||||
max={10}
|
||||
step={0.1}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontHeatingTolerance(val);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Set Cooling Tolerance">
|
||||
<EditValue
|
||||
title="Set Cooling Tolerance"
|
||||
min={0}
|
||||
value={state?.tolerance_states.front.cooling}
|
||||
max={10}
|
||||
step={0.1}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontCoolingTolerance(val);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Back Temperature Tolerances">
|
||||
<Label label="Set Heating Tolerance">
|
||||
<EditValue
|
||||
title="Set Heating Tolerance"
|
||||
min={0}
|
||||
value={state?.tolerance_states.back.heating}
|
||||
max={10}
|
||||
step={0.1}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackHeatingTolerance(val);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Set Cooling Tolerance">
|
||||
<EditValue
|
||||
title="Set Cooling Tolerance"
|
||||
min={0}
|
||||
value={state?.tolerance_states.back.cooling}
|
||||
max={10}
|
||||
step={0.1}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackCoolingTolerance(val);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -51,6 +51,23 @@ export const flowStatesSchema = z.object({
|
||||
back: flowStateSchema,
|
||||
});
|
||||
|
||||
export const fanStateSchema = z.object({
|
||||
revolutions: z.number(),
|
||||
max_revolutions: z.number(),
|
||||
});
|
||||
export const fanStatesSchema = z.object({
|
||||
back: fanStateSchema,
|
||||
front: fanStateSchema,
|
||||
});
|
||||
|
||||
export const toleranceStateSchema = z.object({
|
||||
heating: z.number(),
|
||||
cooling: z.number(),
|
||||
});
|
||||
export const toleranceStatesSchema = z.object({
|
||||
back: toleranceStateSchema,
|
||||
front: toleranceStateSchema,
|
||||
});
|
||||
/**
|
||||
* Live values event schema (time-series data)
|
||||
*/
|
||||
@@ -61,6 +78,12 @@ export const liveValuesEventDataSchema = z.object({
|
||||
back_temperature: z.number(),
|
||||
front_temp_reservoir: z.number(),
|
||||
back_temp_reservoir: z.number(),
|
||||
front_revolutions: z.number(),
|
||||
back_revolutions: z.number(),
|
||||
back_power: z.number(),
|
||||
front_power: z.number(),
|
||||
front_total_energy: z.number(),
|
||||
back_total_energy: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -71,6 +94,8 @@ export const stateEventDataSchema = z.object({
|
||||
mode_state: modeStateSchema,
|
||||
flow_states: flowStatesSchema,
|
||||
temperature_states: tempStatesSchema,
|
||||
fan_states: fanStatesSchema,
|
||||
tolerance_states: toleranceStatesSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
@@ -96,6 +121,15 @@ export type Aquapath1NamespaceStore = {
|
||||
|
||||
front_temp_reservoir: TimeSeries;
|
||||
back_temp_reservoir: TimeSeries;
|
||||
|
||||
front_revolutions: TimeSeries;
|
||||
back_revolutions: TimeSeries;
|
||||
|
||||
front_power: TimeSeries;
|
||||
back_power: TimeSeries;
|
||||
|
||||
front_total_energy: TimeSeries;
|
||||
back_total_energy: TimeSeries;
|
||||
};
|
||||
|
||||
const { initialTimeSeries: front_temperature, insert: addTemperature1 } =
|
||||
@@ -108,6 +142,18 @@ const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries();
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries();
|
||||
const { initialTimeSeries: front_revolutions, insert: addFan1 } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_revolutions, insert: addFan2 } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_power, insert: addFrontPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_power, insert: addBackPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_total_energy, insert: addFrontEnergy } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_total_energy, insert: addBackEnergy } =
|
||||
createTimeSeries();
|
||||
|
||||
/**
|
||||
* Factory function to create a new Aquapath namespace store
|
||||
@@ -125,6 +171,12 @@ export const createAquapath1NamespaceStore =
|
||||
back_flow: back_flow,
|
||||
front_temp_reservoir: front_temp_reservoir,
|
||||
back_temp_reservoir: back_temp_reservoir,
|
||||
front_revolutions: front_revolutions,
|
||||
back_revolutions: back_revolutions,
|
||||
back_power: back_power,
|
||||
front_power: front_power,
|
||||
front_total_energy: front_total_energy,
|
||||
back_total_energy: back_total_energy,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -192,6 +244,30 @@ export function aquapath1MessageHandler(
|
||||
value: liveValuesEvent.data.back_temp_reservoir,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_revolutions: addFan1(state.front_revolutions, {
|
||||
value: liveValuesEvent.data.front_revolutions,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_revolutions: addFan2(state.back_revolutions, {
|
||||
value: liveValuesEvent.data.back_revolutions,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_power: addFrontPower(state.front_power, {
|
||||
value: liveValuesEvent.data.front_power,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_power: addBackPower(state.back_power, {
|
||||
value: liveValuesEvent.data.back_power,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_total_energy: addFrontEnergy(state.front_total_energy, {
|
||||
value: liveValuesEvent.data.front_total_energy,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_total_energy: addBackEnergy(state.back_total_energy, {
|
||||
value: liveValuesEvent.data.back_total_energy,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
}));
|
||||
} else {
|
||||
handleUnhandledEventError(eventName);
|
||||
|
||||
@@ -42,12 +42,18 @@ export function useAquapath1() {
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
front_temperature: front_temperature,
|
||||
back_temperature: back_temperature,
|
||||
front_flow: front_flow,
|
||||
back_flow: back_flow,
|
||||
front_temp_reservoir: front_temp_reservoir,
|
||||
back_temp_reservoir: back_temp_reservoir,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
front_revolutions,
|
||||
back_revolutions,
|
||||
front_power,
|
||||
back_power,
|
||||
front_total_energy,
|
||||
back_total_energy,
|
||||
} = useAquapath1Namespace(machineIdentification);
|
||||
|
||||
// Single optimistic state for all state management
|
||||
@@ -125,6 +131,89 @@ export function useAquapath1() {
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontRevolutions = (revolutions: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.fan_states.back.revolutions = revolutions;
|
||||
},
|
||||
() => {
|
||||
requestFrontRevolutions({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontRevolutions: revolutions },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setBackRevolutions = (revolutions: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.fan_states.back.revolutions = revolutions;
|
||||
},
|
||||
() =>
|
||||
requestBackRevolutions({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackRevolutions: revolutions },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontHeatingTolerance = (tolerance: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.tolerance_states.front.heating = tolerance;
|
||||
},
|
||||
() => {
|
||||
requestFrontHeatingTolerance({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontHeatingTolerance: tolerance },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setBackHeatingTolerance = (tolerance: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.tolerance_states.back.heating = tolerance;
|
||||
},
|
||||
() => {
|
||||
requestBackHeatingTolerance({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackHeatingTolerance: tolerance },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontCoolingTolerance = (tolerance: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.tolerance_states.front.cooling = tolerance;
|
||||
},
|
||||
() => {
|
||||
requestFrontCoolingTolerance({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontCoolingTolerance: tolerance },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setBackCoolingTolerance = (tolerance: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.tolerance_states.back.cooling = tolerance;
|
||||
},
|
||||
() => {
|
||||
requestBackCoolingTolerance({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackCoolingTolerance: tolerance },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Mutation hooks
|
||||
const { request: requestAquapathMode } = useMachineMutation(
|
||||
z.object({ SetAquaPathMode: z.enum(["Standby", "Auto"]) }),
|
||||
@@ -141,6 +230,25 @@ export function useAquapath1() {
|
||||
const { request: requestBackFlow } = useMachineMutation(
|
||||
z.object({ SetBackFlow: z.boolean() }),
|
||||
);
|
||||
const { request: requestFrontRevolutions } = useMachineMutation(
|
||||
z.object({ SetFrontRevolutions: z.number() }),
|
||||
);
|
||||
const { request: requestBackRevolutions } = useMachineMutation(
|
||||
z.object({ SetBackRevolutions: z.number() }),
|
||||
);
|
||||
const { request: requestFrontHeatingTolerance } = useMachineMutation(
|
||||
z.object({ SetFrontHeatingTolerance: z.number() }),
|
||||
);
|
||||
const { request: requestFrontCoolingTolerance } = useMachineMutation(
|
||||
z.object({ SetFrontCoolingTolerance: z.number() }),
|
||||
);
|
||||
const { request: requestBackHeatingTolerance } = useMachineMutation(
|
||||
z.object({ SetBackHeatingTolerance: z.number() }),
|
||||
);
|
||||
const { request: requestBackCoolingTolerance } = useMachineMutation(
|
||||
z.object({ SetBackCoolingTolerance: z.number() }),
|
||||
);
|
||||
|
||||
// Helper function for optimistic updates using produce
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
@@ -165,11 +273,23 @@ export function useAquapath1() {
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
front_revolutions,
|
||||
back_revolutions,
|
||||
front_power,
|
||||
back_power,
|
||||
front_total_energy,
|
||||
back_total_energy,
|
||||
|
||||
setAquapathMode,
|
||||
setFrontTemperature,
|
||||
setBackTemperature,
|
||||
setFrontFlow,
|
||||
setBackFlow,
|
||||
setFrontRevolutions,
|
||||
setBackRevolutions,
|
||||
setFrontHeatingTolerance,
|
||||
setBackHeatingTolerance,
|
||||
setFrontCoolingTolerance,
|
||||
setBackCoolingTolerance,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { Label } from "@/control/Label";
|
||||
import { useDigitalInputTestMachine } from "./useDigitalInputTestMachine";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { toastError } from "@/components/Toast";
|
||||
|
||||
export function DigitalInputTestMachineControlPage() {
|
||||
const { state } = useDigitalInputTestMachine();
|
||||
|
||||
const safeState = state ?? { led_on: [false, false, false, false] };
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlCard title="Machine LEDs">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{safeState.led_on.map((led, index) => (
|
||||
<Label key={index} label={`LED ${index + 1}`}>
|
||||
<Badge variant={led ? "outline" : "destructive"}>
|
||||
{led ? "On" : "OFF"}
|
||||
</Badge>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</ControlCard>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { digitalInputTestMachineSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function DigitalInputTestMachinePage() {
|
||||
const { serial } = digitalInputTestMachineSerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/digitalInputTestMachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "@/client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
|
||||
// ========== Event Schema ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
led_on: z.array(z.boolean()).length(4),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type DigitalTestMachineNamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
};
|
||||
|
||||
export const createTestMachineNamespaceStore =
|
||||
(): StoreApi<DigitalTestMachineNamespaceStore> =>
|
||||
create<DigitalTestMachineNamespaceStore>(() => ({
|
||||
state: null,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
export function digitalInputTestMachineMessageHandler(
|
||||
store: StoreApi<DigitalTestMachineNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<DigitalTestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const updateStore = (
|
||||
updater: (
|
||||
state: DigitalTestMachineNamespaceStore,
|
||||
) => DigitalTestMachineNamespaceStore,
|
||||
) => throttledUpdater.updateWith(updater);
|
||||
|
||||
try {
|
||||
if (event.name === "StateEvent") {
|
||||
const parsed = stateEventSchema.parse(event);
|
||||
updateStore(() => ({ state: parsed.data }));
|
||||
} else {
|
||||
handleUnhandledEventError(event.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${event.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Namespace Hook ==========
|
||||
const useDigitalInputTestMachineNamespaceImplementation =
|
||||
createNamespaceHookImplementation<DigitalTestMachineNamespaceStore>({
|
||||
createStore: createTestMachineNamespaceStore,
|
||||
createEventHandler: digitalInputTestMachineMessageHandler,
|
||||
});
|
||||
|
||||
export function useDigitalInputTestMachineNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): DigitalTestMachineNamespaceStore {
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
return useDigitalInputTestMachineNamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Toast, toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { digitalInputTestMachineSerialRoute } from "@/routes/routes";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
useDigitalInputTestMachineNamespace,
|
||||
StateEvent,
|
||||
} from "./digitalInputTestMachineNamespace";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { digitalInputTestMachine } from "@/machines/properties";
|
||||
import { z } from "zod";
|
||||
export function useDigitalInputTestMachine() {
|
||||
const { serial: serialString } =
|
||||
digitalInputTestMachineSerialRoute.useParams();
|
||||
|
||||
// Memoize machine identification
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString);
|
||||
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
|
||||
return {
|
||||
machine_identification: { vendor: 0, machine: 0 },
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
machine_identification: digitalInputTestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Namespace state from backend
|
||||
const { state } = useDigitalInputTestMachineNamespace(machineIdentification);
|
||||
|
||||
// Optimistic state
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
useEffect(() => {
|
||||
if (state) stateOptimistic.setReal(state);
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value,
|
||||
};
|
||||
}
|
||||
@@ -66,19 +66,13 @@ export type Laser1NamespaceStore = {
|
||||
roundness: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const HALF_SECOND = 500;
|
||||
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries({
|
||||
sampleIntervalLong: HALF_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries();
|
||||
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
|
||||
{ sampleIntervalLong: HALF_SECOND },
|
||||
);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } =
|
||||
createTimeSeries();
|
||||
|
||||
/**
|
||||
* Factory function to create a new Laser1 namespace store
|
||||
|
||||
@@ -462,6 +462,17 @@ export const testmachine: MachineProperties = {
|
||||
},
|
||||
],
|
||||
};
|
||||
export const digitalInputTestMachine: MachineProperties = {
|
||||
name: "Digital Input Machine",
|
||||
version: "V1",
|
||||
slug: "digitalInputTestMachine",
|
||||
icon: "lu:Disc3",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0040,
|
||||
},
|
||||
device_roles: [],
|
||||
};
|
||||
export const wagoPower1: MachineProperties = {
|
||||
name: "WAGO Power",
|
||||
version: "V1",
|
||||
@@ -533,6 +544,30 @@ export const ip20TestMachine: MachineProperties = {
|
||||
],
|
||||
};
|
||||
|
||||
export const wagoAiTestMachine: MachineProperties = {
|
||||
name: "Wago AI Test",
|
||||
version: "V1",
|
||||
slug: "wagoaitestmachine",
|
||||
icon: "lu:Activity",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0036,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "Wago 750-354 Bus Coupler",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 0x21,
|
||||
product_id: 0x07500354,
|
||||
revision: 0x2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const machineProperties: MachineProperties[] = [
|
||||
winder2,
|
||||
extruder3,
|
||||
@@ -544,6 +579,8 @@ export const machineProperties: MachineProperties[] = [
|
||||
aquapath1,
|
||||
testmachine,
|
||||
analogInputTestMachine,
|
||||
wagoAiTestMachine,
|
||||
digitalInputTestMachine,
|
||||
ip20TestMachine,
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { MiniGraph } from "@/components/graph/MiniGraph";
|
||||
import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
|
||||
import React from "react";
|
||||
import { useWagoAiTestMachine } from "./useWagoAiTestMachine";
|
||||
|
||||
export function WagoAiTestMachineControl(): React.JSX.Element {
|
||||
const { seriesData, state, updateMeasurementRate } = useWagoAiTestMachine();
|
||||
|
||||
if (!seriesData) return <div>Initializing Sensor...</div>;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Measurement Control">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div>Measurement Rate (Hz)</div>
|
||||
<input
|
||||
type="number"
|
||||
style={{
|
||||
border: "2px solid transparent",
|
||||
borderRadius: "4px",
|
||||
padding: "3px",
|
||||
}}
|
||||
className={"bg-neutral-100 shadow"}
|
||||
value={state.measurementRateHz}
|
||||
onChange={(v) => {
|
||||
const val = Number(v.currentTarget.value);
|
||||
updateMeasurementRate(val);
|
||||
}}
|
||||
min={1}
|
||||
></input>
|
||||
</div>
|
||||
</ControlCard>
|
||||
<ControlCard title="Analog Input 1 (4-20mA)">
|
||||
<div className="flex flex-row">
|
||||
<MiniGraph
|
||||
newData={seriesData.ai1}
|
||||
width={400}
|
||||
renderValue={(v) => v.toFixed(2)}
|
||||
></MiniGraph>
|
||||
<div className="ml-4 flex flex-col justify-center">
|
||||
<div>{seriesData.ai1.current?.value.toFixed(2)} mA</div>
|
||||
{state.wiringErrors?.[0] && (
|
||||
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
|
||||
Wiring Error
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
<ControlCard title="Analog Input 2 (4-20mA)">
|
||||
<div className="flex flex-row">
|
||||
<MiniGraph
|
||||
newData={seriesData.ai2}
|
||||
width={400}
|
||||
renderValue={(v) => v.toFixed(2)}
|
||||
></MiniGraph>
|
||||
<div className="ml-4 flex flex-col justify-center">
|
||||
<div>{seriesData.ai2.current?.value.toFixed(2)} mA</div>
|
||||
{state.wiringErrors?.[1] && (
|
||||
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
|
||||
Wiring Error
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
<ControlCard title="Analog Input 3 (4-20mA)">
|
||||
<div className="flex flex-row">
|
||||
<MiniGraph
|
||||
newData={seriesData.ai3}
|
||||
width={400}
|
||||
renderValue={(v) => v.toFixed(2)}
|
||||
></MiniGraph>
|
||||
<div className="ml-4 flex flex-col justify-center">
|
||||
<div>{seriesData.ai3.current?.value.toFixed(2)} mA</div>
|
||||
{state.wiringErrors?.[2] && (
|
||||
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
|
||||
Wiring Error
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
<ControlCard title="Analog Input 4 (4-20mA)">
|
||||
<div className="flex flex-row">
|
||||
<MiniGraph
|
||||
newData={seriesData.ai4}
|
||||
width={400}
|
||||
renderValue={(v) => v.toFixed(2)}
|
||||
></MiniGraph>
|
||||
<div className="ml-4 flex flex-col justify-center">
|
||||
<div>{seriesData.ai4.current?.value.toFixed(2)} mA</div>
|
||||
{state.wiringErrors?.[3] && (
|
||||
<div className="mt-2 rounded bg-red-100 px-2 py-1 text-sm font-semibold text-red-800">
|
||||
Wiring Error
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { wagoAiTestMachineSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function WagoAiTestMachine(): React.JSX.Element {
|
||||
const { serial } = wagoAiTestMachineSerialRoute.useParams();
|
||||
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/wagoaitestmachine/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import {
|
||||
createTimeSeries,
|
||||
TimeSeries,
|
||||
TimeSeriesValue,
|
||||
TimeSeriesWithInsert,
|
||||
} from "@/lib/timeseries";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { wagoAiTestMachineSerialRoute } from "@/routes/routes";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import z from "zod";
|
||||
import { wagoAiTestMachine } from "../properties";
|
||||
import {
|
||||
StateEvent,
|
||||
useWagoAiTestMachineNamespace,
|
||||
} from "./useWagoAiTestMachineNamespace";
|
||||
|
||||
export type AnalogInputTimeSeries = {
|
||||
ai1: TimeSeries;
|
||||
ai2: TimeSeries;
|
||||
ai3: TimeSeries;
|
||||
ai4: TimeSeries;
|
||||
};
|
||||
|
||||
export function useWagoAiTestMachine() {
|
||||
const { serial: serialString } = wagoAiTestMachineSerialRoute.useParams();
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString);
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
return {
|
||||
machine_identification: { vendor: 0, machine: 0 },
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
machine_identification: wagoAiTestMachine.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
const state = useWagoAiTestMachineNamespace({
|
||||
type: "machine",
|
||||
machine_identification_unique: machineIdentification,
|
||||
});
|
||||
|
||||
const mutateMachine = useMachineMutate(
|
||||
z.object({
|
||||
measurement_rate_hz: z.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest?: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState)
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
serverRequest?.();
|
||||
};
|
||||
|
||||
const updateMeasurementRate = (val: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.measurementRateHz = val;
|
||||
},
|
||||
() => {
|
||||
mutateMachine.request({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { measurement_rate_hz: val },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Create time series for each analog input
|
||||
const timeSeriesInstances: {
|
||||
ai1: TimeSeriesWithInsert;
|
||||
ai2: TimeSeriesWithInsert;
|
||||
ai3: TimeSeriesWithInsert;
|
||||
ai4: TimeSeriesWithInsert;
|
||||
} = {
|
||||
ai1: createTimeSeries({
|
||||
sampleIntervalShort: 20,
|
||||
sampleIntervalLong: 1000,
|
||||
retentionDurationShort: 5000,
|
||||
retentionDurationLong: 60 * 60 * 1000,
|
||||
}),
|
||||
ai2: createTimeSeries({
|
||||
sampleIntervalShort: 20,
|
||||
sampleIntervalLong: 1000,
|
||||
retentionDurationShort: 5000,
|
||||
retentionDurationLong: 60 * 60 * 1000,
|
||||
}),
|
||||
ai3: createTimeSeries({
|
||||
sampleIntervalShort: 20,
|
||||
sampleIntervalLong: 1000,
|
||||
retentionDurationShort: 5000,
|
||||
retentionDurationLong: 60 * 60 * 1000,
|
||||
}),
|
||||
ai4: createTimeSeries({
|
||||
sampleIntervalShort: 20,
|
||||
sampleIntervalLong: 1000,
|
||||
retentionDurationShort: 5000,
|
||||
retentionDurationLong: 60 * 60 * 1000,
|
||||
}),
|
||||
};
|
||||
|
||||
const [seriesData, setSeriesData] = useState<AnalogInputTimeSeries | null>({
|
||||
ai1: timeSeriesInstances.ai1.initialTimeSeries,
|
||||
ai2: timeSeriesInstances.ai2.initialTimeSeries,
|
||||
ai3: timeSeriesInstances.ai3.initialTimeSeries,
|
||||
ai4: timeSeriesInstances.ai4.initialTimeSeries,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const analogInputs = state.analogInputs;
|
||||
|
||||
if (analogInputs && seriesData) {
|
||||
const [ai1, ai2, ai3, ai4, timestampStr] = analogInputs;
|
||||
const timestamp = Number(timestampStr);
|
||||
|
||||
const dataPoint1: TimeSeriesValue = {
|
||||
value: ai1 * 1000, // Convert to mA
|
||||
timestamp,
|
||||
};
|
||||
const dataPoint2: TimeSeriesValue = {
|
||||
value: ai2 * 1000,
|
||||
timestamp,
|
||||
};
|
||||
const dataPoint3: TimeSeriesValue = {
|
||||
value: ai3 * 1000,
|
||||
timestamp,
|
||||
};
|
||||
const dataPoint4: TimeSeriesValue = {
|
||||
value: ai4 * 1000,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
setSeriesData({
|
||||
ai1: timeSeriesInstances.ai1.insert(seriesData.ai1, dataPoint1),
|
||||
ai2: timeSeriesInstances.ai2.insert(seriesData.ai2, dataPoint2),
|
||||
ai3: timeSeriesInstances.ai3.insert(seriesData.ai3, dataPoint3),
|
||||
ai4: timeSeriesInstances.ai4.insert(seriesData.ai4, dataPoint4),
|
||||
});
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
seriesData,
|
||||
state,
|
||||
updateMeasurementRate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
createNamespaceHookImplementation,
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
} from "@/client/socketioStore";
|
||||
import z from "zod";
|
||||
import { create, StoreApi } from "zustand";
|
||||
|
||||
// ========== Event Schema ==========
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
measurementRateHz: z.number().optional(),
|
||||
analogInputs: z
|
||||
.tuple([z.number(), z.number(), z.number(), z.number(), z.string()])
|
||||
.optional(),
|
||||
wiringErrors: z
|
||||
.tuple([z.boolean(), z.boolean(), z.boolean(), z.boolean()])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type WagoAiTestMachineNamespaceStore = z.infer<
|
||||
typeof stateEventDataSchema
|
||||
>;
|
||||
|
||||
const createMachineStore = (): StoreApi<WagoAiTestMachineNamespaceStore> =>
|
||||
create<WagoAiTestMachineNamespaceStore>(() => ({
|
||||
measurementRateHz: 1,
|
||||
analogInputs: undefined,
|
||||
wiringErrors: undefined,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
function wagoAiTestMachineMessageHandler(
|
||||
store: StoreApi<WagoAiTestMachineNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event) => {
|
||||
const oldState = store.getState();
|
||||
|
||||
switch (event.name) {
|
||||
case "MeasurementRateHz":
|
||||
{
|
||||
const newMeasurementRateHz = event.data["MeasurementRateHz"];
|
||||
if (
|
||||
newMeasurementRateHz &&
|
||||
newMeasurementRateHz !== oldState.measurementRateHz
|
||||
)
|
||||
store.setState({
|
||||
...oldState,
|
||||
measurementRateHz: newMeasurementRateHz,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "AnalogInputs":
|
||||
{
|
||||
const newAnalogInputs = event.data["AnalogInputs"];
|
||||
if (newAnalogInputs && newAnalogInputs !== oldState.analogInputs)
|
||||
store.setState({
|
||||
...oldState,
|
||||
analogInputs: newAnalogInputs,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "WiringErrors":
|
||||
{
|
||||
const newWiringErrors = event.data["WiringErrors"];
|
||||
if (newWiringErrors && newWiringErrors !== oldState.wiringErrors)
|
||||
store.setState({
|
||||
...oldState,
|
||||
wiringErrors: newWiringErrors,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Namespace Hook ==========
|
||||
export const useWagoAiTestMachineNamespace = createNamespaceHookImplementation({
|
||||
createStore: () => createMachineStore(),
|
||||
createEventHandler: wagoAiTestMachineMessageHandler,
|
||||
});
|
||||
@@ -52,12 +52,19 @@ import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
|
||||
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
|
||||
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
|
||||
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
|
||||
import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Settings";
|
||||
|
||||
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
|
||||
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
|
||||
|
||||
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
|
||||
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
|
||||
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
|
||||
import { WagoAiTestMachineControl } from "@/machines/wagoaitestmachine/WagoAiTestMachineControlPage";
|
||||
|
||||
import { DigitalInputTestMachinePage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachinePage";
|
||||
import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
|
||||
|
||||
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
|
||||
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
|
||||
|
||||
@@ -99,6 +106,30 @@ export const analogInputTestMachineControlRoute = createRoute({
|
||||
component: () => <AnalogInputTestMachineControl />,
|
||||
});
|
||||
|
||||
export const wagoAiTestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "wagoaitestmachine/$serial",
|
||||
component: () => <WagoAiTestMachine />,
|
||||
});
|
||||
|
||||
export const wagoAiTestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => wagoAiTestMachineSerialRoute,
|
||||
path: "control",
|
||||
component: () => <WagoAiTestMachineControl />,
|
||||
});
|
||||
|
||||
export const digitalInputTestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "digitalInputTestMachine/$serial",
|
||||
component: () => <DigitalInputTestMachinePage />,
|
||||
});
|
||||
|
||||
export const digitalInputTestMachineControlRoute = createRoute({
|
||||
getParentRoute: () => digitalInputTestMachineSerialRoute,
|
||||
path: "control",
|
||||
component: () => <DigitalInputTestMachineControlPage />,
|
||||
});
|
||||
|
||||
export const ip20TestMachineSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "ip20testmachine/$serial",
|
||||
@@ -314,6 +345,12 @@ export const aquapath1ControlRoute = createRoute({
|
||||
component: () => <Aquapath1ControlPage />,
|
||||
});
|
||||
|
||||
export const aquapath1SettingsRoute = createRoute({
|
||||
getParentRoute: () => aquapath1SerialRoute,
|
||||
path: "settings",
|
||||
component: () => <Aquapath1SettingsPage />,
|
||||
});
|
||||
|
||||
export const buffer1SettingsRoute = createRoute({
|
||||
getParentRoute: () => buffer1SerialRoute,
|
||||
path: "settings",
|
||||
@@ -439,11 +476,18 @@ export const rootTree = RootRoute.addChildren([
|
||||
analogInputTestMachineControlRoute,
|
||||
]),
|
||||
|
||||
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
|
||||
|
||||
digitalInputTestMachineSerialRoute.addChildren([
|
||||
digitalInputTestMachineControlRoute,
|
||||
]),
|
||||
|
||||
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
aquapath1ControlRoute,
|
||||
aquapath1GraphRoute,
|
||||
aquapath1SettingsRoute,
|
||||
]),
|
||||
|
||||
winder2SerialRoute.addChildren([
|
||||
|
||||
@@ -15,7 +15,13 @@ import {
|
||||
machineProperties,
|
||||
VENDOR_QITECH,
|
||||
} from "@/machines/properties";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -150,6 +156,15 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const updateNumpadPosition = useCallback(() => {
|
||||
if (!numpadOpen || !dialogRef.current) return;
|
||||
const rect = dialogRef.current.getBoundingClientRect();
|
||||
setNumpadPosition({
|
||||
left: rect.right + 20,
|
||||
top: rect.top + rect.height / 2,
|
||||
});
|
||||
}, [numpadOpen]);
|
||||
|
||||
const machinePreset = useMemo(() => {
|
||||
if (!values.machine) return;
|
||||
return getMachineProperties({
|
||||
@@ -187,12 +202,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
|
||||
// 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,
|
||||
});
|
||||
updateNumpadPosition();
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Close numpad when clicking outside input/numpad
|
||||
@@ -226,6 +236,14 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
}
|
||||
}, [numpadOpen]);
|
||||
|
||||
// Update numpad position whenever the window resizes
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", updateNumpadPosition);
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateNumpadPosition);
|
||||
};
|
||||
}, [updateNumpadPosition]);
|
||||
|
||||
// Numpad handlers for serial input
|
||||
const numpadHandlers = useMemo(() => {
|
||||
const ensureFocus = () => {
|
||||
|
||||
@@ -7,6 +7,8 @@ use crate::{
|
||||
devices::{
|
||||
DynamicEthercatDevice, Module,
|
||||
wago_modules::{
|
||||
wago_750_402::{WAGO_750_402_MODULE_IDENT, WAGO_750_402_PRODUCT_ID},
|
||||
wago_750_455::{WAGO_750_455_MODULE_IDENT, WAGO_750_455_PRODUCT_ID},
|
||||
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_PRODUCT_ID},
|
||||
wago_750_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
|
||||
wago_750_652::{WAGO_750_652_MODULE_IDENT, WAGO_750_652_PRODUCT_ID},
|
||||
@@ -228,6 +230,10 @@ impl Wago750_354 {
|
||||
module.has_tx = true;
|
||||
module.has_rx = true;
|
||||
}
|
||||
WAGO_750_455_PRODUCT_ID => {
|
||||
module.has_tx = true;
|
||||
module.has_rx = false;
|
||||
}
|
||||
WAGO_750_501_PRODUCT_ID => {
|
||||
module.has_tx = false;
|
||||
module.has_rx = true;
|
||||
@@ -240,6 +246,10 @@ impl Wago750_354 {
|
||||
module.has_tx = true;
|
||||
module.has_rx = true;
|
||||
}
|
||||
WAGO_750_402_PRODUCT_ID => {
|
||||
module.has_tx = true;
|
||||
module.has_rx = false;
|
||||
}
|
||||
_ => println!(
|
||||
"Wago-750-354 found Unknown/Unimplemented Module: {}",
|
||||
ident_iom
|
||||
@@ -274,6 +284,9 @@ impl Wago750_354 {
|
||||
m.vendor_id,
|
||||
m.product_id,
|
||||
) {
|
||||
WAGO_750_455_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_455::Wago750_455::new()))
|
||||
}
|
||||
WAGO_750_501_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_501::Wago750_501::new()))
|
||||
}
|
||||
@@ -286,6 +299,9 @@ impl Wago750_354 {
|
||||
WAGO_750_652_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
|
||||
}
|
||||
WAGO_750_402_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
|
||||
|
||||
@@ -430,6 +430,9 @@ impl IP20EcDi8Do8 {
|
||||
wago_750_652::WAGO_750_652_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
|
||||
}
|
||||
wago_750_402::WAGO_750_402_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
|
||||
}
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod ip20_ec_di8_do8;
|
||||
pub mod wago_750_1506;
|
||||
pub mod wago_750_402;
|
||||
pub mod wago_750_455;
|
||||
pub mod wago_750_501;
|
||||
pub mod wago_750_530;
|
||||
pub mod wago_750_652;
|
||||
@@ -0,0 +1,194 @@
|
||||
use crate::{
|
||||
devices::{
|
||||
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
|
||||
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
|
||||
},
|
||||
io::digital_input::{DigitalInputDevice, DigitalInputInput},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Wago750_402InputPort {
|
||||
DI1,
|
||||
DI2,
|
||||
DI3,
|
||||
DI4,
|
||||
}
|
||||
|
||||
impl From<Wago750_402InputPort> for usize {
|
||||
fn from(value: Wago750_402InputPort) -> Self {
|
||||
match value {
|
||||
Wago750_402InputPort::DI1 => 0,
|
||||
Wago750_402InputPort::DI2 => 1,
|
||||
Wago750_402InputPort::DI3 => 2,
|
||||
Wago750_402InputPort::DI4 => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Wago750_402TxPdo {
|
||||
port1: bool,
|
||||
port2: bool,
|
||||
port3: bool,
|
||||
port4: bool,
|
||||
}
|
||||
|
||||
impl DigitalInputDevice<Wago750_402InputPort> for Wago750_402 {
|
||||
fn get_input(&self, port: Wago750_402InputPort) -> Result<DigitalInputInput, anyhow::Error> {
|
||||
Ok(DigitalInputInput {
|
||||
value: match port {
|
||||
Wago750_402InputPort::DI1 => self.tx_pdo.port1,
|
||||
Wago750_402InputPort::DI2 => self.tx_pdo.port2,
|
||||
Wago750_402InputPort::DI3 => self.tx_pdo.port3,
|
||||
Wago750_402InputPort::DI4 => self.tx_pdo.port4,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wago750_402 {
|
||||
is_used: bool,
|
||||
tx_bit_offset: usize,
|
||||
// Should always be on but not when calling a constructor
|
||||
module: Option<Module>,
|
||||
tx_pdo: Wago750_402TxPdo,
|
||||
}
|
||||
|
||||
impl DynamicEthercatDevice for Wago750_402 {}
|
||||
|
||||
impl EthercatDynamicPDO for Wago750_402 {
|
||||
fn get_tx_offset(&self) -> usize {
|
||||
self.tx_bit_offset
|
||||
}
|
||||
|
||||
fn get_rx_offset(&self) -> usize {
|
||||
0 //this device has no rx
|
||||
}
|
||||
|
||||
fn set_tx_offset(&mut self, offset: usize) {
|
||||
self.tx_bit_offset = offset
|
||||
}
|
||||
|
||||
fn set_rx_offset(&mut self, _offset: usize) {
|
||||
// it does nothing because the device has no rx
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceUsed for Wago750_402 {
|
||||
fn is_used(&self) -> bool {
|
||||
self.is_used
|
||||
}
|
||||
|
||||
fn set_used(&mut self, used: bool) {
|
||||
self.is_used = used;
|
||||
}
|
||||
}
|
||||
|
||||
impl Wago750_402 {}
|
||||
|
||||
impl EthercatDevice for Wago750_402 {
|
||||
// input_len 48 offset: 40
|
||||
fn input(
|
||||
&mut self,
|
||||
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let base = self.tx_bit_offset;
|
||||
|
||||
let idx1 = base + Into::<usize>::into(Wago750_402InputPort::DI1);
|
||||
self.tx_pdo.port1 = *input.get(idx1).expect("Bit 1 out of bounds");
|
||||
|
||||
let idx2 = base + Into::<usize>::into(Wago750_402InputPort::DI2);
|
||||
self.tx_pdo.port2 = *input.get(idx2).expect("Bit 2 out of bounds");
|
||||
|
||||
let idx3 = base + Into::<usize>::into(Wago750_402InputPort::DI3);
|
||||
self.tx_pdo.port3 = *input.get(idx3).expect("Bit 3 out of bounds");
|
||||
|
||||
let idx4 = base + Into::<usize>::into(Wago750_402InputPort::DI4);
|
||||
self.tx_pdo.port4 = *input.get(idx4).expect("Bit 4 out of bounds");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_len(&self) -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn output(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// println!("output: {} {}",self.rx_bit_offset,_output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn is_module(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn input_checked(
|
||||
&mut self,
|
||||
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_checked(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_module(&self) -> Option<Module> {
|
||||
self.module.clone()
|
||||
}
|
||||
|
||||
fn set_module(&mut self, module: Module) {
|
||||
self.tx_bit_offset = module.tx_offset;
|
||||
self.module = Some(module);
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceProcessing for Wago750_402 {
|
||||
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl NewEthercatDevice for Wago750_402 {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
is_used: false,
|
||||
tx_bit_offset: 0,
|
||||
module: None,
|
||||
tx_pdo: Wago750_402TxPdo::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Wago750_402 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Wago750_402")
|
||||
}
|
||||
}
|
||||
|
||||
pub const WAGO_750_402_VENDOR_ID: u32 = 0x00000021;
|
||||
pub const WAGO_750_402_PRODUCT_ID: u32 = 2147483713;
|
||||
pub const WAGO_750_402_MODULE_IDENT: SubDeviceProductTuple =
|
||||
(WAGO_750_402_VENDOR_ID, WAGO_750_402_PRODUCT_ID);
|
||||
@@ -0,0 +1,198 @@
|
||||
use bitvec::field::BitField;
|
||||
|
||||
use crate::devices::{
|
||||
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
|
||||
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
|
||||
};
|
||||
use crate::io::analog_input::physical::AnalogInputRange;
|
||||
use crate::io::analog_input::{AnalogInputDevice, AnalogInputInput};
|
||||
use units::electric_current::milliampere;
|
||||
use units::f64::ElectricCurrent;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Wago750_455Port {
|
||||
AI1,
|
||||
AI2,
|
||||
AI3,
|
||||
AI4,
|
||||
}
|
||||
|
||||
impl From<Wago750_455Port> for usize {
|
||||
fn from(value: Wago750_455Port) -> Self {
|
||||
match value {
|
||||
Wago750_455Port::AI1 => 0,
|
||||
Wago750_455Port::AI2 => 16,
|
||||
Wago750_455Port::AI3 => 32,
|
||||
Wago750_455Port::AI4 => 48,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Wago750_455TxPdo {
|
||||
pub ai1: u16,
|
||||
pub ai2: u16,
|
||||
pub ai3: u16,
|
||||
pub ai4: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wago750_455 {
|
||||
is_used: bool,
|
||||
tx_bit_offset: usize,
|
||||
rx_bit_offset: usize,
|
||||
module: Option<Module>,
|
||||
tx_pdo: Wago750_455TxPdo,
|
||||
}
|
||||
|
||||
impl AnalogInputDevice<Wago750_455Port> for Wago750_455 {
|
||||
fn get_input(&self, port: Wago750_455Port) -> AnalogInputInput {
|
||||
let raw = match port {
|
||||
Wago750_455Port::AI1 => self.tx_pdo.ai1,
|
||||
Wago750_455Port::AI2 => self.tx_pdo.ai2,
|
||||
Wago750_455Port::AI3 => self.tx_pdo.ai3,
|
||||
Wago750_455Port::AI4 => self.tx_pdo.ai4,
|
||||
};
|
||||
let wiring_error = (raw & 0x0003) == 0x0003;
|
||||
let raw_value = (raw & 0x7FF0) as i16;
|
||||
let normalized = self.analog_input_range().raw_to_normalized(raw_value) as f32;
|
||||
AnalogInputInput {
|
||||
normalized,
|
||||
wiring_error,
|
||||
}
|
||||
}
|
||||
|
||||
fn analog_input_range(&self) -> AnalogInputRange {
|
||||
AnalogInputRange::Current {
|
||||
min: ElectricCurrent::new::<milliampere>(4.0),
|
||||
max: ElectricCurrent::new::<milliampere>(20.0),
|
||||
min_raw: 0,
|
||||
max_raw: 0x7FF0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceUsed for Wago750_455 {
|
||||
fn is_used(&self) -> bool {
|
||||
self.is_used
|
||||
}
|
||||
|
||||
fn set_used(&mut self, used: bool) {
|
||||
self.is_used = used;
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicEthercatDevice for Wago750_455 {}
|
||||
|
||||
impl EthercatDynamicPDO for Wago750_455 {
|
||||
fn get_tx_offset(&self) -> usize {
|
||||
self.tx_bit_offset
|
||||
}
|
||||
|
||||
fn get_rx_offset(&self) -> usize {
|
||||
self.rx_bit_offset
|
||||
}
|
||||
|
||||
fn set_tx_offset(&mut self, offset: usize) {
|
||||
self.tx_bit_offset = offset
|
||||
}
|
||||
|
||||
fn set_rx_offset(&mut self, offset: usize) {
|
||||
self.rx_bit_offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDevice for Wago750_455 {
|
||||
fn input(
|
||||
&mut self,
|
||||
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let base = self.tx_bit_offset;
|
||||
let ai1 = input[base..(base + 16)].load_le::<u16>();
|
||||
let ai2 = input[(base + 16)..(base + 32)].load_le::<u16>();
|
||||
let ai3 = input[(base + 32)..(base + 48)].load_le::<u16>();
|
||||
let ai4 = input[(base + 48)..(base + 64)].load_le::<u16>();
|
||||
|
||||
self.tx_pdo.ai1 = ai1;
|
||||
self.tx_pdo.ai2 = ai2;
|
||||
self.tx_pdo.ai3 = ai3;
|
||||
self.tx_pdo.ai4 = ai4;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_len(&self) -> usize {
|
||||
64
|
||||
}
|
||||
|
||||
fn output(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn is_module(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn input_checked(
|
||||
&mut self,
|
||||
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.input(input)
|
||||
}
|
||||
|
||||
fn output_checked(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_module(&self) -> Option<Module> {
|
||||
self.module.clone()
|
||||
}
|
||||
|
||||
fn set_module(&mut self, module: Module) {
|
||||
self.tx_bit_offset = module.tx_offset;
|
||||
self.rx_bit_offset = module.rx_offset;
|
||||
self.module = Some(module)
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceProcessing for Wago750_455 {}
|
||||
|
||||
impl NewEthercatDevice for Wago750_455 {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
is_used: false,
|
||||
tx_bit_offset: 0,
|
||||
rx_bit_offset: 0,
|
||||
module: None,
|
||||
tx_pdo: Wago750_455TxPdo::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Wago750_455 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Wago750_455")
|
||||
}
|
||||
}
|
||||
|
||||
pub const WAGO_750_455_VENDOR_ID: u32 = 0x00000021;
|
||||
pub const WAGO_750_455_PRODUCT_ID: u32 = 0x045541b3;
|
||||
pub const WAGO_750_455_MODULE_IDENT: SubDeviceProductTuple =
|
||||
(WAGO_750_455_VENDOR_ID, WAGO_750_455_PRODUCT_ID);
|
||||
@@ -20,6 +20,12 @@ pub struct LiveValuesEvent {
|
||||
pub back_temperature: f64,
|
||||
pub front_temp_reservoir: f64,
|
||||
pub back_temp_reservoir: f64,
|
||||
pub front_revolutions: f64,
|
||||
pub back_revolutions: f64,
|
||||
pub front_power: f64,
|
||||
pub back_power: f64,
|
||||
pub front_total_energy: f64,
|
||||
pub back_total_energy: f64,
|
||||
}
|
||||
|
||||
impl LiveValuesEvent {
|
||||
@@ -35,6 +41,8 @@ pub struct StateEvent {
|
||||
pub mode_state: ModeState,
|
||||
pub flow_states: FlowStates,
|
||||
pub temperature_states: TempStates,
|
||||
pub fan_states: FanStates,
|
||||
pub tolerance_states: ToleranceStates,
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
@@ -70,6 +78,28 @@ pub struct FlowState {
|
||||
pub should_flow: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct FanState {
|
||||
pub revolutions: f64,
|
||||
pub max_revolutions: f64,
|
||||
}
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct FanStates {
|
||||
pub front: FanState,
|
||||
pub back: FanState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ToleranceState {
|
||||
pub heating: f64,
|
||||
pub cooling: f64,
|
||||
}
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ToleranceStates {
|
||||
pub front: ToleranceState,
|
||||
pub back: ToleranceState,
|
||||
}
|
||||
|
||||
pub enum AquaPathV1Events {
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
State(Event<StateEvent>),
|
||||
@@ -85,6 +115,14 @@ enum Mutation {
|
||||
|
||||
SetFrontFlow(bool),
|
||||
SetBackFlow(bool),
|
||||
|
||||
SetFrontRevolutions(f64),
|
||||
SetBackRevolutions(f64),
|
||||
|
||||
SetFrontHeatingTolerance(f64),
|
||||
SetBackHeatingTolerance(f64),
|
||||
SetFrontCoolingTolerance(f64),
|
||||
SetBackCoolingTolerance(f64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -145,6 +183,24 @@ impl MachineApi for AquaPathV1 {
|
||||
Mutation::SetFrontFlow(should_pump) => {
|
||||
self.set_should_pump(should_pump, super::AquaPathSideType::Front)
|
||||
}
|
||||
Mutation::SetBackRevolutions(revolution) => {
|
||||
self.set_max_revolutions(revolution, super::AquaPathSideType::Back)
|
||||
}
|
||||
Mutation::SetFrontRevolutions(revolutions) => {
|
||||
self.set_max_revolutions(revolutions, super::AquaPathSideType::Front)
|
||||
}
|
||||
Mutation::SetBackHeatingTolerance(tolerance) => {
|
||||
self.set_heating_tolerance(tolerance, super::AquaPathSideType::Back)
|
||||
}
|
||||
Mutation::SetFrontHeatingTolerance(tolerance) => {
|
||||
self.set_heating_tolerance(tolerance, super::AquaPathSideType::Front)
|
||||
}
|
||||
Mutation::SetBackCoolingTolerance(tolerance) => {
|
||||
self.set_cooling_tolerance(tolerance, super::AquaPathSideType::Back);
|
||||
}
|
||||
Mutation::SetFrontCoolingTolerance(tolerance) => {
|
||||
self.set_cooling_tolerance(tolerance, super::AquaPathSideType::Front);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ use ethercat_hal::io::encoder_input::EncoderInput;
|
||||
use ethercat_hal::io::{
|
||||
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
use units::AngularVelocity;
|
||||
use units::angular_velocity::revolution_per_minute;
|
||||
use units::f64::ThermodynamicTemperature;
|
||||
use units::thermodynamic_temperature::degree_celsius;
|
||||
use units::thermodynamic_temperature::{degree_celsius, kelvin};
|
||||
use units::volume_rate::liter_per_minute;
|
||||
#[derive(Debug)]
|
||||
|
||||
pub struct Controller {
|
||||
pub pid: PidController,
|
||||
temperature_pid_output: f64,
|
||||
window_start: Instant,
|
||||
pwm_period: Duration,
|
||||
|
||||
pub temperature: Temperature,
|
||||
pub target_temperature: ThermodynamicTemperature,
|
||||
@@ -27,10 +27,19 @@ pub struct Controller {
|
||||
pub cooling_controller: AnalogOutput,
|
||||
pub cooling_relais: DigitalOutput,
|
||||
|
||||
pub cooling_tolerance: ThermodynamicTemperature,
|
||||
pub heating_tolerance: ThermodynamicTemperature,
|
||||
|
||||
pub current_revolutions: AngularVelocity,
|
||||
pub max_revolutions: AngularVelocity,
|
||||
|
||||
pub heating_relais_1: DigitalOutput,
|
||||
pub temperature_sensor_in: TemperatureInput,
|
||||
pub temperature_sensor_out: TemperatureInput,
|
||||
|
||||
pub power: f64,
|
||||
pub total_energy: f64,
|
||||
|
||||
pub cooling_allowed: bool,
|
||||
pub heating_allowed: bool,
|
||||
|
||||
@@ -48,7 +57,6 @@ impl Controller {
|
||||
kp: f64,
|
||||
ki: f64,
|
||||
kd: f64,
|
||||
pwm_duration: Duration,
|
||||
temp: Temperature,
|
||||
target_tempetature: ThermodynamicTemperature,
|
||||
cooling_controller: AnalogOutput,
|
||||
@@ -56,6 +64,7 @@ impl Controller {
|
||||
heating_relais_1: DigitalOutput,
|
||||
temp_sensor_in: TemperatureInput,
|
||||
temp_sensor_out: TemperatureInput,
|
||||
max_revolutions: AngularVelocity,
|
||||
|
||||
flow: Flow,
|
||||
pump_relais: DigitalOutput,
|
||||
@@ -63,9 +72,7 @@ impl Controller {
|
||||
) -> Self {
|
||||
Self {
|
||||
pid: PidController::new(kp, ki, kd),
|
||||
temperature_pid_output: 0.0,
|
||||
window_start: Instant::now(),
|
||||
pwm_period: pwm_duration,
|
||||
target_temperature: target_tempetature,
|
||||
current_temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
temp_reservoir: ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
@@ -74,13 +81,24 @@ impl Controller {
|
||||
|
||||
temperature: temp,
|
||||
cooling_controller: cooling_controller,
|
||||
|
||||
cooling_tolerance: ThermodynamicTemperature::new::<degree_celsius>(2.0),
|
||||
heating_tolerance: ThermodynamicTemperature::new::<degree_celsius>(2.0),
|
||||
|
||||
current_revolutions: AngularVelocity::new::<revolution_per_minute>(0.0),
|
||||
max_revolutions: max_revolutions,
|
||||
|
||||
cooling_relais: cooling_relais,
|
||||
heating_relais_1: heating_relais_1,
|
||||
|
||||
cooling_allowed: false,
|
||||
heating_allowed: false,
|
||||
temperature_sensor_in: temp_sensor_in,
|
||||
temperature_sensor_out: temp_sensor_out,
|
||||
|
||||
power: 700.0,
|
||||
total_energy: 0.0,
|
||||
|
||||
flow: flow,
|
||||
pump_relais: pump_relais,
|
||||
flow_sensor: flow_sensor,
|
||||
@@ -174,12 +192,12 @@ impl Controller {
|
||||
|
||||
pub fn turn_cooling_on(&mut self) {
|
||||
self.cooling_relais.set(true);
|
||||
self.cooling_controller.set(10.0);
|
||||
self.temperature.cooling = true;
|
||||
}
|
||||
|
||||
pub fn turn_cooling_off(&mut self) {
|
||||
self.cooling_relais.set(false);
|
||||
self.current_revolutions = AngularVelocity::new::<revolution_per_minute>(0.0);
|
||||
self.temperature.cooling = false;
|
||||
}
|
||||
|
||||
@@ -191,6 +209,7 @@ impl Controller {
|
||||
pub fn turn_heating_off(&mut self) {
|
||||
self.heating_relais_1.set(false);
|
||||
self.temperature.heating = false;
|
||||
self.power = 0.0;
|
||||
}
|
||||
|
||||
pub fn set_should_pump(&mut self, should_pump: bool) {
|
||||
@@ -224,6 +243,35 @@ impl Controller {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_revolutions(&self) -> AngularVelocity {
|
||||
self.current_revolutions
|
||||
}
|
||||
|
||||
pub fn get_max_revolutions(&self) -> AngularVelocity {
|
||||
self.max_revolutions
|
||||
}
|
||||
|
||||
pub fn set_max_revolutions(&mut self, revolutions: AngularVelocity) {
|
||||
self.max_revolutions = revolutions;
|
||||
}
|
||||
|
||||
pub fn set_cooling_tolerance(&mut self, tolerance: ThermodynamicTemperature) {
|
||||
self.cooling_tolerance = tolerance;
|
||||
}
|
||||
|
||||
pub fn set_heating_tolerance(&mut self, tolerance: ThermodynamicTemperature) {
|
||||
self.heating_tolerance = tolerance;
|
||||
}
|
||||
|
||||
// no power/energy unit implemented
|
||||
pub fn get_current_power(&self) -> f64 {
|
||||
self.power
|
||||
}
|
||||
|
||||
pub fn get_total_energy(&self) -> f64 {
|
||||
self.total_energy
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant) -> () {
|
||||
let current_flow = self.get_flow();
|
||||
self.current_flow = current_flow;
|
||||
@@ -250,43 +298,50 @@ impl Controller {
|
||||
// Calculate PID error once
|
||||
let error = self.target_temperature.get::<degree_celsius>()
|
||||
- self.current_temperature.get::<degree_celsius>();
|
||||
let control = self.pid.update(error, now);
|
||||
self.temperature_pid_output = control;
|
||||
|
||||
let elapsed = now.duration_since(self.window_start);
|
||||
if elapsed >= self.pwm_period {
|
||||
self.window_start = now;
|
||||
}
|
||||
let elapsed = now - self.window_start;
|
||||
self.window_start = now;
|
||||
|
||||
// Decide whether to heat or cool based on error
|
||||
if error > 0.0 {
|
||||
if error > self.heating_tolerance.get::<degree_celsius>() {
|
||||
// Need heating (current < target)
|
||||
if self.temperature.cooling {
|
||||
self.turn_cooling_off();
|
||||
}
|
||||
if self.heating_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
|
||||
// Only start heating if pump is on
|
||||
let duty = control.clamp(0.0, 1.0);
|
||||
let on_time = self.pwm_period.mul_f64(duty);
|
||||
let on = elapsed < on_time;
|
||||
if on && !self.temperature.heating {
|
||||
self.turn_heating_on();
|
||||
} else if !on && self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
self.turn_heating_on();
|
||||
|
||||
self.total_energy += self.get_current_power() * elapsed.as_secs_f64() / 3600.0;
|
||||
} else {
|
||||
// Pump is off or heating not allowed - don't heat
|
||||
if self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if error < -self.cooling_tolerance.get::<degree_celsius>() {
|
||||
// Need cooling (current > target)
|
||||
if self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
if self.cooling_allowed && !self.temperature.cooling {
|
||||
self.turn_cooling_on();
|
||||
if self.cooling_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
|
||||
if !self.temperature.cooling {
|
||||
self.turn_cooling_on();
|
||||
}
|
||||
|
||||
let max_revolutions = self.get_max_revolutions();
|
||||
let temp_offset = self.current_temperature - self.target_temperature;
|
||||
|
||||
let target_revolutions = (temp_offset.get::<kelvin>() * 10.0)
|
||||
.clamp(0.0, max_revolutions.get::<revolution_per_minute>());
|
||||
|
||||
self.cooling_controller
|
||||
.set(target_revolutions as f32 / 10.0);
|
||||
self.current_revolutions =
|
||||
AngularVelocity::new::<revolution_per_minute>(target_revolutions);
|
||||
} else {
|
||||
if self.temperature.cooling {
|
||||
self.turn_cooling_off();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use api::{ToleranceState, ToleranceStates};
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use units::angular_velocity::revolution_per_minute;
|
||||
use units::f64::*;
|
||||
use units::{thermodynamic_temperature::degree_celsius, volume_rate::liter_per_minute};
|
||||
|
||||
@@ -9,8 +11,8 @@ use crate::{
|
||||
MACHINE_AQUAPATH_V1, VENDOR_QITECH,
|
||||
aquapath1::{
|
||||
api::{
|
||||
AquaPathV1Events, AquaPathV1Namespace, FlowState, FlowStates, LiveValuesEvent,
|
||||
ModeState, StateEvent, TempState, TempStates,
|
||||
AquaPathV1Events, AquaPathV1Namespace, FanState, FanStates, FlowState, FlowStates,
|
||||
LiveValuesEvent, ModeState, StateEvent, TempState, TempStates,
|
||||
},
|
||||
controller::Controller,
|
||||
},
|
||||
@@ -123,6 +125,18 @@ impl AquaPathV1 {
|
||||
back_flow: self.back_controller.current_flow.get::<liter_per_minute>(),
|
||||
front_temp_reservoir: self.front_controller.temp_reservoir.get::<degree_celsius>(),
|
||||
back_temp_reservoir: self.back_controller.temp_reservoir.get::<degree_celsius>(),
|
||||
front_revolutions: self
|
||||
.front_controller
|
||||
.current_revolutions
|
||||
.get::<revolution_per_minute>(),
|
||||
back_revolutions: self
|
||||
.back_controller
|
||||
.current_revolutions
|
||||
.get::<revolution_per_minute>(),
|
||||
front_power: self.front_controller.get_current_power(),
|
||||
back_power: self.back_controller.get_current_power(),
|
||||
front_total_energy: self.front_controller.get_total_energy(),
|
||||
back_total_energy: self.back_controller.get_total_energy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +184,50 @@ impl AquaPathV1 {
|
||||
should_flow: self.back_controller.should_pump,
|
||||
},
|
||||
},
|
||||
fan_states: FanStates {
|
||||
front: FanState {
|
||||
revolutions: self
|
||||
.front_controller
|
||||
.max_revolutions
|
||||
.get::<revolution_per_minute>(),
|
||||
max_revolutions: self
|
||||
.front_controller
|
||||
.max_revolutions
|
||||
.get::<revolution_per_minute>(),
|
||||
},
|
||||
back: FanState {
|
||||
revolutions: self
|
||||
.back_controller
|
||||
.max_revolutions
|
||||
.get::<revolution_per_minute>(),
|
||||
max_revolutions: self
|
||||
.back_controller
|
||||
.max_revolutions
|
||||
.get::<revolution_per_minute>(),
|
||||
},
|
||||
},
|
||||
tolerance_states: ToleranceStates {
|
||||
front: ToleranceState {
|
||||
heating: self
|
||||
.front_controller
|
||||
.heating_tolerance
|
||||
.get::<degree_celsius>(),
|
||||
cooling: self
|
||||
.front_controller
|
||||
.cooling_tolerance
|
||||
.get::<degree_celsius>(),
|
||||
},
|
||||
back: ToleranceState {
|
||||
heating: self
|
||||
.back_controller
|
||||
.heating_tolerance
|
||||
.get::<degree_celsius>(),
|
||||
cooling: self
|
||||
.back_controller
|
||||
.cooling_tolerance
|
||||
.get::<degree_celsius>(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,3 +332,45 @@ impl AquaPathV1 {
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
fn set_max_revolutions(&mut self, revolutions: f64, fan_type: AquaPathSideType) {
|
||||
match fan_type {
|
||||
AquaPathSideType::Back => self
|
||||
.back_controller
|
||||
.set_max_revolutions(AngularVelocity::new::<revolution_per_minute>(revolutions)),
|
||||
AquaPathSideType::Front => self
|
||||
.front_controller
|
||||
.set_max_revolutions(AngularVelocity::new::<revolution_per_minute>(revolutions)),
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
fn set_heating_tolerance(&mut self, tolerance: f64, tolerance_type: AquaPathSideType) {
|
||||
match tolerance_type {
|
||||
AquaPathSideType::Back => self
|
||||
.back_controller
|
||||
.set_heating_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
|
||||
AquaPathSideType::Front => self
|
||||
.front_controller
|
||||
.set_heating_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
fn set_cooling_tolerance(&mut self, tolerance: f64, tolerance_type: AquaPathSideType) {
|
||||
match tolerance_type {
|
||||
AquaPathSideType::Back => self
|
||||
.back_controller
|
||||
.set_cooling_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
|
||||
AquaPathSideType::Front => self
|
||||
.front_controller
|
||||
.set_cooling_tolerance(ThermodynamicTemperature::new::<degree_celsius>(tolerance)),
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,12 @@ use ethercat_hal::{
|
||||
temperature_input::TemperatureInput,
|
||||
},
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
|
||||
use std::time::Instant;
|
||||
use units::{
|
||||
AngularVelocity,
|
||||
angular_velocity::revolution_per_minute,
|
||||
thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius},
|
||||
};
|
||||
|
||||
impl MachineNewTrait for AquaPathV1 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
@@ -131,7 +135,6 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
0.10,
|
||||
0.0,
|
||||
0.015,
|
||||
Duration::from_millis(500),
|
||||
Temperature::default(),
|
||||
ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
ao1,
|
||||
@@ -139,6 +142,7 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
do2,
|
||||
t1,
|
||||
t2,
|
||||
AngularVelocity::new::<revolution_per_minute>(100.0),
|
||||
Flow::default(),
|
||||
do1,
|
||||
enc1,
|
||||
@@ -148,7 +152,6 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
0.10,
|
||||
0.0,
|
||||
0.015,
|
||||
Duration::from_millis(500),
|
||||
Temperature::default(),
|
||||
ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
ao2,
|
||||
@@ -156,6 +159,7 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
do6,
|
||||
t3,
|
||||
t4,
|
||||
AngularVelocity::new::<revolution_per_minute>(100.0),
|
||||
Flow::default(),
|
||||
do5,
|
||||
enc2,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
use super::DigitalInputTestMachine;
|
||||
use crate::{MachineAct, MachineMessage, MachineValues};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for DigitalInputTestMachine {
|
||||
fn act(&mut self, now: Instant) {
|
||||
if let Ok(msg) = self.api_receiver.try_recv() {
|
||||
self.act_machine_message(msg);
|
||||
}
|
||||
|
||||
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.emit_state();
|
||||
self.last_state_emit = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn act_machine_message(&mut self, msg: MachineMessage) {
|
||||
match msg {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_state();
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) => {
|
||||
// Does not connect to any Machine; do nothing
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {
|
||||
// Does not connect to any Machine; do nothing
|
||||
}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::to_value(self.get_state())
|
||||
.expect("Failed to serialize state"),
|
||||
live_values: serde_json::Value::Null,
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{MachineApi, digital_input_test_machine::DigitalInputTestMachine};
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct StateEvent {
|
||||
pub led_on: [bool; 4],
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("StateEvent", self.clone())
|
||||
}
|
||||
}
|
||||
pub enum DigitalInputTestMachineEvents {
|
||||
State(Event<StateEvent>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "action", content = "value")]
|
||||
pub enum Mutation {
|
||||
SetLed { index: usize, on: bool },
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DigitalInputTestMachineNamespace {
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<DigitalInputTestMachineEvents> for DigitalInputTestMachineNamespace {
|
||||
fn emit(&mut self, events: DigitalInputTestMachineEvents) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
if let Some(ns) = &mut self.namespace {
|
||||
ns.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheableEvents<DigitalInputTestMachineEvents> for DigitalInputTestMachineEvents {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
match self {
|
||||
DigitalInputTestMachineEvents::State(event) => event.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_first_and_last_event()
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineApi for DigitalInputTestMachine {
|
||||
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
|
||||
self.api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, _value: serde_json::Value) -> Result<(), anyhow::Error> {
|
||||
//Digital Input Test Machine does not Set Values
|
||||
// let mutation: Mutation = serde_json::from_value(value)?;
|
||||
// match mutation {
|
||||
// Mutation::SetLed { index, on } => self.set_led(index, on),
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
AsyncThreadMessage, DIGITAL_INPUT_TEST_MACHINE, Machine, MachineMessage, VENDOR_QITECH,
|
||||
digital_input_test_machine::api::{
|
||||
DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent,
|
||||
},
|
||||
machine_identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
};
|
||||
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod new;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DigitalInputTestMachine {
|
||||
pub api_receiver: Receiver<MachineMessage>,
|
||||
pub api_sender: Sender<MachineMessage>,
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
pub namespace: DigitalInputTestMachineNamespace,
|
||||
pub last_state_emit: Instant,
|
||||
pub led_on: [bool; 4],
|
||||
pub digital_input: [DigitalInput; 4],
|
||||
}
|
||||
|
||||
impl Machine for DigitalInputTestMachine {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl DigitalInputTestMachine {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: DIGITAL_INPUT_TEST_MACHINE,
|
||||
};
|
||||
|
||||
pub fn get_state(&self) -> StateEvent {
|
||||
StateEvent {
|
||||
led_on: self.led_on,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
//Set Led_On after DigitalInput
|
||||
let mut i = 0;
|
||||
|
||||
for di in &self.digital_input {
|
||||
let value = match di.get_value() {
|
||||
Ok(v) => v,
|
||||
Err(_) => false,
|
||||
};
|
||||
self.led_on[i] = value;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let event = StateEvent {
|
||||
led_on: self.led_on,
|
||||
}
|
||||
.build();
|
||||
self.namespace
|
||||
.emit(DigitalInputTestMachineEvents::State(event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{
|
||||
devices::{
|
||||
EthercatDevice, downcast_device,
|
||||
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
|
||||
wago_modules::wago_750_402::{Wago750_402, Wago750_402InputPort},
|
||||
},
|
||||
io::digital_input::DigitalInput,
|
||||
};
|
||||
use smol::{block_on, lock::RwLock};
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait,
|
||||
digital_input_test_machine::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace},
|
||||
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
impl MachineNewTrait for DigitalInputTestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
.iter()
|
||||
.map(|device_identification| device_identification.clone())
|
||||
.collect::<Vec<_>>();
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::EtherCATMachine/TestMachine::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
block_on(async {
|
||||
// Example usage of a Wago Coupler and a 750-402 in the first slot, where the Output Port 1,2,3,4 is used
|
||||
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
|
||||
hardware,
|
||||
params,
|
||||
0,
|
||||
[WAGO_750_354_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
|
||||
|
||||
let mut coupler = _wago_750_354.0.write().await;
|
||||
|
||||
for module in modules {
|
||||
coupler.set_module(module);
|
||||
}
|
||||
|
||||
coupler.init_slot_modules(_wago_750_354.1);
|
||||
let dev = coupler.slot_devices.get(0).unwrap().clone().unwrap();
|
||||
let wago750_402: Arc<RwLock<Wago750_402>> = downcast_device::<Wago750_402>(dev).await?;
|
||||
|
||||
let di1 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI1);
|
||||
let di2 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI2);
|
||||
let di3 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI3);
|
||||
let di4 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI4);
|
||||
|
||||
drop(coupler);
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut my_test = Self {
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: DigitalInputTestMachineNamespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_state_emit: Instant::now(),
|
||||
led_on: [false; 4],
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
digital_input: [di1, di2, di3, di4],
|
||||
};
|
||||
my_test.emit_state();
|
||||
Ok(my_test)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ pub mod analog_input_test_machine;
|
||||
pub mod aquapath1;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub mod buffer1;
|
||||
pub mod digital_input_test_machine;
|
||||
pub mod extruder1;
|
||||
pub mod extruder2;
|
||||
pub mod ip20_test_machine;
|
||||
@@ -30,6 +31,7 @@ pub mod mock;
|
||||
pub mod registry;
|
||||
pub mod serial;
|
||||
pub mod test_machine;
|
||||
pub mod wago_ai_test_machine;
|
||||
pub mod wago_power;
|
||||
pub mod winder2;
|
||||
|
||||
@@ -45,6 +47,8 @@ pub const MACHINE_EXTRUDER_V2: u16 = 0x0016;
|
||||
pub const TEST_MACHINE: u16 = 0x0033;
|
||||
pub const IP20_TEST_MACHINE: u16 = 0x0034;
|
||||
pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
|
||||
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
|
||||
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
|
||||
|
||||
use serde_json::Value;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
@@ -62,6 +62,7 @@ impl MachineIdentification {
|
||||
x if x == TEST_MACHINE => "test_machine".to_string(),
|
||||
x if x == IP20_TEST_MACHINE => "ip20_test_machine".to_string(),
|
||||
x if x == ANALOG_INPUT_TEST_MACHINE => "analog_input_test_machine".to_string(),
|
||||
x if x == WAGO_AI_TEST_MACHINE => "wago_ai_test_machine".to_string(),
|
||||
_ => unreachable!("Unknown machine id"),
|
||||
}
|
||||
}
|
||||
@@ -185,6 +186,7 @@ use crate::MACHINE_WAGO_POWER_V1;
|
||||
use crate::MACHINE_WINDER_V1;
|
||||
use crate::TEST_MACHINE;
|
||||
use crate::VENDOR_QITECH;
|
||||
use crate::WAGO_AI_TEST_MACHINE;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineIdentificationAddresses {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::analog_input_test_machine::AnalogInputTestMachine;
|
||||
use crate::digital_input_test_machine::DigitalInputTestMachine;
|
||||
use crate::ip20_test_machine::IP20TestMachine;
|
||||
use crate::wago_ai_test_machine::WagoAiTestMachine;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use crate::{
|
||||
extruder1::mock::ExtruderV2 as ExtruderV2Mock1, extruder2::mock::ExtruderV2 as ExtruderV2Mock2,
|
||||
@@ -128,6 +130,10 @@ lazy_static! {
|
||||
|
||||
mc.register::<AnalogInputTestMachine>(AnalogInputTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc
|
||||
};
|
||||
}
|
||||
@@ -78,7 +78,6 @@ impl MachineNewTrait for TestMachine {
|
||||
let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
|
||||
let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
|
||||
let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut my_test = Self {
|
||||
api_receiver: receiver,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::{MachineAct, MachineMessage, MachineValues, wago_ai_test_machine::WagoAiTestMachine};
|
||||
|
||||
impl MachineAct for WagoAiTestMachine {
|
||||
fn act_machine_message(&mut self, msg: MachineMessage) {
|
||||
match msg {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_measurement_rate();
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
crate::MachineMessage::ConnectToMachine(_machine_connection) => {}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::Value::Null,
|
||||
live_values: serde_json::Value::Null,
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn act(&mut self, now: std::time::Instant) {
|
||||
let recv = self.api_receiver.try_recv();
|
||||
if let Ok(msg) = recv {
|
||||
self.act_machine_message(msg);
|
||||
}
|
||||
if now.duration_since(self.last_measurement)
|
||||
> Duration::from_secs_f64(1.0 / self.measurement_rate_hz)
|
||||
{
|
||||
let mut values = [0.0; 4];
|
||||
let mut wiring_errors = [false; 4];
|
||||
|
||||
// Read all 4 analog inputs
|
||||
for (i, ai) in self.analog_inputs.iter().enumerate() {
|
||||
let measured_value = ai.get_physical();
|
||||
wiring_errors[i] = ai.get_wiring_error();
|
||||
|
||||
match measured_value {
|
||||
ethercat_hal::io::analog_input::physical::AnalogInputValue::Potential(
|
||||
_quantity,
|
||||
) => {
|
||||
// Don't do anything - this module is current input only
|
||||
}
|
||||
ethercat_hal::io::analog_input::physical::AnalogInputValue::Current(
|
||||
quantity,
|
||||
) => {
|
||||
values[i] = quantity.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now_milliseconds = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Now is expected to be after UNIX_EPOCH")
|
||||
.as_millis();
|
||||
|
||||
self.emit_analog_inputs(values, now_milliseconds);
|
||||
self.emit_wiring_errors(wiring_errors);
|
||||
self.last_measurement = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{MachineApi, wago_ai_test_machine::WagoAiTestMachine};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WagoAiTestMachineNamespace {
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub enum AnalogInputsEvent {
|
||||
MeasurementRateHz(f64),
|
||||
AnalogInputs(f64, f64, f64, f64, String),
|
||||
WiringErrors(bool, bool, bool, bool),
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<WagoAiTestMachineEvents> for WagoAiTestMachineNamespace {
|
||||
fn emit(&mut self, events: WagoAiTestMachineEvents) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
if let Some(ns) = &mut self.namespace {
|
||||
ns.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheableEvents<WagoAiTestMachineEvents> for WagoAiTestMachineEvents {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
match self {
|
||||
WagoAiTestMachineEvents::State(event) => event.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_first_and_last_event()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum WagoAiTestMachineEvents {
|
||||
State(Event<AnalogInputsEvent>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Mutation {
|
||||
measurement_rate_hz: i32,
|
||||
}
|
||||
|
||||
impl MachineApi for WagoAiTestMachine {
|
||||
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
|
||||
self.api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, value: serde_json::Value) -> Result<(), anyhow::Error> {
|
||||
let mutation: Mutation = serde_json::from_value(value)?;
|
||||
self.measurement_rate_hz = f64::from(mutation.measurement_rate_hz);
|
||||
self.emit_measurement_rate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Option<control_core::socketio::namespace::Namespace> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use control_core::socketio::{event::Event, namespace::NamespaceCacheingLogic};
|
||||
use ethercat_hal::io::analog_input::AnalogInput;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH, WAGO_AI_TEST_MACHINE,
|
||||
machine_identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
wago_ai_test_machine::api::{
|
||||
AnalogInputsEvent, WagoAiTestMachineEvents, WagoAiTestMachineNamespace,
|
||||
},
|
||||
};
|
||||
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod new;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WagoAiTestMachine {
|
||||
api_receiver: Receiver<MachineMessage>,
|
||||
api_sender: Sender<MachineMessage>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
namespace: WagoAiTestMachineNamespace,
|
||||
|
||||
last_measurement: Instant,
|
||||
measurement_rate_hz: f64,
|
||||
|
||||
analog_inputs: [AnalogInput; 4],
|
||||
}
|
||||
|
||||
impl Machine for WagoAiTestMachine {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WagoAiTestMachine {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: WAGO_AI_TEST_MACHINE,
|
||||
};
|
||||
|
||||
pub fn emit_analog_inputs(&mut self, values: [f64; 4], unix_timestamp_ms: u128) {
|
||||
let event = AnalogInputsEvent::AnalogInputs(
|
||||
values[0],
|
||||
values[1],
|
||||
values[2],
|
||||
values[3],
|
||||
unix_timestamp_ms.to_string(),
|
||||
);
|
||||
self.namespace
|
||||
.emit(WagoAiTestMachineEvents::State(Event::new(
|
||||
"AnalogInputs",
|
||||
event.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn emit_wiring_errors(&mut self, errors: [bool; 4]) {
|
||||
let event = AnalogInputsEvent::WiringErrors(errors[0], errors[1], errors[2], errors[3]);
|
||||
self.namespace
|
||||
.emit(WagoAiTestMachineEvents::State(Event::new(
|
||||
"WiringErrors",
|
||||
event.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn emit_measurement_rate(&mut self) {
|
||||
let event = AnalogInputsEvent::MeasurementRateHz(self.measurement_rate_hz);
|
||||
self.namespace
|
||||
.emit(WagoAiTestMachineEvents::State(Event::new(
|
||||
"MeasurementRateHz",
|
||||
event.clone(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{
|
||||
devices::{
|
||||
EthercatDevice, downcast_device,
|
||||
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
|
||||
wago_modules::wago_750_455::{Wago750_455, Wago750_455Port},
|
||||
},
|
||||
io::analog_input::AnalogInput,
|
||||
};
|
||||
use smol::{block_on, channel::unbounded, lock::RwLock};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
wago_ai_test_machine::{WagoAiTestMachine, api::WagoAiTestMachineNamespace},
|
||||
};
|
||||
|
||||
impl MachineNewTrait for WagoAiTestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
.iter()
|
||||
.map(|device_identification| device_identification.clone())
|
||||
.collect::<Vec<_>>();
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::WagoAiTestMachine::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
block_on(async {
|
||||
// Get the Wago 750-354 bus coupler
|
||||
let wago_750_354 = get_ethercat_device::<Wago750_354>(
|
||||
hardware,
|
||||
params,
|
||||
0,
|
||||
[WAGO_750_354_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Initialize modules on the bus coupler
|
||||
let modules = Wago750_354::initialize_modules(wago_750_354.1).await?;
|
||||
|
||||
let mut coupler = wago_750_354.0.write().await;
|
||||
|
||||
for module in modules {
|
||||
coupler.set_module(module);
|
||||
}
|
||||
|
||||
coupler.init_slot_modules(wago_750_354.1);
|
||||
|
||||
// Get the 750-455 analog input module from slot 0 (first module)
|
||||
let dev = coupler
|
||||
.slot_devices
|
||||
.get(0)
|
||||
.ok_or_else(|| anyhow::anyhow!("No device in slot 0"))?
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Slot 0 is empty"))?;
|
||||
|
||||
let wago750_455: Arc<RwLock<Wago750_455>> = downcast_device::<Wago750_455>(dev).await?;
|
||||
|
||||
// Create AnalogInput instances for all 4 channels
|
||||
let ai1 = AnalogInput::new(wago750_455.clone(), Wago750_455Port::AI1);
|
||||
let ai2 = AnalogInput::new(wago750_455.clone(), Wago750_455Port::AI2);
|
||||
let ai3 = AnalogInput::new(wago750_455.clone(), Wago750_455Port::AI3);
|
||||
let ai4 = AnalogInput::new(wago750_455.clone(), Wago750_455Port::AI4);
|
||||
|
||||
drop(coupler);
|
||||
|
||||
let (sender, receiver) = unbounded();
|
||||
let namespace = WagoAiTestMachineNamespace {
|
||||
namespace: params.namespace.clone(),
|
||||
};
|
||||
|
||||
let new_wago_ai_test_machine = Self {
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
namespace,
|
||||
|
||||
last_measurement: Instant::now(),
|
||||
measurement_rate_hz: 1.0,
|
||||
|
||||
analog_inputs: [ai1, ai2, ai3, ai4],
|
||||
};
|
||||
|
||||
Ok(new_wago_ai_test_machine)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,6 @@ in {
|
||||
options.services.qitech = {
|
||||
enable = mkEnableOption "QiTech Control";
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
"Whether to open ports in the firewall for the QiTech server";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "qitech-service";
|
||||
@@ -133,9 +126,16 @@ in {
|
||||
value = "-20";
|
||||
}
|
||||
];
|
||||
|
||||
# Open firewall if requested
|
||||
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 3001 ]; };
|
||||
# needed so dnsmasq can get managed by server
|
||||
security.polkit.extraConfig = ''
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||||
action.lookup("unit") == "dnsmasq.service" &&
|
||||
subject.user == "${cfg.user}") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
'';
|
||||
|
||||
# Desktop integration
|
||||
xdg.mime.enable = true;
|
||||
|
||||
@@ -219,7 +219,6 @@ in {
|
||||
# Enable the QiTech Control server
|
||||
services.qitech = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
user = "qitech-service";
|
||||
group = "qitech-service";
|
||||
package = pkgs.qitechPackages.server;
|
||||
@@ -323,10 +322,12 @@ in {
|
||||
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
resolveLocalQueries = false;
|
||||
|
||||
settings = {
|
||||
interface = "enp1s0"; # only this interface
|
||||
bind-interfaces = true;
|
||||
server = [ "1.1.1.1" "8.8.8.8" ];
|
||||
|
||||
# DHCP subnet + pool
|
||||
dhcp-range = "10.10.10.50,10.10.10.250,255.255.255.0,12h";
|
||||
@@ -339,11 +340,8 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
# Open ports in the firewall.
|
||||
# networking.firewall.allowedTCPPorts = [ ... ];
|
||||
networking.firewall.allowedUDPPorts = [ 53 67 69 ];
|
||||
# Or disable the firewall altogether.
|
||||
# networking.firewall.enable = false;
|
||||
networking.firewall.enable = true;
|
||||
networking.firewall.trustedInterfaces = [ "enp1s0" ];
|
||||
|
||||
# This value determines the NixOS release from which the default
|
||||
# settings for stateful data, like file locations and database versions
|
||||
|
||||
@@ -10,7 +10,7 @@ buildNpmPackage rec {
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
|
||||
|
||||
makeCacheWritable = true;
|
||||
npmDepsHash = "sha256-UHPIoyMTzr7jkvbpZsePaNJ6s76h3R/0pcnbCkOcdYo=";
|
||||
npmDepsHash = "sha256-twp+30+hPKoMUojuGyJI1R6dknWWx9i5waqFw2FlHPc=";
|
||||
npmFlags = [ "--no-audit" "--no-fund" ];
|
||||
|
||||
installPhase = ''
|
||||
|
||||
@@ -19,6 +19,7 @@ use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::{
|
||||
IP20_EC_DI8_DO8_PRODUCT_ID, IP20_EC_DI8_DO8_VENDOR_ID, IP20EcDi8Do8,
|
||||
};
|
||||
|
||||
use crate::utils::{start_dnsmasq, stop_dnsmasq};
|
||||
use ethercrab::std::ethercat_now;
|
||||
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
|
||||
use machines::machine_identification::{
|
||||
@@ -172,6 +173,14 @@ pub async fn setup_loop(
|
||||
) -> Result<EthercatSetup, anyhow::Error> {
|
||||
tracing::info!("Starting Ethercat PDU loop");
|
||||
|
||||
let res = stop_dnsmasq();
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(e) => tracing::error!("Failed to stop dnsmasq: {:?}", e),
|
||||
};
|
||||
// Small Timeout to ensure interfaces get released
|
||||
smol::Timer::after(Duration::from_millis(1500)).await;
|
||||
|
||||
// Setup ethercrab tx/rx task
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<MAX_FRAMES, MAX_PDU_DATA>::new()));
|
||||
let (tx, rx, pdu) = pdu_storage.try_split().expect("can only split once");
|
||||
@@ -481,6 +490,12 @@ pub async fn setup_loop(
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
|
||||
}
|
||||
|
||||
let res = start_dnsmasq();
|
||||
match res {
|
||||
Ok(o) => o,
|
||||
Err(e) => tracing::error!("Failed to start dnsmasq: {:?}", e),
|
||||
};
|
||||
|
||||
Ok(EthercatSetup {
|
||||
devices,
|
||||
group: group_op,
|
||||
|
||||
@@ -16,6 +16,7 @@ use machines::{
|
||||
};
|
||||
#[cfg(feature = "development-build")]
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use utils::start_dnsmasq;
|
||||
|
||||
use app_state::{HotThreadMessage, SharedState};
|
||||
use ethercat::ethercat_discovery_info::send_ethercat_discovering;
|
||||
@@ -57,6 +58,7 @@ pub mod panic;
|
||||
pub mod performance_metrics;
|
||||
pub mod rest;
|
||||
pub mod socketio;
|
||||
pub mod utils;
|
||||
|
||||
pub async fn send_empty_machines_event(shared_state: Arc<SharedState>) {
|
||||
shared_state.current_machines_meta.lock().await.clear();
|
||||
@@ -137,7 +139,6 @@ pub async fn start_interface_discovery(
|
||||
let interface = find_ethercat_interface().await;
|
||||
tracing::info!("Inferface found {}, setting up EtherCAT loop", interface);
|
||||
set_ethercat_iface(interface.clone());
|
||||
|
||||
let res = setup_loop(&interface, app_state.clone()).await;
|
||||
|
||||
match res {
|
||||
@@ -152,9 +153,10 @@ pub async fn start_interface_discovery(
|
||||
module_path!(),
|
||||
e
|
||||
);
|
||||
// if this doesnt work, unlucky
|
||||
let _ = start_dnsmasq();
|
||||
}
|
||||
}
|
||||
|
||||
send_ethercat_found(app_state.clone(), &interface).await;
|
||||
}
|
||||
|
||||
@@ -338,6 +340,7 @@ fn main() {
|
||||
let app_state = Arc::new(shared_state);
|
||||
let _loop_thread = start_loop_thread(receiver, CYCLE_TARGET_TIME);
|
||||
let _ = start_api_thread(app_state.clone());
|
||||
|
||||
spawn_runtime_metrics_sampler(RuntimeMetricsConfig {
|
||||
csv_path: "runtime_metrics.csv".to_string(),
|
||||
interval: Duration::from_secs(1),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::process::Command;
|
||||
|
||||
pub fn start_dnsmasq() -> std::io::Result<()> {
|
||||
// action can be "start", "stop", or "restart"
|
||||
let status = Command::new("systemctl")
|
||||
.arg("start")
|
||||
.arg("dnsmasq.service")
|
||||
.status()?;
|
||||
|
||||
if status.success() {
|
||||
tracing::info!("Successfully started dnsmasq");
|
||||
} else {
|
||||
tracing::info!("Failed to start dnsmasq");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_dnsmasq() -> std::io::Result<()> {
|
||||
// action can be "start", "stop", or "restart"
|
||||
let status = Command::new("systemctl")
|
||||
.arg("stop")
|
||||
.arg("dnsmasq.service")
|
||||
.status()?;
|
||||
|
||||
if status.success() {
|
||||
tracing::info!("Successfully stopped");
|
||||
} else {
|
||||
tracing::info!("Failed to stop dnsmasq");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||