Compare commits

...
Author SHA1 Message Date
Oshgnacknak bb3f4e1503 More data 2026-02-04 14:52:52 +01:00
Robin Krämer 850b2d411d Merge pull request #1080 from qitechgmbh/changelog-2.14.1
updated Changelog
2026-01-26 16:37:03 +01:00
kraemr 008efe6ba1 updated Changelog 2026-01-26 16:36:09 +01:00
Robin KrämerandOshgnacknak 3bd248b9fe Don't use half sec interval (#1079)
Co-authored-by: Oshgnacknak <osh@oshgnacknak.de>
2026-01-26 16:29:11 +01:00
Oshgnacknak d73cfe7a02 Don't use half sec interval 2026-01-26 13:19:08 +01:00
dependabot[bot] 6d2279d44b Merge pull request #1062 from qitechgmbh/dependabot/npm_and_yarn/electron/seroval-1.4.2
Bump seroval from 1.4.0 to 1.4.2 in /electron
2026-01-25 17:00:04 +01:00
Oshgnacknak 728dd6eb26 Merge pull request #1066 from qitechgmbh/update_numpad_position
Numpad position now updating on resize
2026-01-23 16:10:00 +01:00
Hans Sebastian Pöhlmann 742fd35d5f Numpad position now updating on resize 2026-01-23 15:56:58 +01:00
Milan von dem Bussche 5e1126e896 Revise machine table in README.md
Updated the machine table format and added version details.
2026-01-23 12:38:14 +01:00
Milan von dem Bussche e91719f902 Update getting started documentation with new sections 2026-01-23 12:37:30 +01:00
Milan von dem Bussche 0efa9bcb07 Revise README structure and add documentation links
Updated README to enhance structure and clarity, added minimal hardware examples section, and included API documentation link.
2026-01-23 12:33:58 +01:00
Milan von dem Bussche 37d897d240 Revise machine table with new entries and videos
Updated machine specifications and added video links for various machines.
2026-01-23 11:38:36 +01:00
Robin Krämer 7488e1d97a updated Change Log (#1069) 2026-01-22 20:35:25 +01:00
Christian 0a87e45901 Merge pull request #1058 from qitechgmbh/1041-implement-wago-750-402-terminal
1041 implement wago 750 402 terminal
2026-01-22 13:43:37 +01:00
Renosch 6205189b14 Christians Request Resolved 2026-01-22 13:00:42 +01:00
Renosch 9d7da4f0af Resolved 2026-01-22 12:39:30 +01:00
Renosch 241b24be43 Resolved 2026-01-22 12:06:06 +01:00
Renosch aa77d12879 Documentation with Images 2026-01-22 12:05:16 +01:00
Renosch 78f183a8e5 Immplement Digital Input Machine WAGo 750-402 Final Product 2026-01-22 12:05:16 +01:00
Renosch 9f6fc8184e Working Digital Input Machine 2026-01-22 12:05:16 +01:00
Renosch ec3bdcb1df it now funktional! 2026-01-22 12:05:16 +01:00
Renosch 30292650bc finaly the DigitalInputTestMachines Tab is working 2026-01-22 12:05:16 +01:00
Renosch 1e9bcf9b19 alpha version Frontend and Backend 2026-01-22 12:04:49 +01:00
Robin Krämer 1e88979ced dnsmasq now only runs after ethercat is OP (#1063) 2026-01-21 18:46:06 +01:00
47 changed files with 1201 additions and 167 deletions

No files matched your search

+12 -1
View File
@@ -1,3 +1,15 @@
# `2.14.1`
_26.01.2026_
## Laser
-[#1079](https://github.com/qitechgmbh/control/pull/1079) Fixed Overflowing of buffer for Graphs causing crashes
# `2.14.0`
_22.01.2026_
## General
- [#1040](https://github.com/qitechgmbh/control/pull/1040) Added API for reading Machine Data
# `2.13.0` # `2.13.0`
_07.01.2026_ _07.01.2026_
@@ -5,7 +17,6 @@ _07.01.2026_
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected - [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO - [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page - [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
- []
## Extruder V3 ## Extruder V3
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3 - [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
+40 -98
View File
@@ -17,22 +17,22 @@ QiTech Control combines the modularity and reliability of standard EtherCAT term
# Repo Structure # Repo Structure
Frontend **Frontend**
- `/electron`: Frontend code for the control software built with React and Electron. - `/electron`: Frontend code for the control software built with React and Electron.
Backend **Backend**
- `/server`: Glue between Beckhoff and Electron. Implements machine logic. - `/server`: Glue between Beckhoff and Electron. Implements machine logic.
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols. - `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
- `/ethercat-hal-derive`: Macros for `ethercat-hal` - `/ethercat-hal-derive`: Macros for `ethercat-hal`
- `/control-core`: Core control logic for the server. - `/control-core`: Core control logic for the server.
Operating System **Operating System**
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens. - `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
Other **Other**
- `/docs`: Documentation for the project. - `/docs`: Documentation for the project.
@@ -54,115 +54,57 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
[Developer Documentation](./docs/developer-docs/) [Developer Documentation](./docs/developer-docs/)
## Backend [API Documentation](https://github.com/qitechgmbh/control/blob/master/docs/rest-api.md)
-> Backend
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/)) - Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
- `rust-analyzer` extension for VSCode - `rust-analyzer` extension for VSCode
- Set your interface in `server/src/ethercat/init.rs` like `en10` - Set your interface in `server/src/ethercat/init.rs` like `en10`
- Connect a Beckhoff EK1100 to your interface - Connect a Beckhoff EK1100 to your interface
- run `cd server && cargo run` to start the server (localhost:3001) - run `cd server && cargo run` to start the server (localhost:3001)
## Frontend -> Frontend
- nodejs and npm installed - nodejs and npm installed
- run `cd electron && npm i && npm run start` to start the frontend - run `cd electron && npm i && npm run start` to start the frontend
# Machines
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs | ## Minimal Hardware Examples
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
# Minimal Hardware Examples
For complete step-by-step tutorials on setting up your first hardware, including wiring diagrams and software configuration, see the [Getting Started Guide](./docs/developer-docs/getting-started.md#minimal-hardware-examples). For complete step-by-step tutorials on setting up your first hardware, including wiring diagrams and software configuration, see the [Getting Started Guide](./docs/developer-docs/getting-started.md#minimal-hardware-examples).
Available examples: | Example | Vendor | Hardware | Description | How-To Video | Docs |
- **LED Control with EL2004** - Digital output control (simplest setup) | :----------------- | :------- | :------------ | :-------------------------------------- | :----------- | :---------------------------- |
- **Analog Input with EL3021** - Reading analog current measurements | **Digital Input** | WAGO | 750-402 | 4-Channel Digital Input | [ ] | - |
| **Digital Output** | WAGO | 750-753 | 8-Channel Digital Output | [ ] | - |
| **Analog Input** | WAGO | 750-455 | 4-Channel Analog Input | [ ] | - |
| **Analog Output** | WAGO | 750-553 | 4-Channel Analog Output | [ ] | - |
| **Serial Comms** | WAGO | 750-652 | RS-485 Modbus Module | [ ] | - |
| **Stepper Drive** | WAGO | 750-67x | Stepper Controller | [ ] | - |
| **Power Supply** | WAGO | 2787-214 | Power Supply Unit | [ ] | - |
| **LED Control** | Beckhoff | EL2004 | Digital output control (simplest setup) | [ ] | [](./docs/hardware/el2004.md) |
| **Analog Input** | Beckhoff | EL3021 | Reading analog current measurements | [ ] | [](./docs/hardware/el3021.md) |
# QiTech Machines
# More Docs | Machine Name | Description | Machine ID | Implemented | Video | Docs |
| :---------------- | :-------------------------- | :--------- | :---------- | :-------------------------------------------------------------- | :------------------------------ |
| Winder V1 | Winding Filaments & Similar | 1 | Legacy | [Video](https://youtu.be/4aE4cFhioKA?si=Xdj_LVnFrAYWnLm6) | - |
| Winder V2 | Winding Filaments & Similar | 2 | Yes | [Video](https://youtu.be/f2kzh6kpQWE?si=HEYcLOaC9gWSp2Wo) | [](./docs/machines/winder-1.md) |
| XL Winder V1 | Large Scale Winder | 3 | Yes | [Video](https://youtu.be/ynI6ioWIQQY?si=4EDVKXbgIqrvGGtY) | - |
| Buffer V1 | Filament buffering system | 4 | In Progress | [Video](https://youtu.be/VR5mEdZDPA0?si=eip7dQXU4zaXK7Ev) | - |
| Extruder V1 | Single Screw Extruder | 5 | Legacy | [Video](https://youtu.be/gchoG-yGczI?si=bEM4daf5eOVaU5_2) | - |
| Extruder V2 | Single Screw Extruder | 6 | Yes | [Video](https://youtu.be/mexRYGDNWa4?si=h7JBz_XKMwKLPenG) | |
| Waterway V1 | Filament Water Cooling | 7 | In Progress | [Video](https://youtu.be/_T5z1J8bl_k?si=uOC1hIQ1EAP0cHIF) | |
| Laser V1 | Diameter Measuring Laser | 8 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | |
| 2-Axis-Laser V1 | Dual Axis Laser | 9 | Yes | [Video](https://youtu.be/WDM34lj4afM?si=MZzUKkHCrzH4P0aA) | - |
| Mock | Mock Machine for Testing | 10 | Yes | | - |
| Extruder V3 | Single Screw Extruder | 11 | Yes | [Video](https://youtu.be/ipHHuPzCvn4?si=hkZ1b93rVuwDxhFD) | |
| Mini Schredder V1 | mini plastic crusher | 12 | Legacy | [Video](https://youtu.be/m8NplNqdu2Q?si=x6zjDktJtpbSMSmu) | - |
| Pro Schredder V1 | large plastic crusher | 13 | Legacy | [Video](https://youtu.be/pSaVMqp06pU?si=y61enutRpxKscBm4) | - |
| Dryer V1 | polymer dryer | 14 | Reserved | [Video](https://youtu.be/6hdmUUAdZp0?si=eKOW1WlgkqTpnfdS) | - |
| Pelletizier V1 | Filament chopper | 15 | Reserved | | - |
- [x] [Architecture & Data Flow](./docs/architecture-overview.md) # Current Restructuring Efforts
- [x] Example Winder V2 [![](https://img.youtube.com/vi/UlbnSVIhfLI/maxresdefault.jpg)](https://www.youtube.com/live/UlbnSVIhfLI?si=ZTotC5B7gd87tUim)
*Video: Live stream of software dev meeting*
- [ ] Electron
- Folder Structure
- Routing with TanStack Router
- Design with Tailwind & Shadcn
- ...
- [ ] Interfacing with Electron/Server
- [ ] SocketIO
- Machine Namespace
- Main Namespace
- [ ] REST
- Machine Mutations
- Write Device Identification
- Server
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
- Maindevice
- Group
- Extracting Device Identifications
- Identifying Groups
- Validating Machines
- Run Control Loop
- [x] [Control Loop](./docs/control-loop.md)
- [x] [Machine/Device Identification](./docs/identification.md)
- [ ] Machines
- When to create a new Machine?
- Versioning
- Code sharing
- Creating/Validating a Machine
- Validation
- Configuration
- [ ] Machine Implementation Guide
- Link: How to create a Device
- Link: How to create an Actor
- Link: How to create a Machine
- API (SocketIO + REST)
- Creation/Validation Logic
- Optional/Mandatory Devices
- Validate Devices
- Business Logic
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
- Forward `act` in winder.
- [ ] Control Core
- [x] [Actors](./docs/actors.md)
- [ ] SocketIO
- Namespaces & Caching
- Joining leaving namespaces
- NamespaceId
- Caching
- Serverside Caching
- Clientside Caching
- [ ] REST
- [x] Ethercat HAL
- [x] [Devices](./docs/devices.md)
- [x] [Configuration (CoE)](./docs/coe.md)
- [x] [IO](./docs/io.md)
- [x] [PDO](./docs/pdo.md)
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
- [x] [NixOS Operating System](./docs/nixos/README.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

+80
View File
@@ -91,3 +91,83 @@ To get started with actual hardware, check out these step-by-step tutorials:
- **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements - **[Analog Input with EL3021](./minimal-example-el3021.md)** - Reading analog current measurements
These examples provide complete hardware wiring diagrams and software setup instructions. These examples provide complete hardware wiring diagrams and software setup instructions.
# More Docs
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
- [x] Example Winder V2
- [ ] Electron
- Folder Structure
- Routing with TanStack Router
- Design with Tailwind & Shadcn
- ...
- [ ] Interfacing with Electron/Server
- [ ] SocketIO
- Machine Namespace
- Main Namespace
- [ ] REST
- Machine Mutations
- Write Device Identification
- Server
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
- Maindevice
- Group
- Extracting Device Identifications
- Identifying Groups
- Validating Machines
- Run Control Loop
- [x] [Control Loop](./docs/control-loop.md)
- [x] [Machine/Device Identification](./docs/identification.md)
- [ ] Machines
- When to create a new Machine?
- Versioning
- Code sharing
- Creating/Validating a Machine
- Validation
- Configuration
- [ ] Machine Implementation Guide
- Link: How to create a Device
- Link: How to create an Actor
- Link: How to create a Machine
- API (SocketIO + REST)
- Creation/Validation Logic
- Optional/Mandatory Devices
- Validate Devices
- Business Logic
- Link: How to create Machine Abstraction (Like Traverse/Puller/...)
- Forward `act` in winder.
- [ ] Control Core
- [x] [Actors](./docs/actors.md)
- [ ] SocketIO
- Namespaces & Caching
- Joining leaving namespaces
- NamespaceId
- Caching
- Serverside Caching
- Clientside Caching
- [ ] REST
- [x] Ethercat HAL
- [x] [Devices](./docs/devices.md)
- [x] [Configuration (CoE)](./docs/coe.md)
- [x] [IO](./docs/io.md)
- [x] [PDO](./docs/pdo.md)
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
- [x] [NixOS Operating System](./docs/nixos/README.md)
@@ -0,0 +1,240 @@
# Minimal Example — LED Control on the WAGO750-402
A complete hardware + software walkthrough
---
## Table of Contents
1. [Introduction](#1-introduction)
2. [Requirements](#2-requirements)
3. [Hardware Setup](#3-hardware-setup)
4. [Software Setup](#4-software-setup)
5. [Demo](#5-demo)
6. [References](#6-references)
---
## 1. Introduction
The WAGO 750-402 Display Example is a minimal demonstation showing how to display the digital inputs on a WAGO 750-402 EtherCAT terminal using the QiTech machine framework.
It represents the simplest possible hardware interaction in the system:
Displaying the Digital Inputs using the QiTech Control Dashboard.
## 2. Requirements
### Hardware
- WAGO **2787-2144 EtherCAT**
- WAGO **750-354 EtherCAT Coupler**
- WAGO **750-602 EtherCAT Power Terminal**
- WAGO **750-402 EtherCAT Terminal** (4.channle digital input)
- WAGO **750-600 EtherCAT Endmodule**
- WAGO **852-1322 Ethernet-Switch**
- **24V DC Lab power supply**(AC/DC adapter)
- A **Linux PC** (Ubuntu/Debian recommended)
- Standard Ethernet cable
- Flat screwdriver
---
### Software
*(Installation steps in Section 4)*
- Rust tollchain
- Node-js + npm
- Git
- QiTech Control repository
- EtherCAT HAL *(include inside repo)*
## 3. Hardware Setup
### 3.1 Schematic
![](../assets/WAGO750_402_schematic.png)
---
#### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
### 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](../assets/wiring.png)
---
### 3.2.2 Wiring Steps (Used in This Example)
We supply power to the other devices by using the **750-602 EtherCAT Power Terminal**
Perform the following wiring on the Ethernet-Switch:
1. Red wire (+24V) → PWR+
2. Black wire (0V) → RPS-
#### **Figure 1 —**
<img src="../assets/WAGO750_402_switch.JPG" width="400">
---
### 3.3 WAGO Wiring the Coupler
Wire the **750-354 EtherCAT Coupler** to the power supply
1. red wire (+24V)
2. yellow (0V)
#### **Figure 2 —**
<img src="../assets/WAGO750_402_coupler.JPG" width="400">
---
### 3.4 750-602 EtherCAT Power Terminal Integration
Slide the **750-602 EtherCAT Power Terminal** onto the **right side of the 750-354 Coupler** until it locks.
Next We need to wire the EtherCat Power Terminal
Perform the next wiring Steps:
1. Green wire (Ground) → **Terminal4**
2. Red wire (+24V) from **750-602 EtherCAT Power Terminal** → **Terminal 2**
3. Black wire(0V) from **750-602 EtherCAT Power Terminal** → **Terminal 3**
4. Red wire (+24V) from **750-354 EtherCAT Coupler** → **Terminal 6**
5. Yellow wire (0V) from **750-354 EtherCAT Coupler** → **Terminal 7**
#### **Figure 3 — Wiring**
<img src="../assets/WAGO750_402_power_terminal.JPG" width="400">
---
## 3.5 750-402 EtherCAT Terminal Integration
Slide the **750-402 EtherCAT Terminal** onto to **right side of the 750-602 EtherCAT Power Terminal** until it locks.
Wire from the **24V DC Lab Adapter** → **Terminal 1**
#### **Figure 4 — Wiring**
<img src="../assets/WAGO750_402_terminal.JPG" width="400">
## 3.6 750-600 Final Assemly
Slide the **750-600 EtherCAT Endmodule** onto to **right side of the 7750-402 EtherCAT Terminal** until it locks.
No wiring is required
#### **Figure 5 — 750-402 + 750-600 Connected**
<img src="../assets/WAGO750_402_end_modul.JPG" width="400">
## 3.7 Power & Etherneet
### Power
Connect the 2787-2144 power supply to the power outlet
### Ehternet
Use a standard LAN cable to connect to your PC → **852-1322 Ethernet-Switch**
And another from **852-1322 Ethernet-Switch** → **750-354 EtherCAT Coupler**
#### **Figure 6 — Ethernet**
<img src="../assets/WAGO750_402_ethernet.JPG" width="400">
## 4. Software Setup
### 4.1 Installing on Ubuntu/Debian
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
### 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
Ensure the WAGO750-402 is connected.
### 4.3 Running the Frontend
```bash
cd electron
npm run start
```
This launches the QiTech Control dashboard.
## 5. Demo
### 5.1 Assigning Devices in the Dashboard
Once the backend + frontend are running, you should see:
- WAGO750-354 (Coupler)
- wago module (Digital Input Terminal)
![](../assets/WAGO750_402_discovery.png)
Steps:
- Click Assign on the wago module
- Select Digital Input Machine V1
![](../assets/WAGO750_402_setmachine.png)
- Enter a serial number except for 0
![](../assets/WAGO750_402_serserial.png)
- Click Write
### 5.2 Diyplaying LED
Navigate to:
Machines → DigitalInputMachine
![](../assets/WAGO750_402_machinedetected.png)
You will see this interface:
![](../assets/WAGO750_402_machinecontrol1.png)
Now you can apply a Voltage of the Range of 15V-30V (+24V recommended) from the DC lab power supply to the Terminal.
The Terminal can read the digital input, which is signaled by the LED on the Terminal and should be displayed in the Control software.
In our example it lights up the first LED
![](../assets/WAGO750_402_machinecontrol2.png)
## 6. References
This guide incorporates information from official WAGO documentation.
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
Referenced Manuals
[WAGO 750-402 Documentation](https://www.wago.com/de/io-systeme/4-kanal-digitaleingang/p/750-402#downloads)
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/feldbuskoppler-ethercat/p/750-354#details)
[WAGO 750-602 Documentation](https://www.wago.com/de/io-systeme/potentialeinspeisung/p/750-602#details)
[WAGO 750-354 Documentation](https://www.wago.com/de/io-systeme/endmodul/p/750-600#details)
+18 -39
View File
@@ -162,7 +162,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -659,7 +658,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -683,7 +681,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -3283,7 +3280,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.1.tgz",
"integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==", "integrity": "sha512-pLQ6ZFCh5s86ewZIAu2wQc2svf+DqttD7CFd1NPSxdEU20KvMMj9RPQdDHj/pbf2VvB3i1nEDxjli/Z0PBvnCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@tanstack/history": "1.141.0", "@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0", "@tanstack/react-store": "^0.8.0",
@@ -3374,7 +3370,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.141.1.tgz",
"integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==", "integrity": "sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@tanstack/history": "1.141.0", "@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0", "@tanstack/store": "^0.8.0",
@@ -3595,7 +3590,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
@@ -3761,7 +3757,6 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -3778,7 +3773,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -3789,7 +3783,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -3877,7 +3870,6 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0", "@typescript-eslint/types": "8.49.0",
@@ -4236,7 +4228,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -4300,6 +4291,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -4614,7 +4606,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -4997,8 +4988,7 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/data-urls": { "node_modules/data-urls": {
"version": "5.0.0", "version": "5.0.0",
@@ -5271,7 +5261,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/dom-helpers": { "node_modules/dom-helpers": {
"version": "5.2.1", "version": "5.2.1",
@@ -5817,7 +5808,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -5878,7 +5868,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -5980,7 +5969,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -6939,7 +6927,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4" "@babel/runtime": "^7.28.4"
}, },
@@ -6973,7 +6960,6 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
@@ -7576,7 +7562,6 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssstyle": "^4.2.1", "cssstyle": "^4.2.1",
"data-urls": "^5.0.0", "data-urls": "^5.0.0",
@@ -8110,6 +8095,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"lz-string": "bin/bin.js" "lz-string": "bin/bin.js"
} }
@@ -9529,7 +9515,6 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -9646,6 +9631,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1", "ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0", "ansi-styles": "^5.0.0",
@@ -9661,6 +9647,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -9750,7 +9737,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -9760,7 +9746,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -9773,7 +9758,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -9816,7 +9800,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/react-lifecycles-compat": { "node_modules/react-lifecycles-compat": {
"version": "3.0.4", "version": "3.0.4",
@@ -10326,11 +10311,10 @@
} }
}, },
"node_modules/seroval": { "node_modules/seroval": {
"version": "1.4.0", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
"integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==", "integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@@ -10585,6 +10569,7 @@
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.1.0", "csstype": "^3.1.0",
"seroval": "~1.3.0", "seroval": "~1.3.0",
@@ -10606,6 +10591,7 @@
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz",
"integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -10930,8 +10916,7 @@
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
"version": "1.0.7", "version": "1.0.7",
@@ -11020,7 +11005,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -11284,7 +11268,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -11621,7 +11604,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -11734,8 +11716,7 @@
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
@@ -11773,7 +11754,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -12208,7 +12188,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -0,0 +1,31 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { Label } from "@/control/Label";
import { useDigitalInputTestMachine } from "./useDigitalInputTestMachine";
import { Badge } from "@/components/ui/badge";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroup } from "@/control/SelectionGroup";
import { toastError } from "@/components/Toast";
export function DigitalInputTestMachineControlPage() {
const { state } = useDigitalInputTestMachine();
const safeState = state ?? { led_on: [false, false, false, false] };
return (
<Page>
<ControlCard title="Machine LEDs">
<div className="grid grid-cols-2 gap-6">
{safeState.led_on.map((led, index) => (
<Label key={index} label={`LED ${index + 1}`}>
<Badge variant={led ? "outline" : "destructive"}>
{led ? "On" : "OFF"}
</Badge>
</Label>
))}
</div>
</ControlCard>
</Page>
);
}
@@ -0,0 +1,20 @@
import { Topbar } from "@/components/Topbar";
import { digitalInputTestMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function DigitalInputTestMachinePage() {
const { serial } = digitalInputTestMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/digitalInputTestMachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,78 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
led_on: z.array(z.boolean()).length(4),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type DigitalTestMachineNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineNamespaceStore =
(): StoreApi<DigitalTestMachineNamespaceStore> =>
create<DigitalTestMachineNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function digitalInputTestMachineMessageHandler(
store: StoreApi<DigitalTestMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<DigitalTestMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (
state: DigitalTestMachineNamespaceStore,
) => DigitalTestMachineNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useDigitalInputTestMachineNamespaceImplementation =
createNamespaceHookImplementation<DigitalTestMachineNamespaceStore>({
createStore: createTestMachineNamespaceStore,
createEventHandler: digitalInputTestMachineMessageHandler,
});
export function useDigitalInputTestMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): DigitalTestMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useDigitalInputTestMachineNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,53 @@
import { Toast, toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { digitalInputTestMachineSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import {
useDigitalInputTestMachineNamespace,
StateEvent,
} from "./digitalInputTestMachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { digitalInputTestMachine } from "@/machines/properties";
import { z } from "zod";
export function useDigitalInputTestMachine() {
const { serial: serialString } =
digitalInputTestMachineSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: digitalInputTestMachine.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useDigitalInputTestMachineNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
return {
state: stateOptimistic.value,
};
}
@@ -66,19 +66,14 @@ export type Laser1NamespaceStore = {
roundness: TimeSeries; roundness: TimeSeries;
}; };
// Constants for time durations const { initialTimeSeries: diameter, insert: addDiameter } =
const HALF_SECOND = 500; createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries({
sampleIntervalLong: HALF_SECOND,
});
const { initialTimeSeries: x_diameter, insert: addXDiameter } = const { initialTimeSeries: x_diameter, insert: addXDiameter } =
createTimeSeries({ sampleIntervalLong: HALF_SECOND }); createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: y_diameter, insert: addYDiameter } = const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries({ sampleIntervalLong: HALF_SECOND }); createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries( const { initialTimeSeries: roundness, insert: addRoundness } =
{ sampleIntervalLong: HALF_SECOND }, createTimeSeries({ sampleIntervalLong: 500 });
);
/** /**
* Factory function to create a new Laser1 namespace store * Factory function to create a new Laser1 namespace store
@@ -78,7 +78,7 @@ export type Mock1NamespaceStore = {
sineWave3: TimeSeries; sineWave3: TimeSeries;
}; };
const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries(); const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries({ retentionDurationShort: 300, sampleIntervalLong: 500 });
/** /**
* Factory function to create a new Mock1 namespace store * Factory function to create a new Mock1 namespace store
+12
View File
@@ -462,6 +462,17 @@ export const testmachine: MachineProperties = {
}, },
], ],
}; };
export const digitalInputTestMachine: MachineProperties = {
name: "Digital Input Machine",
version: "V1",
slug: "digitalInputTestMachine",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0040,
},
device_roles: [],
};
export const wagoPower1: MachineProperties = { export const wagoPower1: MachineProperties = {
name: "WAGO Power", name: "WAGO Power",
version: "V1", version: "V1",
@@ -569,6 +580,7 @@ export const machineProperties: MachineProperties[] = [
testmachine, testmachine,
analogInputTestMachine, analogInputTestMachine,
wagoAiTestMachine, wagoAiTestMachine,
digitalInputTestMachine,
ip20TestMachine, ip20TestMachine,
]; ];
@@ -215,14 +215,15 @@ export type Winder2NamespaceStore = {
//Store Factory and Message Handler -> no param, so default values //Store Factory and Message Handler -> no param, so default values
const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } = const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } =
createTimeSeries(); createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: traversePosition, insert: addTraversePosition } = const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries(); createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } = const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
createTimeSeries(); createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(); const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } =
createTimeSeries({ sampleIntervalLong: 500 });
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } = const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries(); createTimeSeries({ sampleIntervalLong: 500 });
/** /**
* Factory function to create a new Winder2 namespace store * Factory function to create a new Winder2 namespace store
+20
View File
@@ -61,6 +61,10 @@ import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/Analog
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage"; import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage"; import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
import { WagoAiTestMachineControl } from "@/machines/wagoaitestmachine/WagoAiTestMachineControlPage"; import { WagoAiTestMachineControl } from "@/machines/wagoaitestmachine/WagoAiTestMachineControlPage";
import { DigitalInputTestMachinePage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachinePage";
import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage"; import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage"; import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
@@ -114,6 +118,18 @@ export const wagoAiTestMachineControlRoute = createRoute({
component: () => <WagoAiTestMachineControl />, component: () => <WagoAiTestMachineControl />,
}); });
export const digitalInputTestMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "digitalInputTestMachine/$serial",
component: () => <DigitalInputTestMachinePage />,
});
export const digitalInputTestMachineControlRoute = createRoute({
getParentRoute: () => digitalInputTestMachineSerialRoute,
path: "control",
component: () => <DigitalInputTestMachineControlPage />,
});
export const ip20TestMachineSerialRoute = createRoute({ export const ip20TestMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute, getParentRoute: () => machinesRoute,
path: "ip20testmachine/$serial", path: "ip20testmachine/$serial",
@@ -462,6 +478,10 @@ export const rootTree = RootRoute.addChildren([
wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]), wagoAiTestMachineSerialRoute.addChildren([wagoAiTestMachineControlRoute]),
digitalInputTestMachineSerialRoute.addChildren([
digitalInputTestMachineControlRoute,
]),
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]), ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
aquapath1SerialRoute.addChildren([ aquapath1SerialRoute.addChildren([
+25 -7
View File
@@ -15,7 +15,13 @@ import {
machineProperties, machineProperties,
VENDOR_QITECH, VENDOR_QITECH,
} from "@/machines/properties"; } from "@/machines/properties";
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { z } from "zod"; import { z } from "zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -150,6 +156,15 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
}); });
}; };
const updateNumpadPosition = useCallback(() => {
if (!numpadOpen || !dialogRef.current) return;
const rect = dialogRef.current.getBoundingClientRect();
setNumpadPosition({
left: rect.right + 20,
top: rect.top + rect.height / 2,
});
}, [numpadOpen]);
const machinePreset = useMemo(() => { const machinePreset = useMemo(() => {
if (!values.machine) return; if (!values.machine) return;
return getMachineProperties({ return getMachineProperties({
@@ -187,12 +202,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
// Position numpad once when it opens // Position numpad once when it opens
useEffect(() => { useEffect(() => {
if (!numpadOpen || !dialogRef.current) return; updateNumpadPosition();
const rect = dialogRef.current.getBoundingClientRect();
setNumpadPosition({
left: rect.right + 20,
top: rect.top + rect.height / 2,
});
}, [numpadOpen]); }, [numpadOpen]);
// Close numpad when clicking outside input/numpad // Close numpad when clicking outside input/numpad
@@ -226,6 +236,14 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
} }
}, [numpadOpen]); }, [numpadOpen]);
// Update numpad position whenever the window resizes
useEffect(() => {
window.addEventListener("resize", updateNumpadPosition);
return () => {
window.removeEventListener("resize", updateNumpadPosition);
};
}, [updateNumpadPosition]);
// Numpad handlers for serial input // Numpad handlers for serial input
const numpadHandlers = useMemo(() => { const numpadHandlers = useMemo(() => {
const ensureFocus = () => { const ensureFocus = () => {
+8
View File
@@ -7,6 +7,7 @@ use crate::{
devices::{ devices::{
DynamicEthercatDevice, Module, DynamicEthercatDevice, Module,
wago_modules::{ wago_modules::{
wago_750_402::{WAGO_750_402_MODULE_IDENT, WAGO_750_402_PRODUCT_ID},
wago_750_455::{WAGO_750_455_MODULE_IDENT, WAGO_750_455_PRODUCT_ID}, wago_750_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_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_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
@@ -245,6 +246,10 @@ impl Wago750_354 {
module.has_tx = true; module.has_tx = true;
module.has_rx = true; module.has_rx = true;
} }
WAGO_750_402_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = false;
}
_ => println!( _ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}", "Wago-750-354 found Unknown/Unimplemented Module: {}",
ident_iom ident_iom
@@ -294,6 +299,9 @@ impl Wago750_354 {
WAGO_750_652_MODULE_IDENT => { WAGO_750_652_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_652::Wago750_652::new())) Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
} }
WAGO_750_402_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
}
_ => { _ => {
println!( println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !", "{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
@@ -430,6 +430,9 @@ impl IP20EcDi8Do8 {
wago_750_652::WAGO_750_652_MODULE_IDENT => { wago_750_652::WAGO_750_652_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_652::Wago750_652::new())) Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
} }
wago_750_402::WAGO_750_402_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_402::Wago750_402::new()))
}
_ => { _ => {
return; return;
} }
@@ -1,5 +1,6 @@
pub mod ip20_ec_di8_do8; pub mod ip20_ec_di8_do8;
pub mod wago_750_1506; pub mod wago_750_1506;
pub mod wago_750_402;
pub mod wago_750_455; pub mod wago_750_455;
pub mod wago_750_501; pub mod wago_750_501;
pub mod wago_750_530; pub mod wago_750_530;
@@ -0,0 +1,194 @@
use crate::{
devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
},
io::digital_input::{DigitalInputDevice, DigitalInputInput},
};
#[derive(Debug, Clone)]
pub enum Wago750_402InputPort {
DI1,
DI2,
DI3,
DI4,
}
impl From<Wago750_402InputPort> for usize {
fn from(value: Wago750_402InputPort) -> Self {
match value {
Wago750_402InputPort::DI1 => 0,
Wago750_402InputPort::DI2 => 1,
Wago750_402InputPort::DI3 => 2,
Wago750_402InputPort::DI4 => 3,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_402TxPdo {
port1: bool,
port2: bool,
port3: bool,
port4: bool,
}
impl DigitalInputDevice<Wago750_402InputPort> for Wago750_402 {
fn get_input(&self, port: Wago750_402InputPort) -> Result<DigitalInputInput, anyhow::Error> {
Ok(DigitalInputInput {
value: match port {
Wago750_402InputPort::DI1 => self.tx_pdo.port1,
Wago750_402InputPort::DI2 => self.tx_pdo.port2,
Wago750_402InputPort::DI3 => self.tx_pdo.port3,
Wago750_402InputPort::DI4 => self.tx_pdo.port4,
},
})
}
}
#[derive(Clone)]
pub struct Wago750_402 {
is_used: bool,
tx_bit_offset: usize,
// Should always be on but not when calling a constructor
module: Option<Module>,
tx_pdo: Wago750_402TxPdo,
}
impl DynamicEthercatDevice for Wago750_402 {}
impl EthercatDynamicPDO for Wago750_402 {
fn get_tx_offset(&self) -> usize {
self.tx_bit_offset
}
fn get_rx_offset(&self) -> usize {
0 //this device has no rx
}
fn set_tx_offset(&mut self, offset: usize) {
self.tx_bit_offset = offset
}
fn set_rx_offset(&mut self, _offset: usize) {
// it does nothing because the device has no rx
}
}
impl EthercatDeviceUsed for Wago750_402 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl Wago750_402 {}
impl EthercatDevice for Wago750_402 {
// input_len 48 offset: 40
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
let base = self.tx_bit_offset;
let idx1 = base + Into::<usize>::into(Wago750_402InputPort::DI1);
self.tx_pdo.port1 = *input.get(idx1).expect("Bit 1 out of bounds");
let idx2 = base + Into::<usize>::into(Wago750_402InputPort::DI2);
self.tx_pdo.port2 = *input.get(idx2).expect("Bit 2 out of bounds");
let idx3 = base + Into::<usize>::into(Wago750_402InputPort::DI3);
self.tx_pdo.port3 = *input.get(idx3).expect("Bit 3 out of bounds");
let idx4 = base + Into::<usize>::into(Wago750_402InputPort::DI4);
self.tx_pdo.port4 = *input.get(idx4).expect("Bit 4 out of bounds");
Ok(())
}
fn input_len(&self) -> usize {
4
}
fn output(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
// println!("output: {} {}",self.rx_bit_offset,_output);
Ok(())
}
fn output_len(&self) -> usize {
0
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn is_module(&self) -> bool {
true
}
fn input_checked(
&mut self,
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_checked(
&self,
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
Ok(())
}
fn get_module(&self) -> Option<Module> {
self.module.clone()
}
fn set_module(&mut self, module: Module) {
self.tx_bit_offset = module.tx_offset;
self.module = Some(module);
}
}
impl EthercatDeviceProcessing for Wago750_402 {
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl NewEthercatDevice for Wago750_402 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
module: None,
tx_pdo: Wago750_402TxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_402 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_402")
}
}
pub const WAGO_750_402_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_402_PRODUCT_ID: u32 = 2147483713;
pub const WAGO_750_402_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_402_VENDOR_ID, WAGO_750_402_PRODUCT_ID);
@@ -0,0 +1,46 @@
use super::DigitalInputTestMachine;
use crate::{MachineAct, MachineMessage, MachineValues};
use std::time::{Duration, Instant};
impl MachineAct for DigitalInputTestMachine {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::DisconnectMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::Value::Null,
})
.expect("Failed to send values");
sender.close();
}
}
}
}
@@ -0,0 +1,77 @@
use std::sync::Arc;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, digital_input_test_machine::DigitalInputTestMachine};
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub led_on: [bool; 4],
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum DigitalInputTestMachineEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetLed { index: usize, on: bool },
}
#[derive(Debug, Clone)]
pub struct DigitalInputTestMachineNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<DigitalInputTestMachineEvents> for DigitalInputTestMachineNamespace {
fn emit(&mut self, events: DigitalInputTestMachineEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<DigitalInputTestMachineEvents> for DigitalInputTestMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
DigitalInputTestMachineEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for DigitalInputTestMachine {
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, _value: serde_json::Value) -> Result<(), anyhow::Error> {
//Digital Input Test Machine does not Set Values
// let mutation: Mutation = serde_json::from_value(value)?;
// match mutation {
// Mutation::SetLed { index, on } => self.set_led(index, on),
// }
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -0,0 +1,73 @@
use std::time::Instant;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_input::DigitalInput;
use smol::channel::{Receiver, Sender};
use crate::{
AsyncThreadMessage, DIGITAL_INPUT_TEST_MACHINE, Machine, MachineMessage, VENDOR_QITECH,
digital_input_test_machine::api::{
DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
pub struct DigitalInputTestMachine {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub namespace: DigitalInputTestMachineNamespace,
pub last_state_emit: Instant,
pub led_on: [bool; 4],
pub digital_input: [DigitalInput; 4],
}
impl Machine for DigitalInputTestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl DigitalInputTestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: DIGITAL_INPUT_TEST_MACHINE,
};
pub fn get_state(&self) -> StateEvent {
StateEvent {
led_on: self.led_on,
}
}
pub fn emit_state(&mut self) {
//Set Led_On after DigitalInput
let mut i = 0;
for di in &self.digital_input {
let value = match di.get_value() {
Ok(v) => v,
Err(_) => false,
};
self.led_on[i] = value;
i += 1;
}
let event = StateEvent {
led_on: self.led_on,
}
.build();
self.namespace
.emit(DigitalInputTestMachineEvents::State(event));
}
}
@@ -0,0 +1,87 @@
use std::{sync::Arc, time::Instant};
use anyhow::Error;
use ethercat_hal::{
devices::{
EthercatDevice, downcast_device,
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
wago_modules::wago_750_402::{Wago750_402, Wago750_402InputPort},
},
io::digital_input::DigitalInput,
};
use smol::{block_on, lock::RwLock};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
digital_input_test_machine::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace},
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
};
impl MachineNewTrait for DigitalInputTestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::EtherCATMachine/TestMachine::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
// Example usage of a Wago Coupler and a 750-402 in the first slot, where the Output Port 1,2,3,4 is used
let _wago_750_354 = get_ethercat_device::<Wago750_354>(
hardware,
params,
0,
[WAGO_750_354_IDENTITY_A].to_vec(),
)
.await?;
let modules = Wago750_354::initialize_modules(_wago_750_354.1).await?;
let mut coupler = _wago_750_354.0.write().await;
for module in modules {
coupler.set_module(module);
}
coupler.init_slot_modules(_wago_750_354.1);
let dev = coupler.slot_devices.get(0).unwrap().clone().unwrap();
let wago750_402: Arc<RwLock<Wago750_402>> = downcast_device::<Wago750_402>(dev).await?;
let di1 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI1);
let di2 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI2);
let di3 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI3);
let di4 = DigitalInput::new(wago750_402.clone(), Wago750_402InputPort::DI4);
drop(coupler);
let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: DigitalInputTestMachineNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
led_on: [false; 4],
main_sender: params.main_thread_channel.clone(),
digital_input: [di1, di2, di3, di4],
};
my_test.emit_state();
Ok(my_test)
})
}
}
+2
View File
@@ -21,6 +21,7 @@ pub mod analog_input_test_machine;
pub mod aquapath1; pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))] #[cfg(not(feature = "mock-machine"))]
pub mod buffer1; pub mod buffer1;
pub mod digital_input_test_machine;
pub mod extruder1; pub mod extruder1;
pub mod extruder2; pub mod extruder2;
pub mod ip20_test_machine; pub mod ip20_test_machine;
@@ -47,6 +48,7 @@ pub const TEST_MACHINE: u16 = 0x0033;
pub const IP20_TEST_MACHINE: u16 = 0x0034; pub const IP20_TEST_MACHINE: u16 = 0x0034;
pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035; pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036; pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
use serde_json::Value; use serde_json::Value;
use smol::lock::RwLock; use smol::lock::RwLock;
+1 -1
View File
@@ -29,7 +29,7 @@ impl MachineAct for MockMachine {
}; };
// Only emit live values if machine is in Running mode // Only emit live values if machine is in Running mode
// The live values are updated approximately 30 times per second // The live values are updated approximately 30 times per second
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) { if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 60.0) {
self.emit_live_values(); self.emit_live_values();
self.last_measurement_emit = now; self.last_measurement_emit = now;
} }
+3
View File
@@ -1,4 +1,5 @@
use crate::analog_input_test_machine::AnalogInputTestMachine; use crate::analog_input_test_machine::AnalogInputTestMachine;
use crate::digital_input_test_machine::DigitalInputTestMachine;
use crate::ip20_test_machine::IP20TestMachine; use crate::ip20_test_machine::IP20TestMachine;
use crate::wago_ai_test_machine::WagoAiTestMachine; use crate::wago_ai_test_machine::WagoAiTestMachine;
#[cfg(feature = "mock-machine")] #[cfg(feature = "mock-machine")]
@@ -131,6 +132,8 @@ lazy_static! {
mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION); mc.register::<WagoAiTestMachine>(WagoAiTestMachine::MACHINE_IDENTIFICATION);
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
mc mc
}; };
} }
-1
View File
@@ -78,7 +78,6 @@ impl MachineNewTrait for TestMachine {
let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2); let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3); let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4); let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let (sender, receiver) = smol::channel::unbounded(); let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self { let mut my_test = Self {
api_receiver: receiver, api_receiver: receiver,
+10
View File
@@ -126,6 +126,16 @@ in {
value = "-20"; value = "-20";
} }
]; ];
# 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 # Desktop integration
xdg.mime.enable = true; xdg.mime.enable = true;
+2
View File
@@ -322,10 +322,12 @@ in {
services.dnsmasq = { services.dnsmasq = {
enable = true; enable = true;
resolveLocalQueries = false;
settings = { settings = {
interface = "enp1s0"; # only this interface interface = "enp1s0"; # only this interface
bind-interfaces = true; bind-interfaces = true;
server = [ "1.1.1.1" "8.8.8.8" ];
# DHCP subnet + pool # DHCP subnet + pool
dhcp-range = "10.10.10.50,10.10.10.250,255.255.255.0,12h"; dhcp-range = "10.10.10.50,10.10.10.250,255.255.255.0,12h";
+1 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1; ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true; makeCacheWritable = true;
npmDepsHash = "sha256-UHPIoyMTzr7jkvbpZsePaNJ6s76h3R/0pcnbCkOcdYo="; npmDepsHash = "sha256-twp+30+hPKoMUojuGyJI1R6dknWWx9i5waqFw2FlHPc=";
npmFlags = [ "--no-audit" "--no-fund" ]; npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = '' installPhase = ''
+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, 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::std::ethercat_now;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts}; use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use machines::machine_identification::{ use machines::machine_identification::{
@@ -172,6 +173,14 @@ pub async fn setup_loop(
) -> Result<EthercatSetup, anyhow::Error> { ) -> Result<EthercatSetup, anyhow::Error> {
tracing::info!("Starting Ethercat PDU loop"); 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 // Setup ethercrab tx/rx task
let pdu_storage = Box::leak(Box::new(PduStorage::<MAX_FRAMES, MAX_PDU_DATA>::new())); 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"); 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)); 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 { Ok(EthercatSetup {
devices, devices,
group: group_op, group: group_op,
+5 -2
View File
@@ -16,6 +16,7 @@ use machines::{
}; };
#[cfg(feature = "development-build")] #[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use utils::start_dnsmasq;
use app_state::{HotThreadMessage, SharedState}; use app_state::{HotThreadMessage, SharedState};
use ethercat::ethercat_discovery_info::send_ethercat_discovering; use ethercat::ethercat_discovery_info::send_ethercat_discovering;
@@ -57,6 +58,7 @@ pub mod panic;
pub mod performance_metrics; pub mod performance_metrics;
pub mod rest; pub mod rest;
pub mod socketio; pub mod socketio;
pub mod utils;
pub async fn send_empty_machines_event(shared_state: Arc<SharedState>) { pub async fn send_empty_machines_event(shared_state: Arc<SharedState>) {
shared_state.current_machines_meta.lock().await.clear(); shared_state.current_machines_meta.lock().await.clear();
@@ -137,7 +139,6 @@ pub async fn start_interface_discovery(
let interface = find_ethercat_interface().await; let interface = find_ethercat_interface().await;
tracing::info!("Inferface found {}, setting up EtherCAT loop", interface); tracing::info!("Inferface found {}, setting up EtherCAT loop", interface);
set_ethercat_iface(interface.clone()); set_ethercat_iface(interface.clone());
let res = setup_loop(&interface, app_state.clone()).await; let res = setup_loop(&interface, app_state.clone()).await;
match res { match res {
@@ -152,9 +153,10 @@ pub async fn start_interface_discovery(
module_path!(), module_path!(),
e e
); );
// if this doesnt work, unlucky
let _ = start_dnsmasq();
} }
} }
send_ethercat_found(app_state.clone(), &interface).await; send_ethercat_found(app_state.clone(), &interface).await;
} }
@@ -338,6 +340,7 @@ fn main() {
let app_state = Arc::new(shared_state); let app_state = Arc::new(shared_state);
let _loop_thread = start_loop_thread(receiver, CYCLE_TARGET_TIME); let _loop_thread = start_loop_thread(receiver, CYCLE_TARGET_TIME);
let _ = start_api_thread(app_state.clone()); let _ = start_api_thread(app_state.clone());
spawn_runtime_metrics_sampler(RuntimeMetricsConfig { spawn_runtime_metrics_sampler(RuntimeMetricsConfig {
csv_path: "runtime_metrics.csv".to_string(), csv_path: "runtime_metrics.csv".to_string(),
interval: Duration::from_secs(1), interval: Duration::from_secs(1),
+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(())
}