Compare commits

..
Author SHA1 Message Date
kraemr 5e1b74a583 removed extraneous configuration 2026-01-21 18:41:09 +01:00
kraemr ee93bb150d fixed formatting 2026-01-21 18:33:30 +01:00
kraemr 208fe5734f fixed linter error 2026-01-21 18:30:18 +01:00
kraemr 2abb6b2ce0 fixed formatting 2026-01-21 18:26:40 +01:00
kraemr 6f986d3c45 fixed potential error where a failed setup_loop kills dns 2026-01-21 18:25:04 +01:00
Oshgnacknak 1112fbaae2 Merge pull request #990 from qitechgmbh/983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page
983 refactor improve aquapath UI layout and add fan speed configuration page
2026-01-21 15:55:32 +01:00
Oshgnacknak 1abe0bbbdb Fix firewall (#1059)
* Configure firewall to only trust lan interface
2026-01-21 15:53:30 +01:00
Oshgnacknak 3644d16cb5 Merge branch 'master' into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2026-01-21 15:44:59 +01:00
Christian cbd82ee7c3 Merge pull request #1056 from qitechgmbh/959-wago-analog-input-klemme-750-455
959 wago analog input klemme 750 455
2026-01-21 15:19:38 +01:00
Christian 06ea89d500 Refactor Wago AI Test Machine components: improve layout structure, optimize wiring error display, and clean up code formatting. 2026-01-20 13:22:30 +01:00
Christian 0868a90513 Update minimal example documentation for Wago 750-455: refine hardware requirements, enhance wiring instructions, and clarify testing steps. 2026-01-20 13:17:04 +01:00
Christian 39fb295b02 Remove debug print statements 2026-01-20 13:17:04 +01:00
Christian 85dbae7458 Beginning of the dev doc 455 2026-01-20 13:17:04 +01:00
Christian 6ce2174e25 Add wiring error handling to Wago AI Test Machine interface 2026-01-20 13:17:04 +01:00
Christian 3ccaa1cd26 Fix wiring error detection and adjust raw value range for Wago750_455 analog input 2026-01-20 13:17:04 +01:00
Christian 72fec6dc9e Implement Wago AI Test Machine with analog input support and control interface 2026-01-20 13:17:04 +01:00
Christian 547e4ac2bd Add Wago750_455 analog input device implementation 2026-01-20 13:15:04 +01:00
Robin Krämer 5fbdf7b9ab Merge pull request #1053 from qitechgmbh/fix-ethercat-devices-event
fixed broken schema for ethercat devices event
2026-01-20 13:09:02 +01:00
kraemr 85bd4b0d96 fixed formatting 2026-01-20 12:59:39 +01:00
kraemr dfce760521 fixed validation error 2026-01-20 12:58:15 +01:00
kraemr 30c97bf9aa fixed formatting 2026-01-19 16:00:00 +01:00
Oshgnacknak 2c8373adcf Fix rust enum zod check 2026-01-19 15:54:52 +01:00
kraemr d08972254c fixed broken schema for ethercat devices event 2026-01-19 15:12:43 +01:00
Christian a76d39e0cd Merge pull request #1016 from qitechgmbh/1015-fix-xlsx-vulnerabilities
Update xlsx dependency to version 0.20.3 for improved security
2026-01-19 13:23:34 +01:00
Christian a74d0dbb80 Set npmDepsHash for Electron package 2026-01-18 16:57:30 +01:00
Christian 17ab24739d Remove npmDepsHash for improved compatibility 2026-01-18 15:21:11 +01:00
Christian 09f0ea3454 Updated package-lock 2026-01-18 15:00:50 +01:00
Christian 0dd2494ea0 Update xlsx dependency to version 0.20.3 for improved security 2026-01-17 11:41:48 +01:00
Oshgnacknak 502c6d010e Merge remote-tracking branch 'origin/master' into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2026-01-16 13:43:42 +01:00
Robin Krämer 57c1cda229 Merge pull request #1040 from qitechgmbh/1001-rest-api
Rest API /api/v2
2026-01-15 17:06:34 +01:00
Oshgnacknak 8c2b20b148 Rest API: Docs 2026-01-15 16:57:09 +01:00
Oshgnacknak 12e7f245ce Rest API: Allow posting mutations to each machine 2026-01-15 16:57:09 +01:00
Oshgnacknak 671e36fa0f Rest API: Getting values from all machines 2026-01-15 16:57:09 +01:00
Oshgnacknak 1ffd7eade6 Rest API: Getting values from all mock machines 2026-01-15 16:57:09 +01:00
Oshgnacknak 703bbfc9e6 Rest API: Get mock winder v1 values 2026-01-15 16:57:09 +01:00
Oshgnacknak 057afb0b2e List machines in rest api 2026-01-15 16:57:09 +01:00
Robin Krämer 94bd29c734 Merge pull request #1048 from qitechgmbh/zod4
Upgrade to zod v4
2026-01-15 11:37:41 +01:00
Oshgnacknak d078b7b143 Fix nix hash 2026-01-14 22:03:07 +01:00
dependabot[bot] acbeb98f4f Upgrade to zod v4.
To make this work, I lot if typescript types had to be fixed.
In general, they became a little simpler, so the update is worth it.
Also removes zod-adapter and upgrades zod-validation-error to work with zod v4.

From depbot:

Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.3.4.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.25.76...v4.3.4)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.3.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 21:51:23 +01:00
Robin Krämer cab8bf062d Merge pull request #1046 from qitechgmbh/1038-implement-wago-750-652-driver
added implementation for wago 750-652 serial
2026-01-14 14:45:28 +01:00
kraemr 9e04a7fa33 added implementation for wago 750-652 serial 2026-01-14 14:12:01 +01:00
Robin Krämer c92d4847ca Merge pull request #1045 from qitechgmbh/impl-wago-530-driver
added Wago750_530 implementation
2026-01-14 11:57:57 +01:00
kraemr aceeaa08c2 fixed formatting 2026-01-14 11:09:36 +01:00
kraemr 35946d0c88 added Wago750_530 implementation 2026-01-14 11:03:30 +01:00
Oshgnacknak e864a91caa Merge remote-tracking branch 'origin/master' into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2026-01-13 16:58:54 +01:00
Oshgnacknak a5b9a0b1d5 Merge pull request #1039 from qitechgmbh/883-implement-modbus-tcp-for-wago-2789-9052
Fix modbus discovery
2026-01-11 23:19:10 +01:00
Oshgnacknak 412d625138 Fix modbus discovery 2026-01-09 18:12:33 +01:00
MLobsien 2d2415a801 fix: unused imports 2025-12-20 17:48:57 +01:00
MLobsien b4cce1dba4 fix: unued fields in controller 2025-12-20 17:47:45 +01:00
MLobsien f80cdb05d5 fix: cooling state display in UI 2025-12-20 17:43:50 +01:00
MLobsien f24f31259b fix: temperature calculation
- resolves #987
- however, total_energy calculation is incorrect but stable
2025-12-20 17:31:55 +01:00
MLobsien 6411f9ff80 feat: display heating energy consumption in UI 2025-12-20 15:50:22 +01:00
MLobsien 8b462d5a1d Fix: target_revolutions -> max_revolutions in controller 2025-12-20 10:58:04 +01:00
JalleliAchraf d8a0039db3 fixed double import 2025-12-19 20:52:01 +01:00
Achraf Jalleli dec8ea481c Merge pull request #994 from qitechgmbh/master
merge
2025-12-19 20:45:18 +01:00
EinsPommes 0c8b8388d0 auto-redirect to setup when machine disconnects 2025-12-19 20:42:11 +01:00
MLobsien d3a56710d6 fix: format 2025-12-19 18:49:24 +01:00
MLobsien f9f67d3309 fix: update fans periodically 2025-12-19 18:10:47 +01:00
MLobsien 66122ad957 fix: rename target_revolutions to max_revolutions 2025-12-19 17:54:35 +01:00
MLobsien 95feaf46bf Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-19 17:51:20 +01:00
MLobsien 88f14e0329 fix: emit max_revolutions
- resolves an issue in the UI where the setting wouldnt update
2025-12-19 17:50:00 +01:00
15i1i-s7v 5bf19af844 fix: cargo fmt issues 2025-12-19 17:38:11 +01:00
MLobsien 19e2a337cf Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-19 17:27:30 +01:00
MLobsien 0fd47cfcb1 fix: adjust fan speed in backend
- fine tuning is still necessary
2025-12-19 17:25:58 +01:00
15i1i-s7v e0548d9a65 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-19 17:14:54 +01:00
15i1i-s7v 259ed30dd5 style(aquapath): improve control layout spacing and remove unused import 2025-12-19 17:10:49 +01:00
MLobsien 2e028f0c0f fix: front and back cooling were switched
- use Badge instead of Status Badge
2025-12-19 16:58:15 +01:00
MLobsien 1328c6ce03 Feat: display heating/cooling state in control page 2025-12-19 16:13:48 +01:00
MLobsien adc06623cd Fix: website layout
- variable naming issues in the frontend namespace
- emit state in backend after request to update UI
2025-12-19 14:29:38 +01:00
MLobsien 172ad0ab28 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-19 13:39:40 +01:00
MLobsien bfabb5aa13 feat: UI for temperature tolerance control
- added ui components
- correct frontend api to match backend
2025-12-19 13:38:14 +01:00
15i1i-s7v 94d5a844cf feat: add heating and cooling tolerance functions for front and back
- Added `setFrontHeatingTolerance` and `setBackHeatingTolerance` functions to update heating tolerance values.
- Added `setFrontCoolingTolerance` and `setBackCoolingTolerance` functions to update cooling tolerance values.
- Updated `fanStateSchema` to use `max_revolutions` instead of `target_revolutions`.
- Introduced mutation hooks for handling tolerance updates for front and back heating/cooling.
2025-12-19 13:36:28 +01:00
MLobsien 75c528a430 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-19 11:53:14 +01:00
MLobsien 36045455db WIP: API for heating/cooling tolerance 2025-12-19 11:52:26 +01:00
15i1i-s7v bb12f96723 style: apply code formatting
- Reformatted `controller.rs` and `mod.rs` for better readability.
- Adjusted indentation and spacing for consistency.
2025-12-19 11:48:38 +01:00
15i1i-s7v 8151c99944 fix: simplify cooling logic and remove unnecessary .clone()
- Used `.abs()` directly on the temperature difference instead of duplicating the operation.
- Updated the condition for activating the cooling relay to depend on `cooling_allowed`.
- Refactored the fan speed control to pass the revolutions directly without converting to `AngularVelocity`.
2025-12-19 11:44:24 +01:00
MLobsien 6408010aff Add heating tolerance for controller 2025-12-19 11:23:21 +01:00
MLobsien c96c96217a Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-19 11:14:36 +01:00
MLobsien 93d179a81d WIP: adjust cooling automatically 2025-12-19 11:13:57 +01:00
15i1i-s7v 0f6f2240fc fix: electron fmt issues 2025-12-19 10:48:40 +01:00
15i1i-s7v 74505c6c22 fix: fmt issues resolved 2025-12-19 10:41:23 +01:00
15i1i-s7v 3dfb879101 fix: clean up imports and destructure variables
- Removed unused `use` import in `useAquapath.ts`.
- Added missing `React` import in `Aquapath1Settings.tsx`.
- Refactored variable destructuring in `useAquapath.ts` for readability.
2025-12-19 10:35:42 +01:00
MLobsien 44d4c650cc Fix: format 2025-12-18 17:45:07 +01:00
MLobsien 3121679205 feat: control aquapath fans via UI
- time series is displayed on control page
- device is properly addressed
2025-12-18 17:34:08 +01:00
MLobsien 9fe5e582b0 Refactor: use EditValue components instead of TouchSlider 2025-12-18 16:52:43 +01:00
MLobsien 3e6b838757 Refactor(aquapath1): rename zod fields to match backend
- 'front/back_fan_rpm' -> 'front/back_revolutions'
- integrate a graph view of the fan speed to the config page
2025-12-18 15:56:57 +01:00
MLobsien 3d7bcfecca Feat: Add aquapath fan control to controllers update function 2025-12-18 11:40:27 +01:00
MLobsien 63a472a1ba refactor(aquapath1): rename fan RPM to revolutions and add target tracking
- Rename all occurrences of 'rpm' to 'revolutions' to align with physical
  property naming conventions.
- Update FanState and Controller to distinguish between current and
  target revolutions.
- Refactor Controller fields to use the AngularVelocity type for improved
  type safety and unit handling.
- Update MachineApi mutations and event building to reflect the new
  data structure.
2025-12-18 10:56:42 +01:00
MLobsien df6b9e72c0 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page
fix indentation
2025-12-18 10:22:16 +01:00
MLobsien 6ad4b9c8bb WIP: add fan support 2025-12-18 10:19:52 +01:00
15i1i-s7v 4fcc8dfe12 fix: Change fan RPM type from f32 to f64 for precision
- Updated the `front_fan_rpm` and `back_fan_rpm` fields in `LiveValuesEvent` struct from f32 to f64.
- Adjusted the `FanState` struct to use f64 for the `rpm` field.
- Modified the `SetFrontFan` and `SetBackFan` mutation variants to expect f64 values instead of f32.
2025-12-18 10:08:10 +01:00
MLobsien 1da64c9cea WIP: AquaPath settings page can be rendered 2025-12-17 17:25:06 +01:00
MLobsien 9b6912be8d WIP: Added fan rpm to AquaPath API 2025-12-17 17:10:22 +01:00
15i1i-s7v 3beed9478c feat(aquapath): add fan RPM data and related schemas to live values (untested)
- Introduced fan RPM values for both front and back fans to AquaPathV1 live values.
- Updated TypeScript schemas to include fan state and RPM data.
- Added time series for tracking fan RPM.
- Updated event handling to include fan RPM data (untested).
2025-12-17 16:06:40 +01:00
15i1i-s7v 3715728fc6 feat: Add fan RPM data to AquaPathV1 live values
- Included front and back fan RPM values from the cooling controllers.
2025-12-17 15:23:59 +01:00
MLobsien 5beee78cad Add a second slider to AquaPath Config Page
- Both fans should be adjustable individually
- Therefor the second slider for the other fan
2025-12-17 15:05:01 +01:00
MLobsien 32fe774e62 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-17 15:01:22 +01:00
MLobsien dead77b49c Change intern representation of fan rpm to f32 2025-12-17 14:58:43 +01:00
MLobsien d7aa976eb1 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-17 14:53:47 +01:00
15i1i-s7v 6f9e89d616 fix(aquapath): correct SetFrontFan mutation name typo 2025-12-17 14:53:25 +01:00
MLobsien bb3f6c0d5a Feat: Implement fan rpm
- extend 'emit_state' to integrate FanState
- implement fan rpm setter function for AquaPathV1 struct
2025-12-17 14:51:46 +01:00
15i1i-s7v 620974ac59 feat(aquapath): implement fan RPM monitoring and control API
Add support for reading fan speeds and setting target RPM values
for both front and back fans in the Aquapath V1 machine.
2025-12-17 14:47:36 +01:00
15i1i-s7v 6c78ff95e4 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-17 13:30:19 +01:00
15i1i-s7v a814236bfa Add settings page and route for Aquapath1 2025-12-17 13:27:52 +01:00
MLobsien 2bee030410 set max temperature of aquapath to 80°C 2025-12-17 12:11:58 +01:00
Hamid-el 285ad59b08 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of https://github.com/qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-17 11:58:43 +01:00
Hamid-el f8b861cb39 WIP: add configpage for aquapath 2025-12-17 11:57:17 +01:00
MLobsien f6a3aa6458 Refactor: reorganize 'Control' Page of the AquaPath Machine
- data is now displayed per device instead of per measurement type
2025-12-17 11:40:51 +01:00
MLobsien dd5f3e8e73 Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-17 10:36:28 +01:00
EinsPommes f50e32af02 fix: prettier formatting 2025-12-17 10:35:20 +01:00
MLobsien 345e083edd auto-redirect to setup when machine disconnects 2025-12-17 10:35:20 +01:00
MLobsien 5660ddd05f Merge branch '983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page' of github.com:qitechgmbh/control into 983-refactor-improve-aquapath-ui-layout-and-add-fan-speed-configuration-page 2025-12-17 10:17:41 +01:00
rob c4360850af wago modules detected and wrapped as an EtherCatDevice 2025-12-17 10:13:15 +01:00
rob ce2123c88c added logic for initializing wago modules on a 750_354 2025-12-17 10:13:15 +01:00
Christian 520f33bf29 fix: clean up import formatting in registry.rs 2025-12-17 10:13:15 +01:00
Christian c620f1db7f fix mock machines in dev build 2025-12-17 10:13:15 +01:00
Christian d352e88d6f feat: add mock machine build step to CI workflow 2025-12-17 10:13:15 +01:00
EinsPommes cc009b31f3 fix: prettier formatting 2025-12-17 10:13:15 +01:00
EinsPommes b5c189ab49 transform roundness to percentage and update related components 2025-12-17 10:13:15 +01:00
EinsPommes 64f57277f2 fix: prettier formatting 2025-12-17 10:13:15 +01:00
EinsPommes af8ee60fe8 fix: clarify machine existence check in SidebarLayout 2025-12-17 10:13:15 +01:00
EinsPommes beb2e3cc2d remove console.log statement 2025-12-17 10:13:15 +01:00
EinsPommes 0e2bcfd7bc fix: handle machine disconnects properly
The app now detects when a machine disconnects and redirects you
to the setup page. Before, it would just stay on the machine page
even though the machine was gone.

Fixed both backend (USB detection) and frontend (auto-navigation).
Added error handling for parseInt and pathMatch array access.
2025-12-17 10:13:15 +01:00
EinsPommes 417ffb23c2 update: redirect to setup when machine disconnects 2025-12-17 10:13:15 +01:00
EinsPommes a57d4cdf36 auto-redirect to setup when machine disconnects 2025-12-17 10:13:15 +01:00
99 changed files with 3792 additions and 914 deletions

No files matched your search

@@ -47,7 +47,7 @@ async fn probe_modbus_tcp_addresses(interface: Interface) -> Vec<ModbusTcpProbe>
let prefix = min(24, m.count_ones()); // mask → prefix length
let size = 1u32 << (32 - prefix); // number of addresses
(200..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
(0..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
})
.map(|addr| smol::spawn(ping_modbus_device(addr)))
.join_all()
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -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.
![](../assets/wiring.png)
---
### 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)
+241
View File
@@ -0,0 +1,241 @@
# REST API `/api/v2`
The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at:
- **Base URL:** `http://10.10.10.1:3001`
> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below).
---
## 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. 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. 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`.
---
## List machines `GET /api/v2/machine`
Returns the set of machines currently known/connected to the panel.
Machines are identified by:
- `slug`: the machine type / model identifier (string)
- `serial`: the specific machine instance identifier (int)
Each machine also includes a `legacy_id` to support older **v1** workflows. If a machine reports an issue, the `error` field may be present and non-null (containing an error message).
### Example request
```bash
curl -X GET "http://10.10.10.1:3001/api/v2/machine"
```
### Example response
```json
{
"machines": [
{
"legacy_id": {
"machine_identification": {
"vendor": 1,
"machine": 7
},
"serial": 57922
},
"serial": 57922,
"vendor": "QiTech",
"slug": "mock",
"error": null
},
{
"legacy_id": {
"machine_identification": {
"vendor": 1,
"machine": 4
},
"serial": 57922
},
"serial": 57922,
"vendor": "QiTech",
"slug": "extruder_v1",
"error": null
},
{
"legacy_id": {
"machine_identification": {
"vendor": 1,
"machine": 2
},
"serial": 57922
},
"serial": 57922,
"vendor": "QiTech",
"slug": "winder_v1",
"error": null
},
{
"legacy_id": {
"machine_identification": {
"vendor": 1,
"machine": 10
},
"serial": 48879
},
"serial": 48879,
"vendor": "QiTech",
"slug": "wago_power_v1",
"error": null
}
]
}
```
---
## Get current values `GET /api/v2/machine/<slug>/<serial>`
Returns all currently known values for a single machine.
Values are categorized into two groups:
- **State**: requested/commanded values (these typically change only after a state-change request, or if another controller updates them)
- **Live Values**: measured/observed values coming from the machine and potentially changing quickly
This REST endpoint returns **only the current snapshot**, not a stream of live values.
To receive continuous updates (via WebSockets), subscribe to the machine namespace (see **WebSockets** below).
### Example request (mock machine)
```bash
curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922"
```
### Example response
```json
{
"machine": {
"legacy_id": {
"machine_identification": {
"vendor": 1,
"machine": 7
},
"serial": 57922
},
"serial": 57922,
"vendor": "QiTech",
"slug": "mock",
"error": null
},
"state": {
"frequency1": 100.0,
"frequency2": 200.0,
"frequency3": 500.0,
"is_default_state": false,
"mode_state": {
"mode": "Running"
}
},
"live_values": {
"amplitude1": -0.03438523433309566,
"amplitude2": -0.06872980145477608,
"amplitude3": -0.1711138370170743,
"amplitude_sum": -0.27422887280494607
}
}
```
---
## Change machine state `POST /api/v1/machine/<slug>/<serial>`
State changes are submitted as **mutations**. The mutation payload is defined per machine type in Rust. Conceptually, each item in the mutation list represents a setter-style operation that is applied by the real-time control loop.
The API does **not** return the newly-applied state in the POST response. The panel runs a real-time loop and generally won’t block waiting for the physical system to converge. Instead:
- Submit the mutation via `POST`
- Poll `GET /api/v2/machine/<slug>/<serial>` to observe the updated state and/or any reported errors
### Example request (mock machine)
```bash
curl -X POST \
-d \
-H "Content-Type: application/json" \
"http://10.10.10.1:3001/api/v1/machine/mock/57922"
```
### Example response
```json
null
```
---
## WebSockets
For continuous updates, subscribe to a machine-specific namespace derived from its `legacy_id`:
- Namespace: `/machine/<vendor>/<id>/<serial>`
The stream emits events for:
- state changes (`StateEvent`)
- live value updates (`LiveValuesEvent`)
Both event payloads use the same machine-specific schema as the `/api/v2` REST responses.
---
## List of all machines
Below is a template you can fill with links to the relevant Rust types (mutations + state/live structs).
For each machine, link to:
- **Mutations:** the request payload type used by `POST /api/v1/machine/<slug>/<serial>`
- **State / Live Values:** the response payload types returned by `GET /api/v2/machine/<slug>/<serial>`
### Machines
- **winder_v1**
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L87>
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L163>
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L143>
- **extruder_v1**
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L214>
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L89>
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L55>
- **laser_v1**
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L88>
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L32>
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L17>
- **mock**
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L84>
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L38>
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L24>
- **extruder_v2**
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L126>
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L93>
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L59>
+132 -137
View File
@@ -27,7 +27,6 @@
"@tanstack/react-router": "^1.130.17",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-devtools": "^1.105.0",
"@tanstack/zod-adapter": "^1.116.0",
"@vitejs/plugin-react": "^4.5.2",
"babel-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
"class-variance-authority": "^0.7.1",
@@ -52,9 +51,9 @@
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"uplot": "^1.6.32",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zod-validation-error": "^3.4.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.3.5",
"zod-validation-error": "^5.0.0",
"zustand": "^5.0.8"
},
"devDependencies": {
@@ -163,6 +162,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -659,6 +659,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -682,6 +683,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -3129,6 +3131,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
@@ -3221,6 +3283,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0",
@@ -3311,6 +3374,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0",
@@ -3408,23 +3472,6 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/zod-adapter": {
"version": "1.141.1",
"resolved": "https://registry.npmjs.org/@tanstack/zod-adapter/-/zod-adapter-1.141.1.tgz",
"integrity": "sha512-ZsGxo+/AFZnVdut8g7VkDZ6+xZ94+bHUyXv6FnB5tSmuPVFXuVwRCZnVida7EYYd3SOWpyy222e4Qkpob1xwOw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-router": ">=1.43.2",
"zod": "^3.23.8"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@@ -3548,8 +3595,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3715,6 +3761,7 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3731,6 +3778,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3741,6 +3789,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3828,6 +3877,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -4186,6 +4236,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4216,15 +4267,6 @@
"node": ">=0.4.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -4258,7 +4300,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4573,6 +4614,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4726,19 +4768,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -4857,15 +4886,6 @@
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4930,18 +4950,6 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -4989,7 +4997,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -5262,8 +5271,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5809,6 +5817,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5869,6 +5878,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -5964,6 +5974,30 @@
"eslint": ">=7"
}
},
"node_modules/eslint-plugin-react-compiler/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/eslint-plugin-react-compiler/node_modules/zod-validation-error": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz",
"integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.24.4"
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -6248,15 +6282,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6914,6 +6939,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -6947,6 +6973,7 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -7549,6 +7576,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -8082,7 +8110,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9502,6 +9529,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9618,7 +9646,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9634,7 +9661,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9724,6 +9750,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9733,6 +9760,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9745,6 +9773,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9787,8 +9816,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -10302,6 +10330,7 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz",
"integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -10556,7 +10585,6 @@
"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",
@@ -10578,7 +10606,6 @@
"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"
},
@@ -10623,18 +10650,6 @@
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -10915,7 +10930,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -11004,6 +11020,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11267,6 +11284,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11603,6 +11621,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -11715,7 +11734,8 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
@@ -11753,6 +11773,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12060,24 +12081,6 @@
"node": ">=8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -12118,19 +12121,10 @@
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"version": "0.20.3",
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
@@ -12210,24 +12204,25 @@
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"version": "4.3.5",
"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"
}
},
"node_modules/zod-validation-error": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz",
"integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-5.0.0.tgz",
"integrity": "sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.24.4"
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/zustand": {
+3 -4
View File
@@ -75,7 +75,6 @@
"@tanstack/react-router": "^1.130.17",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-devtools": "^1.105.0",
"@tanstack/zod-adapter": "^1.116.0",
"@vitejs/plugin-react": "^4.5.2",
"babel-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
"class-variance-authority": "^0.7.1",
@@ -100,9 +99,9 @@
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"uplot": "^1.6.32",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zod-validation-error": "^3.4.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.3.5",
"zod-validation-error": "^5.0.0",
"zustand": "^5.0.8"
}
}
+31 -27
View File
@@ -15,25 +15,31 @@ import {
machineIdentificationUnique,
} from "@/machines/types";
import { useRef } from "react";
import { rustEnumSchema } from "@/lib/types";
import { rustEnum } from "@/lib/types";
export type EthercatDevices = z.infer<typeof ethercatDevicesSchema>;
export const ethercatDevicesSchema = z.object({
devices: z.array(
z.object({
configured_address: z.number().int(),
name: z.string(),
vendor_id: z.number().int(),
product_id: z.number().int(),
revision: z.number().int(),
device_identification: deviceIdentification,
}),
),
});
// Update the EthercatDevicesEventData schema
export const ethercatDevicesEventDataSchema = rustEnumSchema({
Initializing: z.boolean(),
Done: z.object({
devices: z.array(
z.object({
configured_address: z.number().int(),
name: z.string(),
vendor_id: z.number().int(),
product_id: z.number().int(),
revision: z.number().int(),
device_identification: deviceIdentification,
}),
),
}),
Error: z.string(),
});
export const ethercatDevicesEventDataSchema = z
.object({
Initializing: z.boolean(),
Done: ethercatDevicesSchema,
Error: z.string(),
})
.check(rustEnum);
export type EthercatDevicesEventData = z.infer<
typeof ethercatDevicesEventDataSchema
@@ -62,10 +68,12 @@ export const machinesEventSchema = eventSchema(machinesEventDataSchema);
export type MachinesEvent = z.infer<typeof machinesEventSchema>;
// Keep the EthercatInterfaceDiscovery event
export const ethercatInterfaceDiscoveryEventDataSchema = rustEnumSchema({
Discovering: z.boolean(),
Done: z.string(),
});
export const ethercatInterfaceDiscoveryEventDataSchema = z
.object({
Discovering: z.boolean(),
Done: z.string(),
})
.check(rustEnum);
export type EthercatInterfaceDiscoveryEventData = z.infer<
typeof ethercatInterfaceDiscoveryEventDataSchema
@@ -110,23 +118,19 @@ export function mainMessageHandler(
try {
// Apply appropriate caching strategy based on event type
if (eventName === "EthercatDevicesEvent") {
const validatedEvent = ethercatDevicesEventSchema.parse(event);
console.log("EthercatDevicesEvent", validatedEvent);
const validatedEvent = event;
store.setState((state) => ({
...state,
ethercatDevices: validatedEvent,
}));
} else if (eventName === "MachinesEvent") {
const validatedEvent = machinesEventSchema.parse(event);
console.log("MachinesEvent", validatedEvent);
store.setState((state) => ({
...state,
machines: validatedEvent,
}));
} else if (eventName === "EthercatInterfaceDiscoveryEvent") {
const validatedEvent =
ethercatInterfaceDiscoveryEventSchema.parse(event);
console.log("EthercatInterfaceDiscoveryEvent", validatedEvent);
const validatedEvent = event;
store.setState((state) => ({
...state,
ethercatInterfaceDiscovery: validatedEvent,
+1
View File
@@ -163,6 +163,7 @@ export const handleEventValidationError = (
error: z.ZodError,
eventName: string,
) => {
console.log(error);
toastZodError(error, `Event Validation Error for ${eventName}`);
throw new Error(`Event validation failed for ${eventName}`);
};
@@ -1,4 +1,3 @@
import { PresetData, PresetSchema } from "@/lib/preset/preset";
import {
Dialog,
DialogContent,
@@ -14,13 +13,13 @@ import { Separator } from "../ui/separator";
import { PresetPreviewEntries, PresetPreviewTable } from "./PresetPreviewTable";
import { Input } from "../ui/input";
export type NewPresetDialogProps<T extends PresetSchema> = {
currentState?: PresetData<T>;
export type NewPresetDialogProps<T> = {
currentState?: T;
previewEntries: PresetPreviewEntries<T>;
onSave: (name: string) => void;
};
export function NewPresetDialog<T extends PresetSchema>({
export function NewPresetDialog<T>({
currentState,
onSave,
previewEntries,
@@ -60,7 +60,7 @@ export function PresetCardMenu({
);
}
export type PresetCardProps<T extends PresetSchema> = {
export type PresetCardProps<T> = {
preset: Preset<T>;
onApply: (preset: Preset<T>) => void;
onOverwrite: (preset: Preset<T>) => void;
@@ -72,7 +72,7 @@ export type PresetCardProps<T extends PresetSchema> = {
isActive?: boolean;
};
export function PresetCard<T extends PresetSchema>({
export function PresetCard<T>({
preset,
onApply,
onOverwrite,
@@ -1,28 +1,27 @@
import React, { Fragment } from "react";
import { PresetData, PresetSchema } from "@/lib/preset/preset";
import { renderUnitSymbol, renderUnitSyntax, Unit } from "@/control/units";
export const previewSeparator = undefined;
type PreviewSeparator = typeof previewSeparator;
type PresetPreviewEntry<T extends PresetSchema> = {
type PresetPreviewEntry<T> = {
name: string;
unit?: Unit;
renderValue: (data: PresetData<T>) => string | undefined;
renderValue: (data: T) => string | undefined;
};
export type PresetPreviewEntries<T extends PresetSchema> = (
export type PresetPreviewEntries<T> = (
| PresetPreviewEntry<T>
| PreviewSeparator
)[];
export type PresetPreviewTableProps<T extends PresetSchema> = {
data: PresetData<T>;
export type PresetPreviewTableProps<T> = {
data?: T;
entries: PresetPreviewEntries<T>;
};
export function PresetPreviewTable<T extends PresetSchema>({
export function PresetPreviewTable<T>({
data,
entries,
}: PresetPreviewTableProps<T>) {
@@ -39,7 +38,7 @@ export function PresetPreviewTable<T extends PresetSchema>({
);
}
const value = entry.renderValue(data);
const value = data !== undefined ? entry.renderValue(data) : undefined;
return (
<Fragment key={entry.name}>
<div>{entry.name}</div>
@@ -1,4 +1,4 @@
import { Preset, PresetSchema } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
import {
Dialog,
DialogContent,
@@ -13,14 +13,14 @@ import { Icon } from "../Icon";
import { Separator } from "../ui/separator";
import { PresetPreviewEntries, PresetPreviewTable } from "./PresetPreviewTable";
export type PresetShowDialogProps<T extends PresetSchema> = {
export type PresetShowDialogProps<T> = {
preset: Preset<T>;
previewEntries: PresetPreviewEntries<T>;
onApply: (preset: Preset<T>) => void;
hideDate?: boolean;
};
export function PresetShowDialog<T extends PresetSchema>({
export function PresetShowDialog<T>({
preset,
onApply,
previewEntries,
@@ -2,19 +2,19 @@ import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { Page } from "@/components/Page";
import { usePresets, UsePresetsParams } from "@/lib/preset/usePresets";
import { Preset, PresetSchema } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
import { PresetCard } from "./PresetCard";
import { PresetPreviewEntries } from "./PresetPreviewTable";
import { NewPresetDialog } from "./NewPresetDialog";
import { downloadJson } from "@/lib/download";
import { JsonFileInput } from "../FileInput";
type PresetsPageProps<T extends PresetSchema> = UsePresetsParams<T> & {
type PresetsPageProps<T> = UsePresetsParams<T> & {
applyPreset: (preset: Preset<T>) => void;
previewEntries: PresetPreviewEntries<T>;
};
export function PresetsPage<T extends PresetSchema>({
export function PresetsPage<T>({
applyPreset,
machine_identification,
currentState,
+2 -13
View File
@@ -36,7 +36,6 @@ type Props = {
maxSlider?: number; // Override the slider max value
maxLabel?: string;
step?: number;
valueSchema?: z.ZodType<number>;
inverted?: boolean;
confirmation?: string;
renderValue: (value: number) => string;
@@ -89,7 +88,6 @@ export function EditValue({
description,
defaultValue,
step = 1,
valueSchema: schema,
inverted,
min,
max,
@@ -104,7 +102,7 @@ export function EditValue({
// Form setup
const formSchema = z.object({
value: schema ?? z.number(),
value: z.number(),
});
type FormSchema = z.infer<typeof formSchema>;
@@ -191,7 +189,6 @@ export function EditValue({
const numericValue = parseFloat(normalizedInput);
const hasValidationError =
isNaN(numericValue) ||
(schema && !schema.safeParse(numericValue).success) ||
(max !== undefined && numericValue > max) ||
(min !== undefined && numericValue < min);
@@ -222,15 +219,7 @@ export function EditValue({
setValueStringError(false);
preventFormSyncRef.current = false;
}
}, [
valueString,
valueStringDirty,
schema,
max,
min,
form,
roundToStepDecimals,
]);
}, [valueString, valueStringDirty, max, min, form, roundToStepDecimals]);
// Reset input state to clean form value
const resetInput = React.useCallback(() => {
+4 -1
View File
@@ -1,4 +1,7 @@
export function deepEquals<T>(a: T, b: T): boolean {
export function deepEquals(
a: object | undefined,
b: object | undefined,
): boolean {
if (a === b) {
return true;
}
+4 -8
View File
@@ -1,20 +1,16 @@
import { machineIdentification } from "@/machines/types";
import { z } from "zod";
export type PresetSchema = z.ZodTypeAny;
export type PresetSchema<T> = z.ZodType<T>;
export const presetSchema = <S extends PresetSchema>(dataSchema: S) =>
export const presetSchema = <T>(dataSchema: PresetSchema<T>) =>
z.object({
id: z.number().int().nonnegative().optional(),
name: z.string().nonempty(),
lastModified: z.coerce.date(),
machineIdentification,
schemaVersion: z.number().int().positive(),
data: dataSchema,
data: dataSchema.optional(),
});
export type Preset<S extends PresetSchema> = z.infer<
ReturnType<typeof presetSchema<S>>
>;
export type PresetData<S extends PresetSchema> = z.infer<S>;
export type Preset<T> = z.infer<ReturnType<typeof presetSchema<T>>>;
+7 -7
View File
@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist, PersistStorage, StorageValue } from "zustand/middleware";
import { Preset, PresetSchema, presetSchema } from "./preset";
import { Preset, presetSchema } from "./preset";
import { MachineIdentification } from "@/machines/types";
import { z } from "zod";
import { toastError } from "@/components/Toast";
@@ -33,9 +33,9 @@ type PresetStoreData = {
};
export type PresetStore = PresetStoreData & {
insert: <T extends PresetSchema>(preset: Preset<T>) => PersistedPreset;
update: <T extends PresetSchema>(preset: Preset<T>) => void;
remove: <T extends PresetSchema>(preset: Preset<T>) => void;
insert: <T>(preset: Preset<T>) => PersistedPreset;
update: <T>(preset: Preset<T>) => void;
remove: <T>(preset: Preset<T>) => void;
getById: (id: number) => PersistedPreset | undefined;
setLatestPresetId: (
@@ -100,7 +100,7 @@ export const usePresetStore = create<PresetStore>()(
presets: [],
latestPresetIds: new Map(),
insert: <T extends PresetSchema>(preset: Preset<T>) => {
insert: <T>(preset: Preset<T>) => {
const state = get();
const { presets } = state;
const ids = presets.map((p: PersistedPreset) => p.id);
@@ -115,7 +115,7 @@ export const usePresetStore = create<PresetStore>()(
return persistedPreset;
},
update: <T extends PresetSchema>(preset: Preset<T>) => {
update: <T>(preset: Preset<T>) => {
const state = get();
const presets: PersistedPreset[] = state.presets.map(
(p: PersistedPreset) =>
@@ -124,7 +124,7 @@ export const usePresetStore = create<PresetStore>()(
set({ ...state, presets });
},
remove: <T extends PresetSchema>(preset: Preset<T>) => {
remove: <T>(preset: Preset<T>) => {
const state = get();
const presets = state.presets.filter(
(p: Preset<any>) => p.id !== preset.id,
+22 -11
View File
@@ -3,13 +3,13 @@ import {
machineIdentificationEquals,
} from "@/machines/types";
import { usePresetStore } from "./presetStore";
import { Preset, PresetData, presetSchema, PresetSchema } from "./preset";
import { Preset, presetSchema, PresetSchema } from "./preset";
import { deepEquals } from "@/lib/objects";
import { useEffect } from "react";
import { toastError } from "@/components/Toast";
import { z } from "zod";
export type Presets<T extends PresetSchema> = {
export type Presets<T> = {
get: () => Preset<T>[];
createFromCurrentState: (name: string) => Preset<T>;
updateFromCurrentState: (preset: Preset<T>) => Preset<T>;
@@ -20,15 +20,15 @@ export type Presets<T extends PresetSchema> = {
import: (json: any) => void;
};
export type UsePresetsParams<T extends PresetSchema> = {
export type UsePresetsParams<T> = {
machine_identification: MachineIdentification;
schemas: Map<number, T>;
schemas: Map<number, PresetSchema<T>>;
schemaVersion: number;
currentState?: PresetData<T>;
defaultState?: PresetData<T>;
currentState?: T;
defaultState?: T;
};
export function usePresets<T extends PresetSchema>({
export function usePresets<T>({
machine_identification,
schemas,
schemaVersion,
@@ -49,7 +49,10 @@ export function usePresets<T extends PresetSchema>({
}
try {
const parsed = presetSchema(schema).parse(preset);
const parsed: Preset<T> = {
...preset,
data: schema.parse(preset.data),
};
if (parsed.schemaVersion < schemaVersion) {
parsed.schemaVersion = schemaVersion;
@@ -93,7 +96,11 @@ export function usePresets<T extends PresetSchema>({
return { ...preset, data: currentState };
};
const updateFromCurrentState = (preset: Preset<T>) => {
const updateFromCurrentState = (preset: Preset<T>): Preset<T> => {
if (!currentState) {
return preset;
}
const data = currentState;
const newPreset = {
@@ -153,12 +160,16 @@ export function usePresets<T extends PresetSchema>({
};
const isActive = (preset: Preset<T>) => {
return deepEquals(currentState, preset.data);
if (!currentState) {
return false;
}
return deepEquals(currentState, preset.data ?? {});
};
const importPreset = (json: any) => {
try {
const anyPreset = presetSchema(z.any()).parse(json);
const anyPreset = presetSchema(z.object()).parse(json);
if (
!machineIdentificationEquals(
+13 -34
View File
@@ -1,38 +1,17 @@
import { z } from "zod";
/**
* Creates a Zod schema for objects where exactly one property must be defined.
* This is useful for handling Rust enum JSON representations where only one variant exists at a time.
*
* @param schemas A record mapping property names to their Zod schemas
* @returns A Zod schema that validates that exactly one property is defined
*/
export function rustEnumSchema<T extends Record<string, z.ZodType>>(
schemas: T,
): z.ZodType<{ [K in keyof T]?: z.infer<T[K]> }> {
// Create an object schema where all properties are optional
const schemaEntries = Object.entries(schemas).map(([key, schema]) => [
key,
schema.optional(),
]);
const objectSchema = z.object(
Object.fromEntries(schemaEntries) as {
[K in keyof T]: z.ZodOptional<T[K]>;
},
export const rustEnum: z.core.CheckFn<object> = (ctx) => {
const definedKeys = Object.keys(ctx.value).filter(
(key) => ctx.value[key] !== undefined,
);
// Add refinement to ensure exactly one property is defined
return objectSchema.refine(
(data) => {
const definedKeys = Object.keys(data).filter(
(key) => data[key] !== undefined,
);
return definedKeys.length === 1;
},
{
message: "Exactly one property must be defined",
path: ["_exclusive"], // Add a path to help with error identification
},
);
}
if (definedKeys.length === 1) {
return;
}
ctx.issues.push({
code: "custom",
message: "Exactly one property must be defined",
input: definedKeys,
});
};
-19
View File
@@ -1,19 +0,0 @@
import { z } from "zod";
// super refine function to validate if a string is a valid u16 number
export function validateU16<T>(): (value: T, ctx: z.RefinementCtx) => boolean {
const refine = (value: T, ctx: z.RefinementCtx) => {
console.log("value", value);
if (typeof value === "string") {
const number = parseInt(value, 10);
if (number < 0 || number > 65535) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Wert muss u16 integer sein",
});
}
}
return true;
};
return refine;
}
@@ -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,
};
}
@@ -7,7 +7,7 @@ import {
PresetPreviewEntries,
previewSeparator,
} from "@/components/preset/PresetPreviewTable";
import { Preset, PresetData } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
const extruder1PresetDataSchema = z
.object({
@@ -31,7 +31,7 @@ const extruder1PresetDataSchema = z
})
.partial();
type Extruder2 = typeof extruder1PresetDataSchema;
type Extruder2 = z.infer<typeof extruder1PresetDataSchema>;
const schemas = new Map([[1, extruder1PresetDataSchema]]);
@@ -39,76 +39,69 @@ const previewEntries: PresetPreviewEntries<Extruder2> = [
{
name: "Target Front Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder2>) =>
renderValue: (data: Extruder2) =>
data.targetFrontHeatingTemperature?.toFixed(1),
},
{
name: "Target Middle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder2>) =>
renderValue: (data: Extruder2) =>
data.targetMiddleHeatingTemperature?.toFixed(1),
},
{
name: "Target Back Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder2>) =>
renderValue: (data: Extruder2) =>
data.targetBackHeatingTemperature?.toFixed(1),
},
{
name: "Target Nozzle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder2>) =>
renderValue: (data: Extruder2) =>
data.targetNozzleHeatingTemperature?.toFixed(1),
},
previewSeparator,
{
name: "Inverter Regulation",
renderValue: (data: PresetData<Extruder2>) => data.inverterRegulation,
renderValue: (data: Extruder2) => data.inverterRegulation,
},
{
name: "Target Inverter RPM",
unit: "rpm",
renderValue: (data: PresetData<Extruder2>) =>
data.targetInverterRpm?.toFixed(2),
renderValue: (data: Extruder2) => data.targetInverterRpm?.toFixed(2),
},
{
name: "Target Inverter Pressure",
unit: "bar",
renderValue: (data: PresetData<Extruder2>) =>
data.targetInverterPressure?.toFixed(1),
renderValue: (data: Extruder2) => data.targetInverterPressure?.toFixed(1),
},
{
name: "Inverter Direction",
renderValue: (data: PresetData<Extruder2>) =>
data.inverterRotationDirection,
renderValue: (data: Extruder2) => data.inverterRotationDirection,
},
previewSeparator,
{
name: "Pressure Limit",
unit: "bar",
renderValue: (data: PresetData<Extruder2>) =>
data.pressureLimit?.toFixed(1),
renderValue: (data: Extruder2) => data.pressureLimit?.toFixed(1),
},
{
name: "Enable Pressure Limit",
renderValue: (data: PresetData<Extruder2>) =>
renderValue: (data: Extruder2) =>
data.pressureLimitEnabled ? "on" : "off",
},
previewSeparator,
{
name: "PID Pressue Kp",
renderValue: (data: PresetData<Extruder2>) =>
data.pidPressureKp?.toFixed(4),
renderValue: (data: Extruder2) => data.pidPressureKp?.toFixed(4),
},
{
name: "PID Pressue Ki",
renderValue: (data: PresetData<Extruder2>) =>
data.pidPressureKi?.toFixed(4),
renderValue: (data: Extruder2) => data.pidPressureKi?.toFixed(4),
},
{
name: "PID Pressue Kd",
renderValue: (data: PresetData<Extruder2>) =>
data.pidPressureKd?.toFixed(4),
renderValue: (data: Extruder2) => data.pidPressureKd?.toFixed(4),
},
];
@@ -135,7 +128,7 @@ export function Extruder2PresetsPage() {
setPressurePidKp,
} = useExtruder2();
const toPresetData = (s?: typeof state): PresetData<Extruder2> => ({
const toPresetData = (s?: typeof state): Extruder2 => ({
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
@@ -161,32 +154,32 @@ export function Extruder2PresetsPage() {
const applyPreset = (preset: Preset<Extruder2>) => {
setFrontHeatingTemperature(
preset.data.targetFrontHeatingTemperature ?? 150.0,
preset?.data?.targetFrontHeatingTemperature ?? 150.0,
);
setMiddleHeatingTemperature(
preset.data.targetMiddleHeatingTemperature ?? 150.0,
preset?.data?.targetMiddleHeatingTemperature ?? 150.0,
);
setBackHeatingTemperature(
preset.data.targetBackHeatingTemperature ?? 150.0,
preset?.data?.targetBackHeatingTemperature ?? 150.0,
);
setNozzleHeatingTemperature(
preset.data.targetNozzleHeatingTemperature ?? 150.0,
preset?.data?.targetNozzleHeatingTemperature ?? 150.0,
);
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
setInverterRegulation(preset.data.inverterRegulation === "RPM");
setInverterTargetRpm(preset?.data?.targetInverterRpm ?? 0);
setInverterTargetPressure(preset?.data?.targetInverterPressure ?? 0);
setInverterRegulation(preset?.data?.inverterRegulation === "RPM");
setInverterRotationDirection(
preset.data.inverterRotationDirection === "Forward",
preset?.data?.inverterRotationDirection === "Forward",
);
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
setExtruderPressureLimit(preset?.data?.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset?.data?.pressureLimitEnabled ?? true);
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
setPressurePidKp(preset?.data?.pidPressureKp ?? 0.16);
setPressurePidKi(preset?.data?.pidPressureKi ?? 0.0);
setPressurePidKd(preset?.data?.pidPressureKd ?? 0.008);
};
return (
@@ -7,7 +7,7 @@ import {
PresetPreviewEntries,
previewSeparator,
} from "@/components/preset/PresetPreviewTable";
import { Preset, PresetData } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
const extruder3PresetDataSchema = z
.object({
@@ -31,7 +31,7 @@ const extruder3PresetDataSchema = z
})
.partial();
type Extruder3 = typeof extruder3PresetDataSchema;
type Extruder3 = z.infer<typeof extruder3PresetDataSchema>;
const schemas = new Map([[1, extruder3PresetDataSchema]]);
@@ -39,76 +39,69 @@ const previewEntries: PresetPreviewEntries<Extruder3> = [
{
name: "Target Front Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
renderValue: (data: Extruder3) =>
data.targetFrontHeatingTemperature?.toFixed(1),
},
{
name: "Target Middle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
renderValue: (data: Extruder3) =>
data.targetMiddleHeatingTemperature?.toFixed(1),
},
{
name: "Target Back Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
renderValue: (data: Extruder3) =>
data.targetBackHeatingTemperature?.toFixed(1),
},
{
name: "Target Nozzle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
renderValue: (data: Extruder3) =>
data.targetNozzleHeatingTemperature?.toFixed(1),
},
previewSeparator,
{
name: "Inverter Regulation",
renderValue: (data: PresetData<Extruder3>) => data.inverterRegulation,
renderValue: (data: Extruder3) => data.inverterRegulation,
},
{
name: "Target Inverter RPM",
unit: "rpm",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterRpm?.toFixed(2),
renderValue: (data: Extruder3) => data.targetInverterRpm?.toFixed(2),
},
{
name: "Target Inverter Pressure",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterPressure?.toFixed(1),
renderValue: (data: Extruder3) => data.targetInverterPressure?.toFixed(1),
},
{
name: "Inverter Direction",
renderValue: (data: PresetData<Extruder3>) =>
data.inverterRotationDirection,
renderValue: (data: Extruder3) => data.inverterRotationDirection,
},
previewSeparator,
{
name: "Pressure Limit",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimit?.toFixed(1),
renderValue: (data: Extruder3) => data.pressureLimit?.toFixed(1),
},
{
name: "Enable Pressure Limit",
renderValue: (data: PresetData<Extruder3>) =>
renderValue: (data: Extruder3) =>
data.pressureLimitEnabled ? "on" : "off",
},
previewSeparator,
{
name: "PID Pressue Kp",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKp?.toFixed(4),
renderValue: (data: Extruder3) => data.pidPressureKp?.toFixed(4),
},
{
name: "PID Pressue Ki",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKi?.toFixed(4),
renderValue: (data: Extruder3) => data.pidPressureKi?.toFixed(4),
},
{
name: "PID Pressue Kd",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKd?.toFixed(4),
renderValue: (data: Extruder3) => data.pidPressureKd?.toFixed(4),
},
];
@@ -135,7 +128,7 @@ export function Extruder3PresetsPage() {
setPressurePidKp,
} = useExtruder3();
const toPresetData = (s?: typeof state): PresetData<Extruder3> => ({
const toPresetData = (s?: typeof state): Extruder3 => ({
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
@@ -161,32 +154,32 @@ export function Extruder3PresetsPage() {
const applyPreset = (preset: Preset<Extruder3>) => {
setFrontHeatingTemperature(
preset.data.targetFrontHeatingTemperature ?? 150.0,
preset?.data?.targetFrontHeatingTemperature ?? 150.0,
);
setMiddleHeatingTemperature(
preset.data.targetMiddleHeatingTemperature ?? 150.0,
preset?.data?.targetMiddleHeatingTemperature ?? 150.0,
);
setBackHeatingTemperature(
preset.data.targetBackHeatingTemperature ?? 150.0,
preset?.data?.targetBackHeatingTemperature ?? 150.0,
);
setNozzleHeatingTemperature(
preset.data.targetNozzleHeatingTemperature ?? 150.0,
preset?.data?.targetNozzleHeatingTemperature ?? 150.0,
);
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
setInverterRegulation(preset.data.inverterRegulation === "RPM");
setInverterTargetRpm(preset?.data?.targetInverterRpm ?? 0);
setInverterTargetPressure(preset?.data?.targetInverterPressure ?? 0);
setInverterRegulation(preset?.data?.inverterRegulation === "RPM");
setInverterRotationDirection(
preset.data.inverterRotationDirection === "Forward",
preset?.data?.inverterRotationDirection === "Forward",
);
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
setExtruderPressureLimit(preset?.data?.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset?.data?.pressureLimitEnabled ?? true);
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
setPressurePidKp(preset?.data?.pidPressureKp ?? 0.16);
setPressurePidKi(preset?.data?.pidPressureKi ?? 0.0);
setPressurePidKd(preset?.data?.pidPressureKd ?? 0.008);
};
return (
@@ -1,7 +1,7 @@
import React from "react";
import { z } from "zod";
import { useLaser1 } from "./useLaser1";
import { Preset, PresetData } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
import { laser1 } from "@/machines/properties";
import { PresetPreviewEntries } from "@/components/preset/PresetPreviewTable";
import { PresetsPage } from "@/components/preset/PresetsPage";
@@ -14,7 +14,7 @@ const laser1PresetDataSchema = z
})
.partial();
type Laser1 = typeof laser1PresetDataSchema;
type Laser1 = z.infer<typeof laser1PresetDataSchema>;
const schemas = new Map([[1, laser1PresetDataSchema]]);
@@ -22,18 +22,17 @@ const previewEntries: PresetPreviewEntries<Laser1> = [
{
name: "Target Diameter",
unit: "mm",
renderValue: (data: PresetData<Laser1>) => data?.targetDiameter?.toFixed(3),
renderValue: (data: Laser1) => data?.targetDiameter?.toFixed(3),
},
{
name: "Lower Tolerance",
unit: "mm",
renderValue: (data: PresetData<Laser1>) => data?.lowerTolerance?.toFixed(3),
renderValue: (data: Laser1) => data?.lowerTolerance?.toFixed(3),
},
{
name: "Higher Tolerance",
unit: "mm",
renderValue: (data: PresetData<Laser1>) =>
data?.higherTolerance?.toFixed(3),
renderValue: (data: Laser1) => data?.higherTolerance?.toFixed(3),
},
];
@@ -47,16 +46,16 @@ export function Laser1PresetsPage() {
} = useLaser1();
const applyPreset = (preset: Preset<Laser1>) => {
const targetDiameter = preset.data.targetDiameter ?? 1.75;
const lowerTolerance = preset.data?.lowerTolerance ?? 0.05;
const higherTolerance = preset.data?.higherTolerance ?? 0.05;
const targetDiameter = preset?.data?.targetDiameter ?? 1.75;
const lowerTolerance = preset?.data?.lowerTolerance ?? 0.05;
const higherTolerance = preset?.data?.higherTolerance ?? 0.05;
setTargetDiameter(targetDiameter);
setLowerTolerance(lowerTolerance);
setHigherTolerance(higherTolerance);
};
const toPresetData = (s: typeof state): PresetData<Laser1> => ({
const toPresetData = (s: typeof state): Laser1 => ({
targetDiameter: s?.laser_state.target_diameter,
lowerTolerance: s?.laser_state.lower_tolerance,
higherTolerance: s?.laser_state.higher_tolerance,
@@ -3,7 +3,7 @@ import { useMock1 } from "./useMock";
import { mock1 } from "@/machines/properties";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { Preset, PresetData } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
import { PresetPreviewEntries } from "@/components/preset/PresetPreviewTable";
import { z } from "zod";
@@ -15,7 +15,7 @@ const mock1PresetDataSchema = z
})
.partial();
type Mock1 = typeof mock1PresetDataSchema;
type Mock1 = z.infer<typeof mock1PresetDataSchema>;
const schemas = new Map([[1, mock1PresetDataSchema]]);
@@ -23,17 +23,17 @@ const previewEntries: PresetPreviewEntries<Mock1> = [
{
name: "Frequency 1",
unit: "mHz",
renderValue: (data: PresetData<Mock1>) => data?.frequency1?.toFixed(3),
renderValue: (data: Mock1) => data?.frequency1?.toFixed(3),
},
{
name: "Frequency 2",
unit: "mHz",
renderValue: (data: PresetData<Mock1>) => data?.frequency2?.toFixed(3),
renderValue: (data: Mock1) => data?.frequency2?.toFixed(3),
},
{
name: "Frequency 3",
unit: "mHz",
renderValue: (data: PresetData<Mock1>) => data?.frequency3?.toFixed(3),
renderValue: (data: Mock1) => data?.frequency3?.toFixed(3),
},
];
@@ -51,7 +51,7 @@ export function Mock1PresetsPage() {
setFrequency3(frequency3);
};
const toPresetData = (s: typeof state): PresetData<Mock1> => ({
const toPresetData = (s: typeof state): Mock1 => ({
frequency1: s?.frequency1 ?? defaultState?.frequency1,
frequency2: s?.frequency2 ?? defaultState?.frequency2,
frequency3: s?.frequency3 ?? defaultState?.frequency3,
+25
View File
@@ -533,6 +533,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 +568,7 @@ export const machineProperties: MachineProperties[] = [
aquapath1,
testmachine,
analogInputTestMachine,
wagoAiTestMachine,
ip20TestMachine,
];
+6 -4
View File
@@ -2,7 +2,7 @@
// each role can only be given once
import { IconName } from "@/components/Icon";
import { rustEnumSchema } from "@/lib/types";
import { rustEnum } from "@/lib/types";
import { z } from "zod";
// EK1100 should have role 0
@@ -62,9 +62,11 @@ export type DeviceHardwareIdentificationEthercat = z.infer<
typeof deviceHardwareIdentificationEthercatSchema
>;
export const deviceHardwareIdentificationSchema = rustEnumSchema({
Ethercat: deviceHardwareIdentificationEthercatSchema,
});
export const deviceHardwareIdentificationSchema = z
.object({
Ethercat: deviceHardwareIdentificationEthercatSchema,
})
.check(rustEnum);
export type DeviceHardwareIdentification = z.infer<
typeof deviceHardwareIdentificationSchema
@@ -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,
});
@@ -3,7 +3,7 @@ import { useWinder2 } from "./useWinder";
import { winder2 } from "@/machines/properties";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { Preset, PresetData } from "@/lib/preset/preset";
import { Preset } from "@/lib/preset/preset";
import {
pullerStateSchema,
spoolSpeedControllerStateSchema,
@@ -16,19 +16,21 @@ import {
const winder2PresetDataSchema = z
.object({
traverse_state: z.object({
limit_inner: z.number(),
limit_outer: z.number(),
step_size: z.number(),
padding: z.number(),
laserpointer: z.boolean(),
}),
puller_state: pullerStateSchema,
spool_speed_controller_state: spoolSpeedControllerStateSchema,
traverse_state: z
.object({
limit_inner: z.number(),
limit_outer: z.number(),
step_size: z.number(),
padding: z.number(),
laserpointer: z.boolean(),
})
.partial(),
puller_state: pullerStateSchema.partial(),
spool_speed_controller_state: spoolSpeedControllerStateSchema.partial(),
})
.deepPartial();
.partial();
type Winder2 = typeof winder2PresetDataSchema;
type Winder2 = z.infer<typeof winder2PresetDataSchema>;
const schemas = new Map([[1, winder2PresetDataSchema]]);
@@ -36,40 +38,39 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
{
name: "Inner Traverse Limit",
unit: "mm",
renderValue: (data: PresetData<Winder2>) =>
data?.traverse_state?.limit_inner?.toFixed(1) ?? "N/A",
renderValue: (data: Winder2) =>
data.traverse_state?.limit_inner?.toFixed(1) ?? "N/A",
},
{
name: "Outer Traverse Limit",
unit: "mm",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data?.traverse_state?.limit_outer?.toFixed(1) ?? "N/A",
},
{
name: "Traverse Step Size",
unit: "mm",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data?.traverse_state?.step_size?.toFixed(1) ?? "N/A",
},
{
name: "Traverse Padding",
unit: "mm",
renderValue: (data: PresetData<Winder2>) =>
data?.traverse_state?.padding?.toFixed(1),
renderValue: (data: Winder2) => data?.traverse_state?.padding?.toFixed(1),
},
previewSeparator,
{
name: "Puller Regulation",
renderValue: (data: PresetData<Winder2>) => data?.puller_state?.regulation,
renderValue: (data: Winder2) => data?.puller_state?.regulation,
},
{
name: "Puller Direction",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.puller_state?.forward ? "Forward" : "Backward",
},
{
name: "Puller Gear Ratio",
renderValue: (data: PresetData<Winder2>) => {
renderValue: (data: Winder2) => {
const ratio = data.puller_state?.gear_ratio;
if (ratio === "OneToOne") return "1:1";
if (ratio === "OneToFive") return "1:5";
@@ -80,74 +81,73 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
{
name: "Puller Target Speed",
unit: "m/min",
renderValue: (data: PresetData<Winder2>) =>
data.puller_state?.target_speed?.toFixed(2),
renderValue: (data: Winder2) => data.puller_state?.target_speed?.toFixed(2),
},
{
name: "Puller Target Diameter",
unit: "mm",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.puller_state?.target_diameter?.toFixed(1),
},
{
name: "Puller Target Diameter",
unit: "mm",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.puller_state?.target_diameter?.toFixed(1),
},
previewSeparator,
{
name: "Spool Regulation",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.regulation_mode,
},
{
name: "Spool Direction",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.forward ? "Forward" : "Reverse",
},
{
name: "Spool Min Speed",
unit: "rpm",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.minmax_min_speed?.toFixed(2),
},
{
name: "Spool Max Speed",
unit: "rpm",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.minmax_max_speed?.toFixed(2),
},
previewSeparator,
{
name: "Adaptive Spool Tension Target",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.adaptive_tension_target?.toFixed(2),
},
{
name: "Adaptive Spool Learning Rate",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.adaptive_radius_learning_rate?.toFixed(
2,
),
},
{
name: "Adaptive Spool Max Speed Multiplier",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.adaptive_max_speed_multiplier?.toFixed(
1,
),
},
{
name: "Adaptive Spool Acceleration Factor",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.adaptive_acceleration_factor?.toFixed(
2,
),
},
{
name: "Adaptive Spool Deaccel. Urgency",
renderValue: (data: PresetData<Winder2>) =>
renderValue: (data: Winder2) =>
data.spool_speed_controller_state?.adaptive_deacceleration_urgency_multiplier?.toFixed(
1,
),
@@ -230,11 +230,11 @@ export function Winder2PresetsPage() {
// );
enableTraverseLaserpointer(
preset.data.traverse_state?.laserpointer ?? false,
preset.data?.traverse_state?.laserpointer ?? false,
);
};
const toPresetData = (s: typeof state): PresetData<Winder2> => ({
const toPresetData = (s: typeof state): Winder2 => ({
traverse_state: {
limit_inner: s?.traverse_state?.limit_inner,
limit_outer: s?.traverse_state?.limit_outer,
+29 -6
View File
@@ -4,7 +4,6 @@ import React from "react";
import { z } from "zod";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { SidebarLayout } from "@/components/SidebarLayout";
import { SetupPage } from "@/setup/SetupPage";
@@ -53,12 +52,15 @@ 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 { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
@@ -100,6 +102,18 @@ 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 ip20TestMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "ip20testmachine/$serial",
@@ -315,6 +329,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",
@@ -381,9 +401,9 @@ export const metricsRoute = createRoute({
export const versionSearchSchema = z
.object({
branch: fallback(z.string().optional(), undefined),
commit: fallback(z.string().optional(), undefined),
tag: fallback(z.string().optional(), undefined),
branch: z.string().optional(),
commit: z.string().optional(),
tag: z.string().optional(),
})
.merge(githubSourceSchema)
.refine(
@@ -405,14 +425,14 @@ export const updateChangelogRoute = createRoute({
getParentRoute: () => updateRoute,
path: "changelog",
component: () => <ChangelogPage />,
validateSearch: zodValidator(versionSearchSchema),
validateSearch: versionSearchSchema,
});
export const updateExecuteRoute = createRoute({
getParentRoute: () => updateRoute,
path: "execute",
component: () => <UpdateExecutePage />,
validateSearch: zodValidator(versionSearchSchema),
validateSearch: versionSearchSchema,
});
export const rootTree = RootRoute.addChildren([
@@ -440,11 +460,14 @@ export const rootTree = RootRoute.addChildren([
analogInputTestMachineControlRoute,
]),
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
aquapath1GraphRoute,
aquapath1SettingsRoute,
]),
winder2SerialRoute.addChildren([
+9 -4
View File
@@ -37,7 +37,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useFormValues } from "@/lib/useFormValues";
import { validateU16 } from "@/lib/validation";
import { DeviceRoleComponent } from "@/components/DeviceRole";
import { Alert } from "@/components/Alert";
import { Separator } from "@/components/ui/separator";
@@ -54,9 +53,15 @@ type Props = {
};
const formSchema = z.object({
machine: z.string().superRefine(validateU16),
serial: z.string().superRefine(validateU16),
role: z.string().superRefine(validateU16),
machine: z
.string()
.refine((v) => parseInt(v) < 0xffff, { error: "Value too big" }),
serial: z
.string()
.refine((v) => parseInt(v) < 0xffff, { error: "Value too big" }),
role: z
.string()
.refine((v) => parseInt(v) < 0xffff, { error: "Value too big" }),
});
type FormSchema = z.infer<typeof formSchema>;
+4 -5
View File
@@ -24,7 +24,6 @@ import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Icon } from "@/components/Icon";
import { TouchButton } from "@/components/touch/TouchButton";
import { fallback } from "@tanstack/zod-adapter";
type Props = {
value: GithubSource;
@@ -32,9 +31,9 @@ type Props = {
};
export const githubSourceSchema = z.object({
githubRepoOwner: fallback(z.string(), "qitechgmbh"),
githubRepoName: fallback(z.string(), "control"),
githubToken: fallback(z.string().optional().nullable(), undefined),
githubRepoOwner: z.string().catch("qitechgmbh"),
githubRepoName: z.string().catch("control"),
githubToken: z.string().optional(),
});
export type GithubSource = z.infer<typeof githubSourceSchema>;
@@ -91,7 +90,7 @@ export function GithubSourceDialogContent({
const onSubmit = (values: GithubSource) => {
onChange({
...values,
githubToken: values.githubToken === "" ? null : values.githubToken,
githubToken: values.githubToken === "" ? undefined : values.githubToken,
});
setOpen(false);
};
+2
View File
@@ -328,6 +328,8 @@ impl TxPdoObject for Standard22ByteMdp600Input {
self.status.overrun_error = bits[6];
// Bit7 is reserved/unused
self.length = bits[8..8 + 8].load_le::<u8>();
// the serial bytes start at bit 16, then we calculate the offset of when the serial bytes buffer is over, which for Standard22ByteMdp600Input is 22bytes
// to get the size in bits we do 22 * 8 -> 176bits
let serial_bytes = bits[16..(16 + 22 * 8_usize)].chunks_exact(8);
for (i, val) in serial_bytes.enumerate() {
self.data[i] = val.load_le();
+18
View File
@@ -7,7 +7,9 @@ use crate::{
devices::{
DynamicEthercatDevice, Module,
wago_modules::{
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},
wago_750_1506::{WAGO_750_1506_MODULE_IDENT, WAGO_750_1506_PRODUCT_ID},
},
@@ -227,10 +229,18 @@ 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;
}
WAGO_750_530_PRODUCT_ID => {
module.has_tx = false;
module.has_rx = true;
}
WAGO_750_652_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
@@ -259,6 +269,8 @@ impl Wago750_354 {
let _ = self.get_pdo_offsets(device, false).await;
});
println!("{:?}\n\n{:?}", self.tx_offsets, self.rx_offsets);
for module in self.slots {
match module {
Some(m) => {
@@ -267,9 +279,15 @@ 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()))
}
WAGO_750_530_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_530::Wago750_530::new()))
}
WAGO_750_1506_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_1506::Wago750_1506::new()))
}
@@ -2,4 +2,5 @@ pub mod ip20_ec_di8_do8;
pub mod wago_750_1506;
pub mod wago_750_455;
pub mod wago_750_501;
pub mod wago_750_530;
pub mod wago_750_652;
@@ -1,91 +1,64 @@
use units::{ElectricCurrent, electric_current::milliampere};
use bitvec::field::BitField;
use crate::{
devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
},
helpers::signing_converter_u16::U16SigningConverter,
io::{
analog_input::{AnalogInputDevice, AnalogInputInput, physical::AnalogInputRange},
digital_input::DigitalInputInput,
},
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;
Wago750_455Presentation {
}
#[derive(Clone)]
pub struct Wago750_455 {
configuration: (),
txpdo: Wago750_455RxPdo,
isUsed: bool,
}
pub struct Wago750_455Configuration {
}
#[derive(Debug, Clone)]
pub enum Wago750_455InputPort {
#[derive(Clone, Debug)]
pub enum Wago750_455Port {
AI1,
AI2,
AI3,
AI4,
}
impl From<Wago750_455InputPort> for usize {
fn from(value: Wago750_455InputPort) -> Self {
impl From<Wago750_455Port> for usize {
fn from(value: Wago750_455Port) -> Self {
match value {
Wago750_455InputPort::AI1 => 0,
Wago750_455InputPort::AI2 => 1,
Wago750_455InputPort::AI3 => 2,
Wago750_455InputPort::AI4 => 3,
Wago750_455Port::AI1 => 0,
Wago750_455Port::AI2 => 16,
Wago750_455Port::AI3 => 32,
Wago750_455Port::AI4 => 48,
}
}
}
#[derive(Clone, Default)]
/**
# Data Word, 16 bits
- B15 ... B4 hold the measured value (B15 is the sign bit which should always be 0)
- B3 and B2 are 00 when measuring <~3.0mA, 11 when measuring >~21.0mA and undefined inbetween.
- B1 and B0 are 11' on an overflow, underflow or wire break. 00' otherwise
*/
pub struct Wago750_455RxPdo {
/** Low Byte: D0, High Byte: D1 */
channel_1: u16,
/** Low Byte: D2, High Byte: D3 */
channel_2: u16,
/** Low Byte: D4, High Byte: D5 */
channel_3: u16,
/** Low Byte: D6, High Byte: D7 */
channel_4: u16,
pub struct Wago750_455TxPdo {
pub ai1: u16,
pub ai2: u16,
pub ai3: u16,
pub ai4: u16,
}
impl AnalogInputDevice<Wago750_455InputPort> for Wago750_455 {
fn get_input(&self, port: Wago750_455InputPort) -> AnalogInputInput {
let raw_value = match port {
Wago750_455InputPort::AI1 => self.txpdo.channel_1,
Wago750_455InputPort::AI2 => self.txpdo.channel_2,
Wago750_455InputPort::AI3 => self.txpdo.channel_3,
Wago750_455InputPort::AI4 => self.txpdo.channel_4,
#[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 signing_bit = raw_value & 0x8000;
// The 4 least significant bits are reserved and diagnostic and the MSB is the signing bit.
let shifted_raw_value = (raw_value & 0x7FF0) >> 4;
// Since only 11 bits actually represent the measurement (ignoring the signing bit) only 2048 different states can be measured.
let prepared_value = U16SigningConverter::load_raw(shifted_raw_value | signing_bit);
let value = prepared_value.as_signed() as i16;
let normalized = f32::from(value) / f32::from(2047);
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: false,
wiring_error,
}
}
@@ -94,7 +67,132 @@ impl AnalogInputDevice<Wago750_455InputPort> for Wago750_455 {
min: ElectricCurrent::new::<milliampere>(4.0),
max: ElectricCurrent::new::<milliampere>(20.0),
min_raw: 0,
max_raw: 2047,
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);
+260
View File
@@ -0,0 +1,260 @@
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
SubDeviceProductTuple,
};
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
#[derive(Clone)]
pub struct Wago750_530 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub rxpdo: Wago750_530RxPdo,
module: Option<Module>,
}
#[derive(Debug, Clone)]
pub enum Wago750_530Port {
Port1,
Port2,
Port3,
Port4,
Port5,
Port6,
Port7,
Port8,
}
impl From<Wago750_530Port> for usize {
fn from(value: Wago750_530Port) -> Self {
match value {
Wago750_530Port::Port1 => 0,
Wago750_530Port::Port2 => 1,
Wago750_530Port::Port3 => 2,
Wago750_530Port::Port4 => 3,
Wago750_530Port::Port5 => 4,
Wago750_530Port::Port6 => 5,
Wago750_530Port::Port7 => 6,
Wago750_530Port::Port8 => 7,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_530RxPdo {
pub port1: bool,
pub port2: bool,
pub port3: bool,
pub port4: bool,
pub port5: bool,
pub port6: bool,
pub port7: bool,
pub port8: bool,
}
impl DigitalOutputDevice<Wago750_530Port> for Wago750_530 {
/// Writes the new output value into the device's RXPDO structure (in-memory PDI).
fn set_output(&mut self, port: Wago750_530Port, value: DigitalOutputOutput) {
// The DigitalOutputOutput is converted to a bool using the From trait
let output_value: bool = value.into();
match port {
Wago750_530Port::Port1 => {
// Modify the internal PDO representation
self.rxpdo.port1 = output_value;
}
Wago750_530Port::Port2 => {
// Modify the internal PDO representation
self.rxpdo.port2 = output_value;
}
Wago750_530Port::Port3 => {
// Modify the internal PDO representation
self.rxpdo.port3 = output_value;
}
Wago750_530Port::Port4 => {
// Modify the internal PDO representation
self.rxpdo.port4 = output_value;
}
Wago750_530Port::Port5 => {
// Modify the internal PDO representation
self.rxpdo.port5 = output_value;
}
Wago750_530Port::Port6 => {
// Modify the internal PDO representation
self.rxpdo.port6 = output_value;
}
Wago750_530Port::Port7 => {
// Modify the internal PDO representation
self.rxpdo.port7 = output_value;
}
Wago750_530Port::Port8 => {
// Modify the internal PDO representation
self.rxpdo.port8 = output_value;
}
}
}
/// Reads the current output value from the device's RXPDO structure (in-memory PDI).
fn get_output(&self, port: Wago750_530Port) -> DigitalOutputOutput {
let current_value = match port {
Wago750_530Port::Port1 => self.rxpdo.port1,
Wago750_530Port::Port2 => self.rxpdo.port2,
Wago750_530Port::Port3 => self.rxpdo.port3,
Wago750_530Port::Port4 => self.rxpdo.port4,
Wago750_530Port::Port5 => self.rxpdo.port5,
Wago750_530Port::Port6 => self.rxpdo.port6,
Wago750_530Port::Port7 => self.rxpdo.port7,
Wago750_530Port::Port8 => self.rxpdo.port8,
};
// Wrap the bool back into the type-safe wrapper
DigitalOutputOutput(current_value)
}
}
impl EthercatDeviceUsed for Wago750_530 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_530 {}
impl EthercatDynamicPDO for Wago750_530 {
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_530 {
fn input(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn input_len(&self) -> usize {
0
}
fn output(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port1),
self.rxpdo.port1,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port2),
self.rxpdo.port2,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port3),
self.rxpdo.port3,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port4),
self.rxpdo.port4,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port5),
self.rxpdo.port5,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port6),
self.rxpdo.port6,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port7),
self.rxpdo.port7,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port8),
self.rxpdo.port8,
);
Ok(())
}
fn output_len(&self) -> usize {
8
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_checked(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn get_module(&self) -> Option<crate::devices::Module> {
self.module.clone()
}
fn set_module(&mut self, module: crate::devices::Module) {
self.tx_bit_offset = module.tx_offset;
self.rx_bit_offset = module.rx_offset;
self.module = Some(module)
}
}
impl EthercatDeviceProcessing for Wago750_530 {}
impl NewEthercatDevice for Wago750_530 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rxpdo: Wago750_530RxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_530 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_530")
}
}
pub const WAGO_750_530_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_530_PRODUCT_ID: u32 = 2147483778;
pub const WAGO_750_530_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_530_VENDOR_ID, WAGO_750_530_PRODUCT_ID);
@@ -1,8 +1,12 @@
use bitvec::field::BitField;
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
SubDeviceProductTuple,
};
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
use crate::io::serial_interface::SerialInterfaceDevice;
const WAGO750_652_MAX_BUF_LENGTH: usize = 48;
#[derive(Clone)]
pub struct Wago750_652 {
@@ -10,6 +14,62 @@ pub struct Wago750_652 {
tx_bit_offset: usize,
rx_bit_offset: usize,
module: Option<Module>,
started_init: bool,
pub rx_pdo: Wago750_652RxPdo,
pub tx_pdo: Wago750_652TxPdo,
has_messages_last_toggle: bool,
}
#[derive(Clone, Default, Debug)]
pub struct Wago750_652Control {
pub transmit_request: bool, // Transmit request
pub receive_ack: bool, // Receive acknowledge
pub init_request: bool, // Initialization request
pub send_continuous: bool, // Send continuous
pub output_length: usize,
}
#[derive(Clone, Debug)]
pub struct Wago750_652RxPdo {
pub control: Wago750_652Control,
pub out_buffer: [u8; WAGO750_652_MAX_BUF_LENGTH],
}
impl Default for Wago750_652RxPdo {
fn default() -> Self {
Self {
control: Default::default(),
out_buffer: [0u8; WAGO750_652_MAX_BUF_LENGTH],
}
}
}
#[derive(Clone, Default, Debug)]
pub struct Wago750_652Status {
pub transmit_ack: bool, //
pub receive_request: bool,
pub init_ack: bool,
pub buf_full: bool,
pub buf_empty: bool, // Buffer is empty (Sending inactive)
pub error_parity: bool,
pub error_framing: bool, // Frame is corrupt?
pub error_overrun: bool, // Byte/s was lost on receive
pub input_length: usize,
}
#[derive(Clone, Debug)]
pub struct Wago750_652TxPdo {
pub status: Wago750_652Status,
pub in_buffer: [u8; WAGO750_652_MAX_BUF_LENGTH],
}
impl Default for Wago750_652TxPdo {
fn default() -> Self {
Self {
status: Default::default(),
in_buffer: [0u8; WAGO750_652_MAX_BUF_LENGTH],
}
}
}
impl EthercatDeviceUsed for Wago750_652 {
@@ -23,7 +83,6 @@ impl EthercatDeviceUsed for Wago750_652 {
}
impl DynamicEthercatDevice for Wago750_652 {}
impl EthercatDynamicPDO for Wago750_652 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
@@ -48,24 +107,84 @@ impl EthercatDevice for Wago750_652 {
*/
fn input(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
self.tx_pdo.in_buffer.fill(0u8);
let base = self.tx_bit_offset;
self.tx_pdo.status.transmit_ack = *input.get(base + 0).expect("Missing buf_empty bit");
self.tx_pdo.status.receive_request = *input.get(base + 1).expect("Missing buf_empty bit");
self.tx_pdo.status.init_ack = *input.get(base + 2).expect("Missing buf_empty bit");
self.tx_pdo.status.buf_full = *input.get(base + 6).expect("Missing buf_empty bit");
self.tx_pdo.status.buf_empty = *input.get(base + 11).expect("Missing buf_empty bit");
self.tx_pdo.status.error_parity = *input.get(base + 13).expect("Missing buf_empty bit");
self.tx_pdo.status.error_framing = *input.get(base + 14).expect("Missing buf_empty bit");
self.tx_pdo.status.error_overrun = *input.get(base + 15).expect("Missing buf_empty bit");
// Im sorry but this is what you get by splitting the len across different bytes ...
self.tx_pdo.status.input_length = ((*input.get(base + 3).expect("Missing buf_empty bit")
as u8)
^ ((*input.get(base + 4).expect("Missing buf_empty bit") as u8) << 1)
^ ((*input.get(base + 5).expect("Missing buf_empty bit") as u8) << 2)
^ ((*input.get(base + 8).expect("Missing buf_empty bit") as u8) << 3)
^ ((*input.get(base + 9).expect("Missing buf_empty bit") as u8) << 4)
^ ((*input.get(base + 10).expect("Missing buf_empty bit") as u8) << 5))
as usize;
// the serial bytes start at bit 16,
// then we calculate the offset of when the serial bytes buffer is over, which for the 24byte pdo is 22bytes
// to get the size in bits we do 22 * 8 -> 176bits
let serial_bytes = input[(base + 16)..((base + 16) + 22 * 8_usize)].chunks_exact(8);
for (i, val) in serial_bytes.enumerate() {
self.tx_pdo.in_buffer[i] = val.load_le();
}
Ok(())
}
fn input_len(&self) -> usize {
0
24
}
fn output(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = 32;
let mut ol0_i = 5; // Ol0 starts at bit 5 and goes to bit 7 (inclusive)
let mut ol1_i = 8;
output.set(base + 0, self.rx_pdo.control.transmit_request);
output.set(base + 1, self.rx_pdo.control.receive_ack);
output.set(base + 2, self.rx_pdo.control.init_request);
output.set(base + 3, self.rx_pdo.control.send_continuous);
for i in 0..3 {
// Rust type system proceeds to annoy once again, we need to add a != at the end, because a cast to bool is just too dangeerous obviously
output.set(
base + ol0_i,
((self.rx_pdo.control.output_length >> i) & 1) != 0,
);
ol0_i += 1;
}
for i in 3..6 {
output.set(
base + ol1_i,
((self.rx_pdo.control.output_length >> i) & 1) != 0,
);
ol1_i += 1;
}
println!("{}", self.rx_pdo.control.output_length);
let data_start = base + 16;
for (i, &byte) in self.rx_pdo.out_buffer.iter().take(22).enumerate() {
output[(data_start + i * 8)..(data_start + (i + 1) * 8)].store_le(byte);
}
//println!("{:?}",self.rx_pdo.out_buffer);
Ok(())
}
fn output_len(&self) -> usize {
0
24
}
fn as_any(&self) -> &dyn std::any::Any {
@@ -84,19 +203,6 @@ impl EthercatDevice for Wago750_652 {
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
// validate input has correct length
let expected = self.input_len();
let actual = input.len();
if actual != expected {
return Err(anyhow::anyhow!(
"[{}::Device::input_checked] Input length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
self.input(input)
}
@@ -105,21 +211,6 @@ impl EthercatDevice for Wago750_652 {
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
self.output(output)?;
// validate input has correct length
let expected = self.output_len();
let actual = output.len();
if output.len() != expected {
return Err(anyhow::anyhow!(
"[{}::Device::output_checked] Output length is {} ({} bytes) and must be {} bits ({} bytes)",
module_path!(),
actual,
actual / 8,
expected,
expected / 8
));
}
Ok(())
}
@@ -143,6 +234,10 @@ impl NewEthercatDevice for Wago750_652 {
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rx_pdo: Wago750_652RxPdo::default(),
tx_pdo: Wago750_652TxPdo::default(),
started_init: false,
has_messages_last_toggle: false,
}
}
}
@@ -153,6 +248,108 @@ impl std::fmt::Debug for Wago750_652 {
}
}
#[derive(Clone)]
pub enum Wago750_652Port {
SI1, // Serial
}
impl SerialInterfaceDevice<Wago750_652Port> for Wago750_652 {
fn serial_interface_read_message(&mut self, port: Wago750_652Port) -> Option<Vec<u8>> {
if !self.serial_interface_has_messages(port) {
return None;
} else {
let in_len = self.tx_pdo.status.input_length;
let received_data = self.tx_pdo.in_buffer[0..in_len].to_vec();
if received_data.is_empty() {
return None;
}
self.has_messages_last_toggle = self.tx_pdo.status.receive_request;
self.rx_pdo.control.receive_ack = !self.rx_pdo.control.receive_ack;
Some(received_data)
}
}
fn serial_interface_write_message(
&mut self,
_port: Wago750_652Port,
message: Vec<u8>,
) -> Result<bool, anyhow::Error> {
if message.len() > WAGO750_652_MAX_BUF_LENGTH {
return Err(anyhow::anyhow!(
"Message is too long for RxPdo Buffer of 22 bytes!"
));
}
if message.is_empty() {
return Ok(self.rx_pdo.control.transmit_request == self.tx_pdo.status.transmit_ack);
}
let bytes = message.as_slice();
self.rx_pdo.control.output_length = message.len();
self.rx_pdo.out_buffer.fill(0u8);
self.rx_pdo.out_buffer[0..message.len()].copy_from_slice(&bytes[0..message.len()]);
self.rx_pdo.control.transmit_request = !self.rx_pdo.control.transmit_request;
Ok(true)
}
fn serial_interface_has_messages(&mut self, _port: Wago750_652Port) -> bool {
return self.tx_pdo.status.receive_request != self.has_messages_last_toggle;
}
fn get_serial_encoding(
&self,
_port: Wago750_652Port,
) -> Option<crate::io::serial_interface::SerialEncoding> {
None
}
fn get_baudrate(&self, _port: Wago750_652Port) -> Option<u32> {
// Right now we cant change it on the fly because WAGO ONLY supports setting via
// Codesys OR WAGO I/O Check the way it is set with Codesys is also NOT documented ...
// My Hunch is that the 0x2010 0x2011 register can be used to configure that with COE its not documented though
None
}
/*
Needs to be called multiple times until it returns true, at which point it is ready to be used
*/
fn serial_interface_initialize(&mut self, port: Wago750_652Port) -> bool {
match port {
Wago750_652Port::SI1 => {
if !self.rx_pdo.control.init_request
&& !self.tx_pdo.status.init_ack
&& !self.started_init
{
self.rx_pdo.control.init_request = true;
self.started_init = true;
return false;
}
if self.rx_pdo.control.init_request && self.tx_pdo.status.init_ack {
self.rx_pdo.control.init_request = false;
return false;
}
if !self.rx_pdo.control.init_request && self.tx_pdo.status.init_ack {
return false;
}
if !self.rx_pdo.control.init_request
&& !self.tx_pdo.status.init_ack
&& self.started_init
{
self.has_messages_last_toggle = self.tx_pdo.status.receive_request;
return true;
}
false
}
}
}
}
pub const WAGO_750_652_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_652_PRODUCT_ID: u32 = 106043250;
pub const WAGO_750_652_MODULE_IDENT: SubDeviceProductTuple =
+2
View File
@@ -52,6 +52,8 @@
devShells.default = pkgs.mkShell {
packages = with pkgs; [
cargo
rustc
pkg-config
libudev-zero
libpcap
+17 -6
View File
@@ -1,21 +1,32 @@
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::{MachineAct, analog_input_test_machine::AnalogInputTestMachine};
use crate::{
MachineAct, MachineMessage, MachineValues, analog_input_test_machine::AnalogInputTestMachine,
};
impl MachineAct for AnalogInputTestMachine {
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
crate::MachineMessage::SubscribeNamespace(namespace) => {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_measurement_rate();
}
crate::MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
crate::MachineMessage::HttpApiJsonRequest(value) => {
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
crate::MachineMessage::ConnectToMachine(_machine_connection) => {}
crate::MachineMessage::DisconnectMachine(_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();
}
}
}
+15 -8
View File
@@ -1,5 +1,5 @@
use super::{AquaPathV1, AquaPathV1Mode};
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for AquaPathV1 {
@@ -45,14 +45,21 @@ impl MachineAct for AquaPathV1 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
}
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
/*Doesnt connect to any Machine so 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::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
+56
View File
@@ -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(())
}
+82 -27
View File
@@ -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();
}
}
}
}
+115 -10
View File
@@ -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,
},
@@ -109,8 +111,8 @@ impl std::fmt::Display for AquaPathV1 {
}
impl AquaPathV1 {
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
front_temperature: self
.front_controller
.current_temperature
@@ -123,13 +125,28 @@ 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>(),
};
let event = live_values.build();
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(),
}
}
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(AquaPathV1Events::LiveValues(event));
}
pub fn emit_state(&mut self) {
let state = StateEvent {
pub fn get_state(&self) -> StateEvent {
StateEvent {
is_default_state: false,
mode_state: ModeState {
mode: self.mode.clone(),
@@ -167,9 +184,55 @@ 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>(),
},
},
}
}
let event = state.build();
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(AquaPathV1Events::State(event));
}
}
@@ -269,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();
}
}
+8 -4
View File
@@ -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,
+15 -8
View File
@@ -1,5 +1,5 @@
use super::BufferV1;
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for BufferV1 {
@@ -33,14 +33,21 @@ impl MachineAct for BufferV1 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
}
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
/*Doesnt connect to any Machine so 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::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
+13 -7
View File
@@ -48,21 +48,27 @@ impl BufferV1 {
vendor: VENDOR_QITECH,
machine: MACHINE_BUFFER_V1,
};
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {};
let event = live_values.build();
pub fn get_live_values(&mut self) -> LiveValuesEvent {
LiveValuesEvent {}
}
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(BufferV1Events::LiveValues(event));
}
pub fn emit_state(&mut self) {
let state = StateEvent {
pub fn get_state(&self) -> StateEvent {
StateEvent {
mode_state: ModeState {
mode: self.mode.clone(),
},
// connected_machine_state: self.connected_winder.to_state(),
};
}
}
let event = state.build();
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(BufferV1Events::State(event));
}
+15 -5
View File
@@ -1,7 +1,7 @@
#[cfg(not(feature = "mock-machine"))]
use crate::extruder1::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
@@ -41,6 +41,7 @@ impl MachineAct for ExtruderV2 {
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.update_total_energy(now);
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
@@ -61,11 +62,20 @@ impl MachineAct for ExtruderV2 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
/*Doesnt connect to any Machine so 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::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
+12 -17
View File
@@ -25,11 +25,11 @@ use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature::
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
use crate::extruder1::api::{TemperaturePid, TemperaturePidStates};
pub fn get_state(&self) -> StateEvent {
use crate::extruder1::api::TemperaturePidStates;
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
is_default_state: !self.emitted_default_state,
rotation_state: RotationState {
forward: self.screw_speed_controller.get_rotation_direction(),
},
@@ -141,16 +141,14 @@ impl ExtruderV2 {
},
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let state = self.get_state();
let hash = hash_with_serde_model(self.screw_speed_controller.get_inverter_status());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
self.emitted_default_state = true;
}
pub fn maybe_emit_state_event(&mut self) {
@@ -168,13 +166,8 @@ impl ExtruderV2 {
}
}
pub fn emit_live_values(&mut self) {
use std::time::Instant;
let now = Instant::now();
let combined_power = self.calculate_combined_power();
self.update_total_energy(combined_power, now);
let live_values = LiveValuesEvent {
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
nozzle_temperature: self
@@ -209,11 +202,13 @@ impl ExtruderV2 {
middle_power: self
.temperature_controller_middle
.get_heating_element_wattage(),
combined_power,
combined_power: self.calculate_combined_power(),
total_energy_kwh: self.total_energy_kwh,
};
}
}
let event = live_values.build();
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
+12 -1
View File
@@ -1,6 +1,6 @@
use super::ExtruderV2;
use crate::MachineAct;
use crate::MachineMessage;
use crate::{MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
@@ -40,6 +40,17 @@ impl MachineAct for ExtruderV2 {
{
()
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.build_state_event())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
}
+6 -4
View File
@@ -51,8 +51,8 @@ impl ExtruderV2 {
}
}
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
pub fn get_live_values(&mut self) -> LiveValuesEvent {
LiveValuesEvent {
motor_status: self.motor_status.clone(),
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
@@ -65,9 +65,11 @@ impl ExtruderV2 {
middle_power: self.middle_power,
combined_power: self.combined_power,
total_energy_kwh: self.total_energy_kwh,
};
}
}
let event = live_values.build();
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
+7 -3
View File
@@ -100,6 +100,9 @@ pub struct ExtruderV2 {
emitted_default_state: bool,
}
#[cfg(feature = "mock-machine")]
pub use mock::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
@@ -129,7 +132,7 @@ impl ExtruderV2 {
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
/// Calculate combined power consumption in watts
fn calculate_combined_power(&mut self) -> f64 {
fn calculate_combined_power(&self) -> f64 {
let motor_power = {
let motor_status = &self.screw_speed_controller.inverter.motor_status;
let voltage = motor_status.voltage.get::<volt>();
@@ -153,10 +156,11 @@ impl ExtruderV2 {
}
/// Update total energy consumption in kWh
fn update_total_energy(&mut self, current_power_watts: f64, now: Instant) {
fn update_total_energy(&mut self, now: Instant) {
let power_watts = self.calculate_combined_power();
if let Some(last_time) = self.last_energy_calculation_time {
let time_delta_hours = now.duration_since(last_time).as_secs_f64() / 3600.0;
let energy_delta_kwh = (current_power_watts / 1000.0) * time_delta_hours;
let energy_delta_kwh = (power_watts / 1000.0) * time_delta_hours;
self.total_energy_kwh += energy_delta_kwh;
}
self.last_energy_calculation_time = Some(now);
@@ -77,11 +77,11 @@ impl ScrewSpeedController {
self.nozzle_pressure_limit = pressure;
}
pub fn get_nozzle_pressure_limit(&mut self) -> Pressure {
pub fn get_nozzle_pressure_limit(&self) -> Pressure {
self.nozzle_pressure_limit
}
pub const fn get_nozzle_pressure_limit_enabled(&mut self) -> bool {
pub const fn get_nozzle_pressure_limit_enabled(&self) -> bool {
self.nozzle_pressure_limit_enabled
}
@@ -89,11 +89,11 @@ impl ScrewSpeedController {
self.nozzle_pressure_limit_enabled = enabled;
}
pub fn get_target_rpm(&mut self) -> AngularVelocity {
pub fn get_target_rpm(&self) -> AngularVelocity {
self.target_rpm
}
pub const fn get_rotation_direction(&mut self) -> bool {
pub const fn get_rotation_direction(&self) -> bool {
self.forward_rotation
}
@@ -129,7 +129,7 @@ impl ScrewSpeedController {
self.inverter.set_frequency_target(target_frequency);
}
pub const fn get_uses_rpm(&mut self) -> bool {
pub const fn get_uses_rpm(&self) -> bool {
self.uses_rpm
}
@@ -148,7 +148,7 @@ impl ScrewSpeedController {
self.motor_on = true;
}
pub fn get_motor_status(&mut self) -> MotorStatus {
pub fn get_motor_status(&self) -> MotorStatus {
let frequency = self.inverter.motor_status.frequency;
let rpm =
AngularVelocity::new::<revolution_per_minute>(frequency.get::<cycle_per_minute>());
@@ -197,7 +197,7 @@ impl ScrewSpeedController {
self.pid.reset()
}
pub fn get_pressure(&mut self) -> Pressure {
pub fn get_pressure(&self) -> Pressure {
let current_result = self.get_sensor_current();
let current = match current_result {
Ok(current) => current.get::<milliampere>(),
@@ -70,7 +70,7 @@ impl TemperatureController {
self.heating_allowed = true;
}
pub fn get_heating_element_wattage(&mut self) -> f64 {
pub fn get_heating_element_wattage(&self) -> f64 {
self.temperature_pid_output * self.heating_element_wattage
}
+14 -5
View File
@@ -2,7 +2,7 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
@@ -65,11 +65,20 @@ impl MachineAct for ExtruderV3 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.build_state_event())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
+11 -6
View File
@@ -9,6 +9,8 @@ use crate::extruder1::{
},
};
#[cfg(not(feature = "mock-machine"))]
use crate::extruder2::api::{LiveValuesEvent, StateEvent};
#[cfg(not(feature = "mock-machine"))]
use control_core::helpers::hasher_serializer::hash_with_serde_model;
#[cfg(not(feature = "mock-machine"))]
use control_core::socketio::event::BuildEvent;
@@ -24,7 +26,7 @@ use units::thermodynamic_temperature::ThermodynamicTemperature;
use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use super::{ExtruderV3, ExtruderV3Mode, api::StateEvent};
use super::{ExtruderV3, ExtruderV3Mode};
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
@@ -176,15 +178,14 @@ impl ExtruderV3 {
}
}
pub fn emit_live_values(&mut self) {
pub fn get_live_values(&mut self) -> LiveValuesEvent {
use std::time::Instant;
use crate::extruder2::api::{ExtruderV3Events, LiveValuesEvent};
let now = Instant::now();
let combined_power = self.calculate_combined_power();
self.update_total_energy(combined_power, now);
let live_values = LiveValuesEvent {
LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
nozzle_temperature: self
@@ -221,9 +222,13 @@ impl ExtruderV3 {
.get_heating_element_wattage(),
combined_power,
total_energy_kwh: self.total_energy_kwh,
};
}
}
let event = live_values.build();
pub fn emit_live_values(&mut self) {
use crate::extruder2::api::ExtruderV3Events;
let event = self.get_live_values().build();
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
+14 -6
View File
@@ -1,6 +1,5 @@
use super::ExtruderV2;
use crate::MachineAct;
use crate::MachineMessage;
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
@@ -34,11 +33,20 @@ impl MachineAct for ExtruderV2 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::ConnectToMachine(_machine_connection) => {}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
/*Doesnt connec 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::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
+10 -10
View File
@@ -10,10 +10,9 @@ use control_core::{
};
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
// bad performance wise, but doesnt matter its only a mock machine
pub fn get_state(&self) -> StateEvent {
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
is_default_state: !self.emitted_default_state,
rotation_state: self.rotation_state.clone(),
mode_state: self.mode_state.clone(),
regulation_state: self.regulation_state.clone(),
@@ -25,13 +24,12 @@ impl ExtruderV2 {
pid_settings: self.pid_settings.clone(),
}
}
}
impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let state = self.get_state();
let hash = hash_with_serde_model(self.inverter_status_state.clone());
self.last_status_hash = Some(hash);
self.emitted_default_state = true;
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
}
@@ -51,8 +49,8 @@ impl ExtruderV2 {
}
}
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
motor_status: self.motor_status.clone(),
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
@@ -65,9 +63,11 @@ impl ExtruderV2 {
middle_power: self.middle_power,
combined_power: self.combined_power,
total_energy_kwh: self.total_energy_kwh,
};
}
}
let event = live_values.build();
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
+12 -1
View File
@@ -1,5 +1,5 @@
use super::IP20TestMachine;
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for IP20TestMachine {
@@ -42,6 +42,17 @@ impl MachineAct for IP20TestMachine {
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::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
}
+10 -6
View File
@@ -45,21 +45,25 @@ impl IP20TestMachine {
}
impl IP20TestMachine {
pub fn emit_state(&mut self) {
let event = StateEvent {
pub fn get_state(&self) -> StateEvent {
StateEvent {
outputs: self.outputs,
}
.build();
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(IP20TestMachineEvents::State(event));
}
pub fn emit_live_values(&mut self) {
let event = LiveValuesEvent {
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
inputs: self.inputs,
}
.build();
}
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace
.emit(IP20TestMachineEvents::LiveValues(event));
}
+16 -7
View File
@@ -1,6 +1,7 @@
use super::LaserMachine;
use crate::MachineAct;
use crate::MachineMessage;
use crate::MachineValues;
use std::time::{Duration, Instant};
/// Implements the `MachineAct` trait for the `LaserMachine`.
@@ -41,7 +42,6 @@ impl MachineAct for LaserMachine {
}
fn act_machine_message(&mut self, msg: MachineMessage) {
tracing::info!("{:?}", msg);
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
@@ -61,13 +61,22 @@ impl MachineAct for LaserMachine {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
}
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
/*Doesnt connect to any Machine so 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::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
()
}
}
+17 -11
View File
@@ -97,22 +97,24 @@ impl LaserMachine {
machine: MACHINE_LASER_V1,
};
///diameter in mm
pub fn emit_live_values(&mut self) {
pub fn get_live_values(&self) -> LiveValuesEvent {
let diameter = self.diameter.get::<millimeter>();
let x_diameter = self.x_diameter.map(|x| x.get::<millimeter>());
let y_diameter = self.y_diameter.map(|y| y.get::<millimeter>());
let roundness = self.roundness;
let live_values = LiveValuesEvent {
LiveValuesEvent {
diameter,
x_diameter,
y_diameter,
roundness,
};
}
}
self.namespace
.emit(LaserEvents::LiveValues(live_values.build()));
///diameter in mm
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(LaserEvents::LiveValues(event));
}
pub fn build_state_event(&self) -> StateEvent {
@@ -129,19 +131,23 @@ impl LaserMachine {
}
}
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
pub fn get_state(&self) -> StateEvent {
StateEvent {
is_default_state: !self.emitted_default_state,
laser_state: LaserState {
higher_tolerance: self.laser_target.higher_tolerance.get::<millimeter>(),
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
target_diameter: self.laser_target.diameter.get::<millimeter>(),
in_tolerance: self.in_tolerance,
},
};
}
}
self.namespace.emit(LaserEvents::State(state.build()));
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(LaserEvents::State(event));
self.did_change_state = false;
self.emitted_default_state = true;
}
pub fn set_higher_tolerance(&mut self, higher_tolerance: f64) {
+31 -2
View File
@@ -13,8 +13,10 @@ use machine_identification::{
use serde::Serialize;
use smol::channel::{Receiver, Sender};
use socketioxide::extract::SocketRef;
use std::any::Any;
use std::fmt::Debug;
use std::{any::Any, sync::Arc, time::Instant};
use std::sync::Arc;
use std::time::Instant;
pub mod analog_input_test_machine;
pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
@@ -28,6 +30,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;
@@ -36,7 +39,6 @@ pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_MOCK: u16 = 0x0007;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
pub const MACHINE_WAGO_POWER_V1: u16 = 0x000A;
@@ -44,6 +46,7 @@ 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;
use serde_json::Value;
use smol::lock::RwLock;
@@ -276,6 +279,12 @@ pub trait MachineAct {
fn act(&mut self, now: Instant);
}
#[derive(Serialize, Debug, Clone)]
pub struct MachineValues {
pub state: serde_json::Value,
pub live_values: serde_json::Value,
}
// generic MachineMessage allows us to implement actions
// to manage or mutate machines with simple messages sent to the Recv Channel of the given Machine,
// which the machine itself will handle to avoid locking
@@ -287,6 +296,7 @@ pub enum MachineMessage {
HttpApiJsonRequest(serde_json::Value),
ConnectToMachine(MachineConnection),
DisconnectMachine(MachineConnection),
RequestValues(Sender<MachineValues>),
}
pub trait MachineApi {
@@ -447,6 +457,9 @@ where
}
pub trait MachineWithChannel: Send + Debug + Sync {
type State: serde::Serialize;
type LiveValues: serde::Serialize;
fn get_machine_channel(&self) -> &MachineChannel;
fn get_machine_channel_mut(&mut self) -> &mut MachineChannel;
@@ -454,6 +467,11 @@ pub trait MachineWithChannel: Send + Debug + Sync {
fn update(&mut self, now: std::time::Instant) -> Result<()>;
fn mutate(&mut self, value: Value) -> Result<()>;
fn get_state(&self) -> Self::State;
fn get_live_values(&self) -> Option<Self::LiveValues> {
None
}
}
impl<C> MachineApi for C
@@ -513,6 +531,17 @@ where
MachineMessage::DisconnectMachine(_machine_connection) => {
todo!();
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
}
+39
View File
@@ -41,6 +41,31 @@ impl MachineIdentification {
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
pub fn vendor_str(&self) -> String {
match self.vendor {
x if x == VENDOR_QITECH => "QiTech".to_string(),
_ => "N/A".to_string(),
}
}
pub fn slug(&self) -> String {
match self.machine {
x if x == MACHINE_WINDER_V1 => "winder_v1".to_string(),
x if x == MACHINE_EXTRUDER_V1 => "extruder_v1".to_string(),
x if x == MACHINE_LASER_V1 => "laser_v1".to_string(),
x if x == MACHINE_MOCK => "mock".to_string(),
x if x == MACHINE_AQUAPATH_V1 => "aquapath_v1".to_string(),
x if x == MACHINE_BUFFER_V1 => "buffer_v1".to_string(),
x if x == MACHINE_EXTRUDER_V2 => "extruder_v2".to_string(),
x if x == MACHINE_WAGO_POWER_V1 => "wago_power_v1".to_string(),
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"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -149,6 +174,20 @@ use ethercat_hal::helpers::ethercrab_types::{
use ethercrab::MainDevice;
use ethercrab::SubDeviceIdentity;
use crate::ANALOG_INPUT_TEST_MACHINE;
use crate::IP20_TEST_MACHINE;
use crate::MACHINE_AQUAPATH_V1;
use crate::MACHINE_BUFFER_V1;
use crate::MACHINE_EXTRUDER_V1;
use crate::MACHINE_EXTRUDER_V2;
use crate::MACHINE_LASER_V1;
use crate::MACHINE_MOCK;
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 {
pub vendor_word: u16,
+12 -1
View File
@@ -1,5 +1,5 @@
use super::MockMachine;
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
/// Implements the `MachineAct` trait for the `MockMachine`.
@@ -57,6 +57,17 @@ impl MachineAct for MockMachine {
{
()
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
}
+21 -15
View File
@@ -58,8 +58,7 @@ impl MockMachine {
vendor: VENDOR_QITECH,
};
/// Emit live values data event with the current sine wave amplitude
pub fn emit_live_values(&mut self) {
pub fn get_live_values(&self) -> LiveValuesEvent {
let now = Instant::now();
let elapsed = now.duration_since(self.t_0).as_secs_f64();
let freq1_hz = self.frequency1.get::<hertz>();
@@ -76,37 +75,44 @@ impl MockMachine {
let amplitude2 = (t * freq2_hz).sin();
let amplitude3 = (t * freq3_hz).sin();
let live_values = LiveValuesEvent {
LiveValuesEvent {
amplitude_sum: amplitude1 + amplitude2 + amplitude3,
amplitude1,
amplitude2,
amplitude3,
};
self.namespace
.emit(MockEvents::LiveValues(live_values.build()));
}
}
/// Emit the current state of the mock machine only if values have changed
pub fn emit_state(&mut self) {
/// Emit live values data event with the current sine wave amplitude
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(MockEvents::LiveValues(event));
}
pub fn get_state(&self) -> StateEvent {
info!(
"Emitting state for MockMachine, is default state: {}",
!self.emitted_default_state
);
let current_state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
StateEvent {
is_default_state: !self.emitted_default_state,
frequency1: self.frequency1.get::<millihertz>(),
frequency2: self.frequency2.get::<millihertz>(),
frequency3: self.frequency3.get::<millihertz>(),
mode_state: ModeState {
mode: self.mode.clone(),
},
};
}
}
self.namespace
.emit(MockEvents::State(current_state.build()));
self.last_emitted_event = Some(current_state);
/// Emit the current state of the mock machine only if values have changed
pub fn emit_state(&mut self) {
let state = self.get_state();
let event = state.build();
self.namespace.emit(MockEvents::State(event));
self.emitted_default_state = true;
self.last_emitted_event = Some(state);
}
/// Set the frequencies of the sine waves
+3
View File
@@ -1,5 +1,6 @@
use crate::analog_input_test_machine::AnalogInputTestMachine;
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 +129,8 @@ lazy_static! {
mc.register::<AnalogInputTestMachine>(AnalogInputTestMachine::MACHINE_IDENTIFICATION);
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
mc
};
}
+11 -1
View File
@@ -1,5 +1,5 @@
use super::TestMachine;
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for TestMachine {
@@ -31,6 +31,16 @@ impl MachineAct for TestMachine {
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();
}
}
}
}
+5 -3
View File
@@ -40,12 +40,14 @@ impl TestMachine {
}
impl TestMachine {
pub fn emit_state(&mut self) {
let event = StateEvent {
pub fn get_state(&self) -> StateEvent {
StateEvent {
led_on: self.led_on,
}
.build();
}
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(TestMachineEvents::State(event));
}
+71
View File
@@ -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();
}
}
}
+71
View File
@@ -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()
}
}
+81
View File
@@ -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(),
)));
}
}
+102
View File
@@ -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 &params.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)
})
}
}
+86 -60
View File
@@ -1,39 +1,45 @@
use crate::{MachineChannel, MachineWithChannel};
use crate::{
MACHINE_WAGO_POWER_V1, MachineChannel, MachineWithChannel, VENDOR_QITECH,
machine_identification::MachineIdentification,
};
use anyhow::Result;
use control_core::{
modbus::tcp::ModbusTcpDevice,
socketio::{
event::{BuildEvent, GenericEvent},
namespace::{
CacheFn, CacheableEvents, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
use control_core::socketio::{
event::{BuildEvent, GenericEvent},
namespace::{
CacheFn, CacheableEvents, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::*;
use smol::lock::Mutex;
use std::{
net::SocketAddr,
time::{Duration, Instant},
};
use units::{
electric_current::milliampere,
electric_potential::{millivolt, volt},
*,
};
use std::time::{Duration, Instant};
#[cfg(not(feature = "mock-machine"))]
mod imports {
pub use control_core::modbus::tcp::ModbusTcpDevice;
pub use smol::lock::Mutex;
pub use std::net::SocketAddr;
pub use units::{
electric_current::milliampere,
electric_potential::{millivolt, volt},
*,
};
}
#[cfg(not(feature = "mock-machine"))]
use imports::*;
const MODBUS_DC_OFF: u16 = 0;
const MODBUS_DC_ON: u16 = 1;
const MODBUS_HICCUP_POWER: u16 = 1 << 8;
#[derive(Serialize, Debug, Clone, BuildEvent)]
pub struct LiveValuesEvent {
pub struct LiveValues {
voltage: f64,
current: f64,
}
impl CacheableEvents<Self> for LiveValuesEvent {
impl CacheableEvents<Self> for LiveValues {
fn event_value(&self) -> GenericEvent {
self.build().into()
}
@@ -59,12 +65,12 @@ impl Mode {
}
#[derive(Serialize, Debug, Clone, BuildEvent)]
pub struct StateEvent {
pub struct State {
mode: Mode,
is_default_state: bool,
}
impl CacheableEvents<Self> for StateEvent {
impl CacheableEvents<Self> for State {
fn event_value(&self) -> GenericEvent {
self.build().into()
}
@@ -87,6 +93,7 @@ pub struct WagoPower {
device: Mutex<ModbusTcpDevice>,
last_emit: Instant,
emitted_default_state: bool,
last_live_values: Option<LiveValues>,
}
impl WagoPower {
@@ -101,44 +108,12 @@ impl WagoPower {
device: Mutex::new(ModbusTcpDevice::new(addr).await?),
last_emit: Instant::now(),
emitted_default_state: false,
})
}
#[cfg(feature = "mock-machine")]
fn get_live_values(&mut self) -> Result<LiveValuesEvent> {
match self.mode {
Mode::Off => Ok(LiveValuesEvent {
voltage: 0.0,
current: 0.0,
}),
Mode::On24V => Ok(LiveValuesEvent {
voltage: 24.0,
current: 5000.0,
}),
}
}
#[cfg(not(feature = "mock-machine"))]
fn get_live_values(&mut self) -> Result<LiveValuesEvent> {
let electric = smol::block_on(async {
let mut dev = self.device.lock().await;
dev.get_holding_registers(0x0500, 2).await
})?;
let voltage = ElectricPotential::new::<millivolt>(f64::from(electric[0]));
let current = ElectricCurrent::new::<milliampere>(f64::from(electric[1]));
Ok(LiveValuesEvent {
voltage: voltage.get::<volt>(),
current: current.get::<milliampere>(),
last_live_values: None,
})
}
fn emit_state(&mut self) {
let event = StateEvent {
mode: self.mode.clone(),
is_default_state: !self.emitted_default_state,
};
let event = self.get_state();
self.channel.emit(event);
}
@@ -180,9 +155,49 @@ impl WagoPower {
let mut dev = self.device.lock().await;
dev.get_u16(0x000B).await
}
#[cfg(feature = "mock-machine")]
fn read_live_values(&mut self) -> Result<LiveValues> {
match self.mode {
Mode::Off => Ok(LiveValues {
voltage: 0.0,
current: 0.0,
}),
Mode::On24V => Ok(LiveValues {
voltage: 24.0,
current: 5000.0,
}),
}
}
#[cfg(not(feature = "mock-machine"))]
fn read_live_values(&mut self) -> Result<LiveValues> {
let electric = smol::block_on(async {
let mut dev = self.device.lock().await;
dev.get_holding_registers(0x0500, 2).await
})?;
let voltage = ElectricPotential::new::<millivolt>(f64::from(electric[0]));
let current = ElectricCurrent::new::<milliampere>(f64::from(electric[1]));
Ok(LiveValues {
voltage: voltage.get::<volt>(),
current: current.get::<milliampere>(),
})
}
}
impl WagoPower {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WAGO_POWER_V1,
};
}
impl MachineWithChannel for WagoPower {
type State = State;
type LiveValues = LiveValues;
fn get_machine_channel(&self) -> &MachineChannel {
&self.channel
}
@@ -214,13 +229,24 @@ impl MachineWithChannel for WagoPower {
}
if now.duration_since(self.last_emit) > Duration::from_secs_f64(1.0 / 30.0) {
if let Ok(event) = self.get_live_values() {
self.channel.emit(event);
}
let live_values = self.read_live_values()?;
self.channel.emit(live_values.clone());
self.last_live_values = Some(live_values);
self.last_emit = now;
}
Ok(())
}
fn get_state(&self) -> Self::State {
State {
mode: self.mode.clone(),
is_default_state: !self.emitted_default_state,
}
}
fn get_live_values(&self) -> Option<Self::LiveValues> {
self.last_live_values.clone()
}
}
+12 -1
View File
@@ -1,7 +1,7 @@
#[cfg(not(feature = "mock-machine"))]
use super::Winder2;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineAct, MachineMessage};
use crate::{MachineAct, MachineMessage, MachineValues};
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
@@ -61,6 +61,17 @@ impl MachineAct for Winder2 {
MachineMessage::DisconnectMachine(_machine_connection) => {
self.connected_machines.clear();
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.build_state_event())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
}
+6 -4
View File
@@ -161,7 +161,7 @@ impl Winder2 {
self.emit_state();
}
pub fn emit_live_values(&mut self) {
pub fn get_live_values(&self) -> LiveValuesEvent {
let angle_deg = self.tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
@@ -192,7 +192,7 @@ impl Winder2 {
.get::<revolution_per_minute>()
.abs();
let live_values = LiveValuesEvent {
LiveValuesEvent {
traverse_position: self
.traverse_controller
.get_current_position()
@@ -201,9 +201,11 @@ impl Winder2 {
spool_rpm,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
};
}
}
let event = live_values.build();
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
+14
View File
@@ -1,6 +1,7 @@
use super::Winder2;
use crate::MachineAct;
use crate::MachineMessage;
use crate::MachineValues;
use std::time::{Duration, Instant};
impl MachineAct for Winder2 {
@@ -38,6 +39,19 @@ impl MachineAct for Winder2 {
{
()
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.build_state_event())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
()
}
}
}
}
+7 -6
View File
@@ -69,17 +69,18 @@ impl Winder2 {
}
pub fn emit_live_values(&mut self) {
let event = LiveValuesEvent {
let event = self.get_live_values().build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
traverse_position: Some(0.0),
puller_speed: 0.0,
spool_rpm: 0.0,
tension_arm_angle: 0.0,
spool_progress: 0.0,
};
let event = event.build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
}
pub fn build_state_event(&mut self) -> StateEvent {
+3
View File
@@ -336,6 +336,9 @@ impl Winder2 {
}
}
#[cfg(feature = "mock-machine")]
pub use mock::Winder2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Winder2Mode {
Standby,
+10 -10
View File
@@ -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;
+4 -6
View File
@@ -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
+1 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-gk/YC3+gc5DVvPS4ght9WmFcuc2FAX/UB+lIuQn436U=";
npmDepsHash = "sha256-UHPIoyMTzr7jkvbpZsePaNJ6s76h3R/0pcnbCkOcdYo=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
+22 -1
View File
@@ -3,6 +3,7 @@ use crate::rest::handlers::write_machine_device_identification::MachineDeviceInf
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::machines_event::{MachineObj, MachinesEventBuilder};
use crate::socketio::namespaces::Namespaces;
use anyhow::{Result, bail};
use control_core::socketio::event::GenericEvent;
use ethercat_hal::devices::EthercatDevice;
use ethercrab::SubDeviceRef;
@@ -123,11 +124,31 @@ impl EthercatSetup {
impl SharedState {
pub async fn send_machines_event(&self) {
let event = MachinesEventBuilder().build(self.current_machines_meta.lock().await.clone());
let event = MachinesEventBuilder().build(self.get_machines_meta().await);
let main_namespace = &mut self.socketio_setup.namespaces.write().await.main_namespace;
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
pub async fn get_machines_meta(&self) -> Vec<MachineObj> {
self.current_machines_meta.lock().await.clone()
}
pub async fn message_machine(
&self,
machine_identification_unique: &MachineIdentificationUnique,
message: MachineMessage,
) -> Result<()> {
let machines = self.api_machines.lock().await;
let sender = machines.get(machine_identification_unique);
if let Some(sender) = sender {
sender.send(message).await?;
return Ok(());
}
bail!("Unknown machine!")
}
/// Removes a machine by its unique identifier
pub async fn remove_machine(&self, machine_id: &MachineIdentificationUnique) {
let mut current_machines = self.current_machines_meta.lock().await;
+15
View File
@@ -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,
+5 -2
View File
@@ -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),
-2
View File
@@ -1,7 +1,5 @@
use crate::add_serial_device;
use crate::app_state::SharedState;
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
use machines::registry::MACHINE_REGISTRY;
use std::sync::Arc;
+14 -14
View File
@@ -1,14 +1,20 @@
use crate::app_state::SharedState;
use control_core::ethernet::modbus_tcp_discovery::probe_modbus_tcp;
use control_core::futures::FutureIteratorExt;
use machines::{
MACHINE_WAGO_POWER_V1, Machine, MachineChannel, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
Machine, MachineChannel, machine_identification::MachineIdentificationUnique,
wago_power::WagoPower,
};
use smol::Timer;
use std::sync::Arc;
use std::time::Duration;
#[cfg(not(feature = "mock-machine"))]
mod imports {
pub use control_core::ethernet::modbus_tcp_discovery::probe_modbus_tcp;
pub use control_core::futures::FutureIteratorExt;
pub use smol::Timer;
pub use std::time::Duration;
}
#[cfg(not(feature = "mock-machine"))]
use imports::*;
#[cfg(not(feature = "mock-machine"))]
pub async fn start_modbus_tcp_discovery(shared_state: Arc<SharedState>) {
@@ -25,10 +31,7 @@ pub async fn start_modbus_tcp_discovery(shared_state: Arc<SharedState>) {
.map(|probe| {
smol::spawn(async move {
let machine_identification_unique = MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WAGO_POWER_V1,
},
machine_identification: WagoPower::MACHINE_IDENTIFICATION,
serial: probe.serial,
};
@@ -51,10 +54,7 @@ pub async fn start_modbus_tcp_discovery(shared_state: Arc<SharedState>) {
#[cfg(feature = "mock-machine")]
pub async fn start_modbus_tcp_discovery(shared_state: Arc<SharedState>) {
let machine_identification_unique = MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WAGO_POWER_V1,
},
machine_identification: WagoPower::MACHINE_IDENTIFICATION,
serial: 0xbeef,
};
+3 -1
View File
@@ -9,6 +9,7 @@ use tracing::Level;
use super::handlers::machine_mutation::post_machine_mutate;
use super::handlers::write_machine_device_identification::post_write_machine_device_identification;
use crate::app_state::SharedState;
use crate::rest::rest_api::rest_api_router;
use crate::socketio::init::init_socketio;
use crate::rest::handlers::metrics::metrics_router;
@@ -29,6 +30,7 @@ async fn init_api(app_state: Arc<SharedState>) -> Result<()> {
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.nest("/api/v1/metrics", metrics_router())
.nest("/api/v2", rest_api_router())
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
@@ -53,7 +55,7 @@ pub fn start_api_thread(app_state: Arc<SharedState>) -> std::thread::JoinHandle<
.build()
.expect("Failed to create Tokio runtime");
if let Err(err) = rt.block_on(init_api(app_state.clone())) {
if let Err(err) = rt.block_on(init_api(app_state)) {
eprintln!("API server exited with error: {err:?}");
}
})
+2
View File
@@ -1,3 +1,5 @@
pub mod handlers;
pub mod init;
pub mod response;
pub mod rest_api;
pub mod util;
+53
View File
@@ -0,0 +1,53 @@
use axum::{Json, body::Body, http::StatusCode};
use serde_json::json;
pub enum ApiError {
ErrBadRequest(String),
ErrNotFound(String),
ErrInternal(String),
}
impl axum::response::IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let json = match self {
Self::ErrBadRequest(ref e) => serde_json::to_string(&json!({ "error_bad_request": e })),
Self::ErrNotFound(ref e) => serde_json::to_string(&json!({ "error_not_found": e })),
Self::ErrInternal(ref e) => serde_json::to_string(&json!({ "error_internal": e })),
};
let body = match json {
Ok(s) => Body::from(s),
Err(_) => Body::empty(),
};
let status = match self {
Self::ErrBadRequest(_) => StatusCode::BAD_REQUEST,
Self::ErrNotFound(_) => StatusCode::NOT_FOUND,
Self::ErrInternal(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
axum::response::Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(body)
.expect("Failed to build error response")
}
}
pub type Result<T> = axum::response::Result<Json<T>, ApiError>;
pub fn json<T>(t: T) -> Result<T> {
Result::Ok(Json(t))
}
pub fn bad_request<E: ToString>(e: E) -> ApiError {
ApiError::ErrBadRequest(e.to_string())
}
pub fn not_found<E: ToString>(e: E) -> ApiError {
ApiError::ErrNotFound(e.to_string())
}
pub fn internal_error<E: ToString>(e: E) -> ApiError {
ApiError::ErrInternal(e.to_string())
}
+166
View File
@@ -0,0 +1,166 @@
use std::sync::Arc;
use axum::extract::{Path, State};
use axum::routing::{get, post};
use axum::{Extension, Json, Router, debug_handler};
use machines::MachineMessage;
use machines::analog_input_test_machine::AnalogInputTestMachine;
use machines::aquapath1::AquaPathV1;
use machines::extruder1::ExtruderV2;
use machines::ip20_test_machine::IP20TestMachine;
use machines::laser::LaserMachine;
use machines::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use machines::mock::MockMachine;
use machines::test_machine::TestMachine;
use machines::wago_power::WagoPower;
use machines::winder2::Winder2;
use serde::Serialize;
use crate::app_state::SharedState;
use crate::rest::response::*;
#[derive(Serialize, Debug, PartialEq)]
struct MachineResponce {
legacy_id: MachineIdentificationUnique,
serial: u16,
vendor: String,
slug: String,
error: Option<String>,
}
impl From<MachineIdentificationUnique> for MachineResponce {
fn from(machine_identification_unique: MachineIdentificationUnique) -> Self {
let vendor = machine_identification_unique
.machine_identification
.vendor_str();
let slug = machine_identification_unique.machine_identification.slug();
let serial = machine_identification_unique.serial;
Self {
legacy_id: machine_identification_unique,
serial,
vendor,
slug,
error: None,
}
}
}
#[derive(Serialize, Debug, PartialEq)]
struct GetMachinesResponce {
machines: Vec<MachineResponce>,
}
#[debug_handler]
async fn get_machines_handler(
State(shared_state): State<Arc<SharedState>>,
) -> Result<GetMachinesResponce> {
let machines = shared_state
.get_machines_meta()
.await
.into_iter()
.map(|m| {
let vendor = m
.machine_identification_unique
.machine_identification
.vendor_str();
let slug = m
.machine_identification_unique
.machine_identification
.slug();
let serial = m.machine_identification_unique.serial;
MachineResponce {
legacy_id: m.machine_identification_unique,
serial,
vendor,
slug,
error: m.error,
}
})
.collect();
json(GetMachinesResponce { machines })
}
#[derive(Serialize, Debug, PartialEq)]
struct GetMachineResponce {
machine: MachineResponce,
state: serde_json::Value,
live_values: serde_json::Value,
}
#[debug_handler]
async fn get_machine_handler(
Extension(id): Extension<MachineIdentification>,
State(shared_state): State<Arc<SharedState>>,
Path(serial): Path<u16>,
) -> Result<GetMachineResponce> {
let id = MachineIdentificationUnique {
machine_identification: id,
serial,
};
let (sender, receiver) = smol::channel::bounded(1);
shared_state
.message_machine(&id, MachineMessage::RequestValues(sender))
.await
.map_err(not_found)?;
let values = receiver.recv().await.map_err(internal_error)?;
json(GetMachineResponce {
machine: MachineResponce::from(id),
state: values.state,
live_values: values.live_values,
})
}
type PostMachineRequest = Vec<serde_json::Value>;
#[debug_handler]
async fn post_machine_handler(
Extension(id): Extension<MachineIdentification>,
State(shared_state): State<Arc<SharedState>>,
Path(serial): Path<u16>,
Json(request): Json<PostMachineRequest>,
) -> Result<()> {
let id = MachineIdentificationUnique {
machine_identification: id,
serial,
};
for value in request {
shared_state
.message_machine(&id, MachineMessage::HttpApiJsonRequest(value))
.await
.map_err(not_found)?;
}
json(())
}
fn make_machine_router(id: MachineIdentification) -> Router<Arc<SharedState>> {
let slug = id.slug();
let path = format!("/machine/{slug}/{{serial}}");
Router::new()
.route(&path, get(get_machine_handler))
.route(&path, post(post_machine_handler))
.layer(Extension(id))
}
pub fn rest_api_router() -> Router<Arc<SharedState>> {
Router::new()
.route("/machine", get(get_machines_handler))
.merge(make_machine_router(LaserMachine::MACHINE_IDENTIFICATION))
.merge(make_machine_router(Winder2::MACHINE_IDENTIFICATION))
.merge(make_machine_router(MockMachine::MACHINE_IDENTIFICATION))
.merge(make_machine_router(ExtruderV2::MACHINE_IDENTIFICATION))
.merge(make_machine_router(AquaPathV1::MACHINE_IDENTIFICATION))
.merge(make_machine_router(TestMachine::MACHINE_IDENTIFICATION))
.merge(make_machine_router(WagoPower::MACHINE_IDENTIFICATION))
.merge(make_machine_router(IP20TestMachine::MACHINE_IDENTIFICATION))
.merge(make_machine_router(
AnalogInputTestMachine::MACHINE_IDENTIFICATION,
))
}
+31
View File
@@ -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(())
}