Compare commits

..
Author SHA1 Message Date
Robin Krämer f6c0744efa Merge pull request #937 from qitechgmbh/860-wago-coupler-750-354-cant-enter-op
860 wago coupler 750 354 cant enter op
2025-12-16 10:45:25 +01:00
rob 8c7e810359 wago modules detected and wrapped as an EtherCatDevice 2025-12-16 08:25:19 +01:00
rob e4d0b3c5be added logic for initializing wago modules on a 750_354 2025-12-12 15:24:59 +01:00
Robin Krämer dad029ab0d Merge pull request #924 from qitechgmbh/905-change-the-laser-graph-roundness-to-display-in-percentage-instead-of-mm
display laser roundness as percentage
2025-12-07 12:00:46 +01:00
EinsPommes 9c780799d1 fix: prettier formatting 2025-12-07 11:59:15 +01:00
EinsPommes 27a33f9def transform roundness to percentage and update related components 2025-12-07 11:59:15 +01:00
Robin Krämer f44cd029f9 Merge pull request #925 from qitechgmbh/891-switch-to-setup-page-when-selected-machine-disconnected
891 switch to setup page when selected machine disconnected
2025-12-07 11:58:58 +01:00
EinsPommes 9abb7629fb fix: prettier formatting 2025-12-07 11:56:41 +01:00
EinsPommes a161d85a63 fix: clarify machine existence check in SidebarLayout 2025-12-07 11:56:41 +01:00
EinsPommes 9df9e6df4a remove console.log statement 2025-12-07 11:56:41 +01:00
EinsPommes 29d465c13e 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-07 11:56:41 +01:00
EinsPommes 622c94f2d9 update: redirect to setup when machine disconnects 2025-12-07 11:56:41 +01:00
EinsPommes f31a2789ca auto-redirect to setup when machine disconnects 2025-12-07 11:56:41 +01:00
Robin Krämer e7632e8d1b Merge pull request #941 from qitechgmbh/936
936-documentation for minimal example
2025-12-07 11:56:25 +01:00
Achraf Jalleli 2f4f3de32a Update schematic asset path
Removed placeholder text for schematic asset path.
2025-12-06 21:23:01 +01:00
Achraf Jalleli c9f49198c7 updated image reference 2025-12-06 19:01:06 +01:00
Achraf Jalleli e2c12a0168 Refinements
Updated instructions for connecting and configuring the EK1100 and EL2004.
2025-12-06 18:59:43 +01:00
Achraf Jalleli cb6e066512 Fix formatting of Acknowledgements section header 2025-12-06 18:50:35 +01:00
Achraf Jalleli 1dd0421f96 documentation for minimal example 2025-12-06 18:48:49 +01:00
Achraf Jalleli 64855f2e7a Minimal Example Documentation added 2025-12-06 18:42:17 +01:00
Achraf Jalleli 855ae7b3cf minimal example docu assets 2025-12-06 18:20:54 +01:00
Robin Krämer 5f9a8605c9 Merge pull request #930 from qitechgmbh/923-create-extruder-v3
923 create extruder v3
2025-12-04 13:55:20 +01:00
rob 567730a8d4 fixed typos in machines 2025-12-04 13:25:07 +01:00
rob 6b2164b88b added frontend for extruder3 and fixed rpm calculation 2025-12-04 11:30:53 +01:00
rob 930ea10409 added extruder2 2025-12-04 11:26:17 +01:00
Robin Krämer 24b703e262 Merge pull request #931 from qitechgmbh/895-small-sample-machine-for-testing
small test machine
2025-12-04 11:23:42 +01:00
JalleliAchraf 152dd3efa0 small test machine 2025-12-04 11:17:24 +01:00
Milan von dem Bussche ebc5bae55f added Minimal Example README.md
Minimal example to explain the Code
2025-12-03 16:19:13 +01:00
Robin Krämer 88214c46a8 Merge pull request #911 from qitechgmbh/909-implement-ecma-error-messages
909 implement ecmy error messages
2025-11-30 00:41:49 +01:00
rob c9485a92e9 fixed formatting 2025-11-30 00:38:34 +01:00
rob 9df42c4d13 fixed server setup 2025-11-30 00:38:34 +01:00
rob fc9a439ae8 removed unused variables 2025-11-30 00:38:34 +01:00
rob e451153406 removed unused variables 2025-11-30 00:38:34 +01:00
rob 500e2fe481 fixed parsing etc 2025-11-30 00:38:34 +01:00
rob a5884d93e7 test 2025-11-30 00:38:34 +01:00
rob d2e32cdd78 added comment 2025-11-30 00:38:34 +01:00
rob f66c6d4f65 Note entirely sure it works ... 2025-11-30 00:38:34 +01:00
Oshgnacknak fb85cc6db7 Merge pull request #896 from qitechgmbh/877-remove-dependencies-that-are-not-necessary-or-bloated
Remove futures crate
2025-11-24 23:13:55 +01:00
Robin Krämer 4e128ded36 Merge pull request #885 from qitechgmbh/699-add-min-and-max-diameter-on-laser-page2
699 add min and max diameter on laser page
2025-11-24 13:02:35 +01:00
Oshgnacknak f50388b141 Just restart failed tasks 2025-11-24 12:54:40 +01:00
ChristianandCopilot 4025a68fe1 Update electron/src/control/MinMaxValue.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-23 17:58:07 +01:00
ChristianandCopilot 7a05fa206f Update electron/src/control/MinMaxValue.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-23 17:58:07 +01:00
Christian 8594b06c3b Reorganize shared timeframe selector for clarity and improve layout for diameter range display 2025-11-23 17:58:07 +01:00
Christian 17523574d8 Add shared timeframe selector to Laser1ControlPage and expose TIMEFRAME_OPTIONS 2025-11-23 17:58:07 +01:00
Christian 7971797178 Refactor diameter range display for 2-axis lasers and improve layout 2025-11-23 17:58:07 +01:00
Christian a8b0837e58 Add MinMaxValue component for displaying diameter ranges 2025-11-23 17:58:07 +01:00
Oshgnacknak e419974844 Refactor init logic to work without futures crate 2025-11-22 17:30:37 +01:00
Oshgnacknak 773d0f6c15 Remove tonic 2025-11-22 16:15:48 +01:00
Oshgnacknak afc4e0d8b4 Completely remove opentelemetry and update dependencies 2025-11-22 16:15:47 +01:00
97 changed files with 7167 additions and 664 deletions

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@ dist/
Cargo.lock
dhat-heap.json
build.sh
shell.nix
Generated
+424 -407
View File
File diff suppressed because it is too large. Load diff
+227 -1
View File
@@ -74,7 +74,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
# More Docs
# More Docs docs
/assets
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
@@ -152,3 +154,227 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
- [x] [NixOS Operating System](./docs/nixos/README.md)
## Minimal Example — LED Control on the EL2004
A complete hardware + software walkthrough
---
## Table of Contents
1. [Introduction](#1-introduction)
2. [Requirements](#2-requirements)
3. [Hardware Setup](#3-hardware-setup)
4. [Software Setup](#4-software-setup)
5. [Demo](#5-demo)
6. [References](#6-references)
7. [Acknowledgements](#7-acknowledgements)
---
# 1. Introduction
The EL2004 LED Toggle Example is a minimal demonstration showing how to control digital outputs on a **Beckhoff EL2004 EtherCAT terminal** using the QiTech machine framework.
It represents the simplest possible hardware interaction in the system:
**toggling LED outputs using the QiTech Control Dashboard.**
---
# 2. Requirements
## Hardware
- Beckhoff **EL2004 EtherCAT Terminal** (4-channel digital output)
- Beckhoff **EK1100 EtherCAT Coupler**
- **24 V DC power supply** (AC/DC adapter + DC hollow plug)
- Jumper / bridge wires (0.5–1.5 mm² recommended)
- A **Linux PC** (Ubuntu/Debian recommended)
- Standard Ethernet cable
- Flat screwdriver
## Software
*(Installation steps in Section 4)*
- Rust toolchain
- Node.js + npm
- Git
- QiTech Control repository
- EtherCAT HAL (included inside repo)
---
# 3. Hardware Setup
## 3.1 Schematic
![](./docs/assets/schematic.png)
---
## 3.2 EK1100 Wiring
This wiring configuration powers the EL2004 and prepares it for LED control.
It is not the only possible wiring but is the **simplest functional setup**.
### ⚠️ Safety Warning
Always disconnect power before wiring.
Working on live EtherCAT terminals can cause serious damage or electrical shock.
---
## 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
1. Insert a screwdriver **straight** into the square release hole.
2. Insert the stripped wire into the round opening.
3. Remove the screwdriver — the spring clamp locks the wire.
![](./docs/assets/wiring.png)
---
## 3.2.2 Wiring Steps (Used in This Example)
We supply power using a **DC hollow-plug adapter**, like this one:
https://www.amazon.de/dp/B093FTFZ8Q
Perform the following wiring on the EK1100:
1. Red wire **(+24 V)** → Terminal **2**
2. Black wire **(0 V)** → Terminal **3**
3. Jumper wire from **Terminal 1 → Terminal 6**
4. Jumper wire from **Terminal 5 → Terminal 7**
After wiring, your module should look like **Figure 1**.
---
### **Figure 1 — EK1100 Minimal Wiring**
<img src="./docs/assets/ek1100.jpeg" width="400">
---
## 3.3 EL2004 Integration
Slide the EL2004 onto the **right side of the EK1100** until it locks.
The EtherCAT E-Bus and power contacts connect automatically — **no wiring required**.
### **Figure 2 — EL2004 Terminal**
<img src="./docs/assets/el2004.jpeg" width="400">
---
## 3.4 Final Assembled Setup
### **Figure 3 — EK1100 + EL2004 Connected**
<img src="./docs/assets/complete.jpeg" width="400">
---
## 3.5 Power & Ethernet
### Power
Connect the 24 V adapter to the hollow plug used earlier.
**Example AC/DC Adapter (Figure 4):**
<img src="./docs/assets/adapter.jpeg" width="400">
### Ethernet
Use a standard LAN cable to connect your PC → EK1100.
The final powered up and connected setup should look like this:
<img src="./docs/assets/power.jpeg" width="400">
---
# 4. Software Setup
## 4.1 Installing on Ubuntu/Debian
Paste this into your terminal:
```bash
# Press Enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt update
sudo apt install -y npm nodejs git
git clone git@github.com:qitechgmbh/control.git
cd control/electron
npm install
```
## 4.2 Running the Backend
```bash
./cargo_run_linux.sh
```
This script:
- Builds the backend
- Grants required system capabilities (raw sockets)
- Starts EtherCAT communication
Ensure the EK1100 is connected.
## 4.3 Running the Frontend
```bash
cd electron
npm run start
```
This launches the QiTech Control dashboard.
# 5. Demo
## 5.1 Assigning Devices in the Dashboard
Once the backend + frontend are running, you should see:
EK1100 Coupler
EL2004 Digital Output Terminal
![](./docs/assets/discovery.png)
Steps:
- Click Assign on the EK1100
- Select TestMachine V1
![](./docs/assets/setmachine.png)
- Enter a serial number (use the same for EK1100 + EL2004)
![](./docs/assets/serserial.png)
- Click Write
- Repeat for the EL2004
## 5.2 Testing LED Control
Navigate to:
Machines → TestMachine
![](./docs/assets/machinedetected.png)
You will see this interface:
![](./docs/assets/machinecontrol.png)
**You can now toggle the four digital outputs of the EL2004.**
# 6. References
This guide incorporates information from official Beckhoff documentation.
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
Referenced Manuals
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
[Beckhoff EL2004 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el20xx_el2124_de.pdf)
# 7. Acknowledgements
This tutorial is inspired by the clarity and educational quality of Beckhoff manuals.
All wiring illustrations and hardware descriptions in this guide are provided for demonstration purposes only and do not replace official Beckhoff installation guidelines.
Special thanks to the QiTech engineering team for providing the backend architecture, EtherCAT HAL abstraction, and the TestMachine framework that makes this example possible.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+5 -4
View File
@@ -91,7 +91,7 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.6.2",
"prettier": "^3.7.3",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
@@ -12045,10 +12045,11 @@
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz",
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
+1 -1
View File
@@ -55,7 +55,7 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.6.2",
"prettier": "^3.7.3",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
+52 -2
View File
@@ -1,10 +1,16 @@
import { OutsideCorner } from "@/components/OutsideCorner";
import { useMachines } from "@/client/useMachines";
import { useOnSubpath } from "@/lib/useOnSubpath";
import { Link, Outlet } from "@tanstack/react-router";
import { Fragment } from "react";
import {
Link,
Outlet,
useRouter,
useRouterState,
} from "@tanstack/react-router";
import { Fragment, useEffect } from "react";
import React from "react";
import { Icon, IconName } from "./Icon";
import { useMainNamespace } from "@/client/mainNamespace";
type SidebarItemContent = {
link: string;
@@ -57,7 +63,51 @@ export function useSidebarlessWidth() {
export function SidebarLayout() {
const machines = useMachines();
const router = useRouter();
const routerState = useRouterState();
const { machines: machinesEvent } = useMainNamespace();
const [contentWidth, setContentWidth] = React.useState<number>(0);
// Machine connection guard: redirect to setup if selected machine disconnects
useEffect(() => {
// Wait for machines data to be loaded
if (!machinesEvent?.data) return;
// Extract serial from current path (e.g., /machines/laser1/12345/control)
const pathMatch = routerState.location.pathname.match(
/\/machines\/[^/]+\/(\d+)/,
);
if (!pathMatch) return; // Not on a machine page
// Safety check: ensure pathMatch has at least 2 elements (full match + capture group)
if (pathMatch.length < 2) {
console.warn("[ConnectionGuard] Invalid pathMatch structure:", pathMatch);
return;
}
// Parse serial number with error handling
const serialString = pathMatch[1];
const serialNumber = parseInt(serialString, 10);
// Check if parseInt succeeded (NaN means parsing failed)
if (isNaN(serialNumber)) {
console.warn(
`[ConnectionGuard] Failed to parse serial number from "${serialString}"`,
{ pathname: routerState.location.pathname },
);
return;
}
// Check if machine still exists in the raw machines data
const machineExists = machinesEvent.data.machines.some(
(m) => m.machine_identification_unique.serial === serialNumber,
);
if (!machineExists) {
router.navigate({ to: "/_sidebar/setup/machines" });
}
}, [machines, machinesEvent, routerState.location.pathname, router]);
const items: SidebarItemContent[] = [
...machines.map((machine) => ({
link: `/_sidebar/machines/${machine.slug}/${machine.machine_identification_unique.serial}/control`,
+5 -1
View File
@@ -63,7 +63,11 @@ export type DataSeries = SeriesData | SeriesData[];
export type BigGraphProps = {
newData: DataSeries;
unit?: Unit;
renderValue?: (value: number) => string;
renderValue?: (
value: number,
seriesIndex?: number,
seriesTitle?: string,
) => string;
config: GraphConfig;
graphId: string;
syncGraph?: PropGraphSync;
+173
View File
@@ -0,0 +1,173 @@
import { Icon, IconName } from "@/components/Icon";
import React, { useState, useMemo } from "react";
import {
getUnitIcon,
renderValueToReactNode,
renderUnitSymbol,
Unit,
} from "./units";
import { Label } from "./Label";
import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries";
type TimeframeOption = {
label: string;
value: number; // in milliseconds
};
export const TIMEFRAME_OPTIONS: TimeframeOption[] = [
{ label: "1 min", value: 1 * 60 * 1000 },
{ label: "5 min", value: 5 * 60 * 1000 },
{ label: "10 min", value: 10 * 60 * 1000 },
{ label: "30 min", value: 30 * 60 * 1000 },
{ label: "1 hour", value: 60 * 60 * 1000 },
];
type Props = {
label: string;
unit?: Unit;
timeseries: TimeSeries;
icon?: IconName;
renderValue?: (value: number) => string;
defaultTimeframe?: number; // in milliseconds, defaults to 5 minutes
/** Optional externally-controlled timeframe (ms). When provided, component becomes controlled. */
timeframe?: number;
/** Optional callback when timeframe is changed via this component's selector */
onTimeframeChange?: (t: number) => void;
/** When true, hide the built-in timeframe selector (useful when sharing a selector outside) */
hideSelector?: boolean;
};
export function MinMaxValue({
unit,
timeseries,
icon,
label,
renderValue,
defaultTimeframe = 5 * 60 * 1000, // 5 minutes default
timeframe,
onTimeframeChange,
hideSelector = false,
}: Props) {
const [internalTimeframe, setInternalTimeframe] = useState(defaultTimeframe);
const selectedTimeframe = timeframe ?? internalTimeframe;
// Calculate min/max from the timeseries using the selected timeframe
const { min, max, hasData } = useMemo(() => {
// Use short series for timeframes <= 5 seconds, otherwise use long series
// Short series typically has ~5 second retention with high resolution
// Long series typically has ~1 hour retention with lower resolution
const series =
selectedTimeframe <= 5000 ? timeseries.short : timeseries.long;
// Manually calculate min/max to exclude zero values (which might be invalid readings)
const cutoffTime = series.lastTimestamp - selectedTimeframe;
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
let hasValidData = false;
let dataCount = 0;
const { values: raw, index, size, validCount } = series;
const startIdx = validCount < size ? 0 : index;
for (let i = 0; i < validCount; i++) {
const idx = (startIdx + i) % size;
const val = raw[idx];
if (
val &&
val.timestamp > 0 &&
val.timestamp >= cutoffTime &&
val.value > 0
) {
hasValidData = true;
dataCount++;
if (val.value < min) min = val.value;
if (val.value > max) max = val.value;
}
}
if (!hasValidData) {
return { min: 0, max: 0, hasData: false };
}
return { min, max, hasData: true };
}, [
timeseries.short,
timeseries.long,
timeseries.current,
selectedTimeframe,
]);
return (
<div className="flex w-full flex-col gap-3">
<Label label={label}>
<div className="flex flex-row items-center gap-6">
{/* Min Value */}
<div className="flex flex-row items-center gap-2">
<Icon
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
className="size-5 text-blue-500"
/>
<div className="flex flex-col">
<span className="text-xs text-gray-500">Min</span>
<div className="flex flex-row items-center gap-1">
<span className="font-mono text-xl font-bold">
{hasData
? renderValueToReactNode(min, unit, renderValue)
: "-"}
</span>
{hasData && (
<span className="text-sm">{renderUnitSymbol(unit)}</span>
)}
</div>
</div>
</div>
{/* Max Value */}
<div className="flex flex-row items-center gap-2">
<Icon
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
className="size-5 text-red-500"
/>
<div className="flex flex-col">
<span className="text-xs text-gray-500">Max</span>
<div className="flex flex-row items-center gap-1">
<span className="font-mono text-xl font-bold">
{hasData
? renderValueToReactNode(max, unit, renderValue)
: "-"}
</span>
{hasData && (
<span className="text-sm">{renderUnitSymbol(unit)}</span>
)}
</div>
</div>
</div>
</div>
</Label>
{/* Timeframe Selector (hidden when sharing selector externally) */}
{!hideSelector && (
<div className="flex flex-row flex-wrap gap-2">
{TIMEFRAME_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => {
if (onTimeframeChange) onTimeframeChange(option.value);
else setInternalTimeframe(option.value);
}}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
selectedTimeframe === option.value
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{option.label}
</button>
))}
</div>
)}
</div>
);
}
+7
View File
@@ -35,6 +35,8 @@ export function getUnitIcon(unit: Unit): IconName {
return "lu:BatteryFull";
case "l/min":
return "lu:Waves";
case "%":
return "lu:ChartNoAxesColumn";
default:
return "lu:ChartNoAxesColumn";
}
@@ -72,6 +74,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
return "kWh";
case "l/min":
return "l/min";
case "%":
return "%";
default:
return "";
}
@@ -131,6 +135,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
return "kilowatt-hours";
case "l/min":
return "liters/minute";
case "%":
return "percent";
default:
return "";
}
@@ -152,6 +158,7 @@ export const units = [
"A",
"kWh",
"l/min",
"%",
] as const;
export type Unit = (typeof units)[number];
@@ -152,7 +152,7 @@ export function Extruder2ControlPage() {
unit="rpm"
title="Target Output RPM"
min={0}
max={106}
max={44}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetRpm}
/>
@@ -0,0 +1,258 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import {
SelectionGroup,
SelectionGroupBoolean,
} from "@/control/SelectionGroup";
import { HeatingZone } from "../HeatingZone";
import { Label } from "@/control/Label";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { StatusBadge } from "@/control/StatusBadge";
export function Extruder3ControlPage() {
const {
state,
defaultState,
nozzleTemperature,
nozzlePower,
frontTemperature,
frontPower,
backTemperature,
backPower,
middleTemperature,
middlePower,
pressure,
motorScrewRpm,
motorPower,
motorCurrent,
totalEnergyKWh,
combinedPower,
setExtruderMode,
setBackHeatingTemperature,
setFrontHeatingTemperature,
setMiddleHeatingTemperature,
setNozzleHeatingTemperature,
setInverterRegulation,
setInverterTargetPressure,
setInverterTargetRpm,
isLoading,
isDisabled,
} = useExtruder3();
function isZoneReadyForExtrusion(
temperature: number,
targetTemperature: number,
) {
// if temperature is 90% of the target temperature, then we are ready for extrusion
return temperature >= 0.9 * targetTemperature && targetTemperature > 0.0;
}
const allZonesReadyForExtrude = () => {
const frontReady = isZoneReadyForExtrusion(
frontTemperature.current?.value ?? 0,
state?.heating_states.front.target_temperature ?? 1,
);
const middleReady = isZoneReadyForExtrusion(
middleTemperature.current?.value ?? 0,
state?.heating_states.middle.target_temperature ?? 1,
);
const backReady = isZoneReadyForExtrusion(
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
return (
<Page>
<ControlGrid>
<HeatingZone
title={"Heating Front"}
heatingState={state?.heating_states.front}
heatingTimeSeries={frontTemperature}
heatingPower={frontPower}
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Middle"}
heatingState={state?.heating_states.middle}
heatingTimeSeries={middleTemperature}
heatingPower={middlePower}
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Back"}
heatingState={state?.heating_states.back}
heatingTimeSeries={backTemperature}
heatingPower={backPower}
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Nozzle"}
heatingState={state?.heating_states.nozzle}
heatingTimeSeries={nozzleTemperature}
heatingPower={nozzlePower}
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
<StatusBadge variant="error">
Inverter is overloaded! Please check the extruder and reduce load
if necessary.
</StatusBadge>
) : state?.inverter_status_state.fault_occurence == true ? (
<StatusBadge variant="error">
Inverter encountered an error!! Press the restart button in Config
</StatusBadge>
) : state?.inverter_status_state.running == true &&
state.inverter_status_state.fault_occurence == false ? (
<StatusBadge variant="success">Running</StatusBadge>
) : null}
{state?.inverter_status_state.running == false &&
state.inverter_status_state.fault_occurence == false && (
<StatusBadge variant="success">Healthy</StatusBadge>
)}
<Label label="Regulation">
<SelectionGroupBoolean
value={state?.regulation_state.uses_rpm}
optionTrue={{ children: "RPM" }}
optionFalse={{ children: "Pressure" }}
onChange={setInverterRegulation}
disabled={isDisabled}
loading={isLoading}
/>
</Label>
<div className="flex flex-row flex-wrap gap-4">
<Label label="Target Output RPM">
<EditValue
value={state?.screw_state.target_rpm}
defaultValue={defaultState?.screw_state.target_rpm}
unit="rpm"
title="Target Output RPM"
min={0}
max={100}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetRpm}
/>
</Label>
<Label label="Target Pressure">
<EditValue
value={state?.pressure_state.target_bar}
defaultValue={defaultState?.pressure_state.target_bar}
unit="bar"
title="Target Pressure"
min={0.0}
max={270.0}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetPressure}
/>
</Label>
</div>
<div className="flex flex-row flex-wrap gap-4">
<TimeSeriesValueNumeric
label="Rpm"
unit="rpm"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorScrewRpm}
/>
{state?.pressure_state?.wiring_error && (
<StatusBadge variant="error">
Cant Measure Pressure! Check Pressure Sensor Wiring!
</StatusBadge>
)}
<TimeSeriesValueNumeric
label="Pressure"
unit="bar"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={pressure}
/>
</div>
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<"Standby" | "Heat" | "Extrude">
value={state?.mode_state.mode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Heat: {
children: "Heat",
icon: "lu:Flame",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Extrude: {
children: "Extrude",
icon: "lu:ArrowBigLeftDash",
isActiveClassName: "bg-green-600",
className: "h-full",
confirmation: allZonesReadyForExtrude()
? undefined
: "Temperature is too low. Are you sure you want to extrude?",
},
}}
onChange={setExtruderMode}
disabled={isDisabled}
loading={isLoading}
/>
</ControlCard>
<ControlCard className="bg-blue" title="Power Consumption">
<TimeSeriesValueNumeric
label="Total Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={combinedPower}
/>
<TimeSeriesValueNumeric
label="Motor Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorPower}
/>
<TimeSeriesValueNumeric
label="Motor Current"
unit="A"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={motorCurrent}
/>
<TimeSeriesValueNumeric
label="Total Energy Consumption"
unit="kWh"
renderValue={(value) => roundToDecimals(value, 3)}
timeseries={totalEnergyKWh}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,325 @@
import { Page } from "@/components/Page";
import {
AutoSyncedBigGraph,
SyncedFloatingControlPanel,
useGraphSync,
type GraphConfig,
} from "@/components/graph";
import React from "react";
import { useExtruder3 } from "./useExtruder";
export function Extruder3GraphsPage() {
const {
state,
nozzleTemperature,
nozzlePower,
frontTemperature,
frontPower,
backTemperature,
backPower,
middleTemperature,
middlePower,
pressure,
motorScrewRpm,
motorCurrent,
motorPower,
combinedPower,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
// Base config
const baseConfig: GraphConfig = {
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
exportFilename: "extruder_data",
showLegend: true,
title: "extruder data",
};
const temperatureData = [
...(nozzleTemperature
? [
{
newData: nozzleTemperature,
title: "Nozzle",
color: "#ef4444",
lines:
state?.heating_states.nozzle?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
color: "#ef4444",
show: true,
},
]
: [],
},
]
: []),
...(frontTemperature
? [
{
newData: frontTemperature,
title: "Front",
color: "#f59e0b",
lines:
state?.heating_states.front?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
color: "#f59e0b",
show: true,
},
]
: [],
},
]
: []),
...(middleTemperature
? [
{
newData: middleTemperature,
title: "Middle",
color: "#8b5cf6",
lines:
state?.heating_states.middle?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
color: "#8b5cf6",
show: true,
},
]
: [],
},
]
: []),
...(backTemperature
? [
{
newData: backTemperature,
title: "Back",
color: "#3b82f6",
lines:
state?.heating_states.back?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
color: "#3b82f6",
show: true,
},
]
: [],
},
]
: []),
];
const temperatureConfig: GraphConfig = {
...baseConfig,
title: "Temperatures",
exportFilename: "temperatures_data",
colors: {
primary: "#ef4444",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Combined Power Graph (no target lines for power)
const powerData = [
...(nozzlePower
? [
{
newData: nozzlePower,
title: "Nozzle",
color: "#ef4444",
},
]
: []),
...(frontPower
? [
{
newData: frontPower,
title: "Front",
color: "#f59e0b",
},
]
: []),
...(middlePower
? [
{
newData: middlePower,
title: "Middle",
color: "#8b5cf6",
},
]
: []),
...(backPower
? [
{
newData: backPower,
title: "Back",
color: "#3b82f6",
},
]
: []),
...(motorPower
? [
{
newData: motorPower,
title: "Motor",
color: "#10b981",
},
]
: []),
...(combinedPower
? [
{
newData: combinedPower,
title: "Total",
color: "#000000",
},
]
: []),
];
const powerConfig: GraphConfig = {
...baseConfig,
title: "Power Outputs",
exportFilename: "power_data",
colors: {
primary: "#10b981",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
const currentConfig: GraphConfig = {
...baseConfig,
title: "Motor Current",
exportFilename: "motor_current_data",
colors: {
primary: "#3b82f6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Pressure Graph with connected target line
const pressureConfig: GraphConfig = {
...baseConfig,
title: "Pressure",
exportFilename: "pressure_data",
colors: {
primary: "#3b82f6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// RPM Graph with connected target line
const rpmConfig: GraphConfig = {
...baseConfig,
title: "RPM",
exportFilename: "rpm_data",
colors: {
primary: "#8b5cf6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
return (
<Page className="pb-25">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: pressure,
color: "#3b82f6",
lines:
state?.pressure_state.target_bar !== undefined
? [
{
type: "target" as const,
value: state.pressure_state.target_bar,
color: "#3b82f6",
show: true,
},
]
: [],
}}
config={pressureConfig}
unit="bar"
renderValue={(value) => value.toFixed(2)}
graphId="pressure-graph"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={temperatureData}
config={temperatureConfig}
unit="C"
renderValue={(value) => value.toFixed(1)}
graphId="combined-temperatures"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={powerData}
config={powerConfig}
unit="W"
renderValue={(value) => value.toFixed(1)}
graphId="combined-power"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: motorCurrent,
color: "#3b82f6",
}}
config={currentConfig}
unit="A"
renderValue={(value) => value.toFixed(2)}
graphId="motor-current"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: motorScrewRpm,
color: "#8b5cf6",
lines:
state?.screw_state.target_rpm !== undefined
? [
{
type: "target" as const,
value: state.screw_state.target_rpm,
color: "#8b5cf6",
show: true,
},
]
: [],
}}
config={rpmConfig}
unit="rpm"
renderValue={(value) => value.toFixed(0)}
graphId="rpm-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
@@ -0,0 +1,12 @@
import { Page } from "@/components/Page";
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
import React from "react";
import extruderManualContent from "@/assets/markdown/extruder/manual.md?raw";
export function ExtruderV3ManualPage() {
return (
<Page>
<MarkdownWithToc markdownContent={extruderManualContent} />
</Page>
);
}
@@ -0,0 +1,44 @@
import { Topbar } from "@/components/Topbar";
import { extruder3Route } from "@/routes/routes";
import React from "react";
export function Extruder3Page() {
const { serial } = extruder3Route.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/extruder3/${serial}`}
items={[
{
link: "control",
title: "Control",
icon: "lu:CirclePlay",
activeLink: "control",
},
{
link: "graphs",
title: "Graphs",
icon: "lu:ChartSpline",
activeLink: "graphs",
},
{
link: "settings",
title: "Config",
icon: "lu:Settings",
activeLink: "settings",
},
{
link: "manual",
title: "Manual",
icon: "lu:BookOpen",
activeLink: "manual",
},
{
link: "presets",
title: "Presets",
icon: "lu:Save",
activeLink: "presets",
},
]}
/>
);
}
@@ -0,0 +1,203 @@
import React from "react";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { extruder3 } from "@/machines/properties";
import { useExtruder3 } from "./useExtruder";
import { z } from "zod";
import {
PresetPreviewEntries,
previewSeparator,
} from "@/components/preset/PresetPreviewTable";
import { Preset, PresetData } from "@/lib/preset/preset";
const extruder3PresetDataSchema = z
.object({
targetFrontHeatingTemperature: z.number(),
targetMiddleHeatingTemperature: z.number(),
targetBackHeatingTemperature: z.number(),
targetNozzleHeatingTemperature: z.number(),
targetInverterRpm: z.number(),
targetInverterPressure: z.number(),
inverterRegulation: z.enum(["RPM", "Pressure"]),
inverterRotationDirection: z.enum(["Forward", "Backward"]),
pressureLimit: z.number(),
pressureLimitEnabled: z.boolean(),
pidPressureKp: z.number(),
pidPressureKi: z.number(),
pidPressureKd: z.number(),
})
.partial();
type Extruder3 = typeof extruder3PresetDataSchema;
const schemas = new Map([[1, extruder3PresetDataSchema]]);
const previewEntries: PresetPreviewEntries<Extruder3> = [
{
name: "Target Front Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetFrontHeatingTemperature?.toFixed(1),
},
{
name: "Target Middle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetMiddleHeatingTemperature?.toFixed(1),
},
{
name: "Target Back Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetBackHeatingTemperature?.toFixed(1),
},
{
name: "Target Nozzle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetNozzleHeatingTemperature?.toFixed(1),
},
previewSeparator,
{
name: "Inverter Regulation",
renderValue: (data: PresetData<Extruder3>) => data.inverterRegulation,
},
{
name: "Target Inverter RPM",
unit: "rpm",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterRpm?.toFixed(2),
},
{
name: "Target Inverter Pressure",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterPressure?.toFixed(1),
},
{
name: "Inverter Direction",
renderValue: (data: PresetData<Extruder3>) =>
data.inverterRotationDirection,
},
previewSeparator,
{
name: "Pressure Limit",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimit?.toFixed(1),
},
{
name: "Enable Pressure Limit",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimitEnabled ? "on" : "off",
},
previewSeparator,
{
name: "PID Pressue Kp",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKp?.toFixed(4),
},
{
name: "PID Pressue Ki",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKi?.toFixed(4),
},
{
name: "PID Pressue Kd",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKd?.toFixed(4),
},
];
export function Extruder3PresetsPage() {
const {
state,
defaultState,
setFrontHeatingTemperature,
setMiddleHeatingTemperature,
setBackHeatingTemperature,
setNozzleHeatingTemperature,
setInverterTargetRpm,
setInverterTargetPressure,
setInverterRegulation,
setInverterRotationDirection,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKd,
setPressurePidKi,
setPressurePidKp,
} = useExtruder3();
const toPresetData = (s?: typeof state): PresetData<Extruder3> => ({
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
targetNozzleHeatingTemperature: s?.heating_states.nozzle.target_temperature,
targetInverterRpm: s?.screw_state.target_rpm,
targetInverterPressure: s?.pressure_state.target_bar,
inverterRegulation: s?.regulation_state.uses_rpm ? "RPM" : "Pressure",
inverterRotationDirection: s?.rotation_state?.forward
? "Forward"
: "Backward",
pressureLimit: s?.extruder_settings_state.pressure_limit,
pressureLimitEnabled: s?.extruder_settings_state.pressure_limit_enabled,
pidPressureKp: s?.pid_settings.pressure.kp,
pidPressureKi: s?.pid_settings.pressure.ki,
pidPressureKd: s?.pid_settings.pressure.kd,
});
const defaults = toPresetData(defaultState);
const applyPreset = (preset: Preset<Extruder3>) => {
setFrontHeatingTemperature(
preset.data.targetFrontHeatingTemperature ?? 150.0,
);
setMiddleHeatingTemperature(
preset.data.targetMiddleHeatingTemperature ?? 150.0,
);
setBackHeatingTemperature(
preset.data.targetBackHeatingTemperature ?? 150.0,
);
setNozzleHeatingTemperature(
preset.data.targetNozzleHeatingTemperature ?? 150.0,
);
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
setInverterRegulation(preset.data.inverterRegulation === "RPM");
setInverterRotationDirection(
preset.data.inverterRotationDirection === "Forward",
);
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
};
return (
<PresetsPage
machine_identification={extruder3.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
schemaVersion={1}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={defaults}
/>
);
}
@@ -0,0 +1,291 @@
import React, { useState } from "react";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { Label } from "@/control/Label";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { ControlGrid } from "@/control/ControlGrid";
export function Extruder3SettingsPage() {
const {
state,
defaultState,
setInverterRotationDirection,
resetInverter,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
return (
<Page>
<ControlCard className="bg-red" title="Inverter Settings">
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.rotation_state.forward}
optionTrue={{ children: "Forward" }}
optionFalse={{ children: "Backward" }}
onChange={setInverterRotationDirection}
/>
</Label>
<Label label="Reset Inverter">
<button
onClick={resetInverter}
className="inline-block w-fit max-w-max rounded bg-red-600 px-4 py-4 text-base whitespace-nowrap text-white hover:bg-red-700"
style={{ minWidth: "auto", width: "fit-content" }}
>
Reset Inverter
</button>
</Label>
</ControlCard>
<ControlCard className="bg-red" title="Extruder Settings">
<Label label="Nozzle Pressure Limit">
<EditValue
value={state?.extruder_settings_state.pressure_limit}
defaultValue={defaultState?.extruder_settings_state.pressure_limit}
unit="bar"
title="Nozzle Pressure Limit"
min={0}
max={350}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setExtruderPressureLimit}
/>
</Label>
<Label label="Nozzle Pressure Limit Enabled">
<SelectionGroupBoolean
value={state?.extruder_settings_state.pressure_limit_enabled}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
optionTrue={{ children: "Show" }}
optionFalse={{ children: "Hide" }}
onChange={setShowAdvanced}
/>
</Label>
</ControlCard>
{showAdvanced && (
<>
<ControlCard title="Pressure PID Settings ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.pressure.kp}
defaultValue={defaultState?.pid_settings.pressure.kp}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKp}
title="Pressure PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.pressure.ki}
defaultValue={defaultState?.pid_settings.pressure.ki}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKi}
title="Pressure PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.pressure.kd}
defaultValue={defaultState?.pid_settings.pressure.kd}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKd}
title="Pressure PID KD"
/>
</Label>
</ControlCard>
<ControlGrid>
<ControlCard title="Temperature PID Settings (Front) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.front.kp}
defaultValue={defaultState?.pid_settings.temperature.front.kp}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.front.ki}
defaultValue={defaultState?.pid_settings.temperature.front.ki}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.front.kd}
defaultValue={defaultState?.pid_settings.temperature.front.kd}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Middle) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.middle.kp}
defaultValue={
defaultState?.pid_settings.temperature.middle.kp
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.middle.ki}
defaultValue={
defaultState?.pid_settings.temperature.middle.ki
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.middle.kd}
defaultValue={
defaultState?.pid_settings.temperature.middle.kd
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Back) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.back.kp}
defaultValue={defaultState?.pid_settings.temperature.back.kp}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.back.ki}
defaultValue={defaultState?.pid_settings.temperature.back.ki}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.back.kd}
defaultValue={defaultState?.pid_settings.temperature.back.kd}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Nozzle) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.nozzle.kp}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.kp
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.nozzle.ki}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.ki
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.nozzle.kd}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.kd
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
</ControlGrid>
</>
)}
</Page>
);
}
@@ -0,0 +1,433 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
handleUnhandledEventError,
} from "../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
import { useMemo } from "react";
// ========== Event Schema Definitions ==========
/**
* Machine operation mode enum
*/
export const modeSchema = z.enum(["Standby", "Heat", "Extrude"]);
export type Mode = z.infer<typeof modeSchema>;
export const liveMotorStatusDataSchema = z.object({
screw_rpm: z.number(),
frequency: z.number(),
current: z.number(),
power: z.number(),
});
export type MotorStatus = z.infer<typeof liveMotorStatusDataSchema>;
/**
* Consolidated live values event schema (30FPS data)
*/
export const liveValuesEventDataSchema = z.object({
motor_status: liveMotorStatusDataSchema,
pressure: z.number(),
nozzle_temperature: z.number(),
front_temperature: z.number(),
back_temperature: z.number(),
middle_temperature: z.number(),
nozzle_power: z.number(),
front_power: z.number(),
back_power: z.number(),
middle_power: z.number(),
combined_power: z.number(),
total_energy_kwh: z.number(),
});
/**
* Rotation state schema
*/
export const rotationStateSchema = z.object({
forward: z.boolean(),
});
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
});
/**
* Regulation state schema
*/
export const regulationStateSchema = z.object({
uses_rpm: z.boolean(),
});
/**
* Pressure state schema
*/
export const pressureStateSchema = z.object({
target_bar: z.number(),
wiring_error: z.boolean(),
});
/**
* Screw state schema
*/
export const screwStateSchema = z.object({
target_rpm: z.number(),
});
/**
* Heating state schema
*/
export const heatingStateSchema = z.object({
target_temperature: z.number(),
wiring_error: z.boolean(),
});
export type HeatingState = z.infer<typeof heatingStateSchema>;
/**
* Heating states schema
*/
export const heatingStatesSchema = z.object({
nozzle: heatingStateSchema,
front: heatingStateSchema,
back: heatingStateSchema,
middle: heatingStateSchema,
});
/**
* Extruder settings state schema
*/
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
});
/**
* Inverter status state schema
*/
export const inverterStatusStateSchema = z.object({
running: z.boolean(),
forward_running: z.boolean(),
reverse_running: z.boolean(),
up_to_frequency: z.boolean(),
overload_warning: z.boolean(),
no_function: z.boolean(),
output_frequency_detection: z.boolean(),
abc_fault: z.boolean(),
fault_occurence: z.boolean(),
});
/**
* PID settings schema
*/
export const pidSettingsSchema = z.object({
temperature: z.object({
front: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
middle: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
back: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
nozzle: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
pressure: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
}),
});
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
rotation_state: rotationStateSchema,
mode_state: modeStateSchema,
regulation_state: regulationStateSchema,
pressure_state: pressureStateSchema,
screw_state: screwStateSchema,
heating_states: heatingStatesSchema,
extruder_settings_state: extruderSettingsStateSchema,
inverter_status_state: inverterStatusStateSchema,
pid_settings: pidSettingsSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
// ========== Type Inferences ==========
export type StateEvent = z.infer<typeof stateEventSchema>;
// Additional exports for backward compatibility
export const SetRegulationSchema = z.object({
uses_rpm: z.boolean(),
});
export const mode = z.object({
mode: modeSchema,
});
export type Extruder3NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
motorFrequency: TimeSeries;
motorCurrent: TimeSeries;
motorScrewRpm: TimeSeries;
motorPower: TimeSeries;
pressure: TimeSeries;
nozzleTemperature: TimeSeries;
frontTemperature: TimeSeries;
backTemperature: TimeSeries;
middleTemperature: TimeSeries;
nozzlePower: TimeSeries;
frontPower: TimeSeries;
middlePower: TimeSeries;
backPower: TimeSeries;
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
};
// Constants for time durations
const TWENTY_MILLISECOND = 20;
const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: frontPower, insert: addFrontPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: middlePower, insert: addMiddlePower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (
updater: (state: Extruder3NamespaceStore) => Extruder3NamespaceStore,
) => {
throttledUpdater.updateWith(updater);
};
try {
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
motorScrewRpm: addMotorScrewRpm(state.motorScrewRpm, {
value: liveValuesEvent.data.motor_status.screw_rpm,
timestamp,
}),
motorCurrent: addMotorCurrent(state.motorCurrent, {
value: liveValuesEvent.data.motor_status.current,
timestamp,
}),
motorFrequency: addMotorFrequency(state.motorFrequency, {
value: liveValuesEvent.data.motor_status.frequency,
timestamp,
}),
motorPower: addMotorPower(state.motorPower, {
value: liveValuesEvent.data.motor_status.power,
timestamp,
}),
pressure: addPressure(state.pressure, {
value: liveValuesEvent.data.pressure,
timestamp,
}),
nozzleTemperature: addNozzleTemperature(state.nozzleTemperature, {
value: liveValuesEvent.data.nozzle_temperature,
timestamp,
}),
frontTemperature: addFrontTemperature(state.frontTemperature, {
value: liveValuesEvent.data.front_temperature,
timestamp,
}),
backTemperature: addBackTemperature(state.backTemperature, {
value: liveValuesEvent.data.back_temperature,
timestamp,
}),
middleTemperature: addMiddleTemperature(state.middleTemperature, {
value: liveValuesEvent.data.middle_temperature,
timestamp,
}),
nozzlePower: addNozzlePower(state.nozzlePower, {
value: liveValuesEvent.data.nozzle_power,
timestamp,
}),
frontPower: addFrontPower(state.frontPower, {
value: liveValuesEvent.data.front_power,
timestamp,
}),
middlePower: addMiddlePower(state.middlePower, {
value: liveValuesEvent.data.middle_power,
timestamp,
}),
backPower: addBackPower(state.backPower, {
value: liveValuesEvent.data.back_power,
timestamp,
}),
combinedPower: addCombinedPower(state.combinedPower, {
value: liveValuesEvent.data.combined_power,
timestamp,
}),
totalEnergyKWh: addTotalEnergyKWh(state.totalEnergyKWh, {
value: liveValuesEvent.data.total_energy_kwh,
timestamp,
}),
}));
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
console.error(`Unexpected error processing ${eventName} event:`, error);
throw error;
}
};
}
export const createExtruder3NamespaceStore =
(): StoreApi<Extruder3NamespaceStore> =>
create<Extruder3NamespaceStore>(() => {
return {
state: null,
defaultState: null,
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
backPower,
middlePower,
combinedPower,
totalEnergyKWh,
};
});
const useExtruder3NamespaceImplementation =
createNamespaceHookImplementation<Extruder3NamespaceStore>({
createStore: createExtruder3NamespaceStore,
createEventHandler: extruder3MessageHandler,
});
export function useExtruder3Namespace(
machine_identification_unique: MachineIdentificationUnique,
): Extruder3NamespaceStore {
// Generate namespace ID from validated machine ID
const namespaceId = useMemo<NamespaceId>(
() => ({
type: "machine",
machine_identification_unique,
}),
[machine_identification_unique],
);
// Use the implementation with validated namespace ID
return useExtruder3NamespaceImplementation(namespaceId);
}
@@ -0,0 +1,448 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { MachineIdentificationUnique } from "@/machines/types";
import { extruder3 } from "@/machines/properties";
import { extruder3Route } from "@/routes/routes";
import { z } from "zod";
import { StateEvent, Mode, useExtruder3Namespace } from "./extruder3Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
export function useExtruder3() {
const { serial: serialString } = extruder3Route.useParams();
// Memoize the machine identification to keep it stable between renders
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: {
vendor: 0,
machine: 0,
},
serial: 0,
};
}
return {
machine_identification: extruder3.machine_identification,
serial,
};
}, [serialString]);
// Get consolidated state and live values from namespace
const {
state,
defaultState,
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
};
// Action functions with verb-first names
const setInverterRotationDirection = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.rotation_state.forward = forward;
},
() =>
requestInverterRotationDirection({
machine_identification_unique: machineIdentification,
data: { SetInverterRotationDirection: forward },
}),
);
};
const setExtruderMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
},
() =>
requestExtruderMode({
machine_identification_unique: machineIdentification,
data: { SetExtruderMode: mode },
}),
);
};
const setInverterRegulation = (usesRpm: boolean) => {
updateStateOptimistically(
(current) => {
current.data.regulation_state.uses_rpm = usesRpm;
},
() =>
requestInverterRegulation({
machine_identification_unique: machineIdentification,
data: { SetInverterRegulation: usesRpm },
}),
);
};
const setInverterTargetRpm = (rpm: number) => {
updateStateOptimistically(
(current) => {
current.data.screw_state.target_rpm = rpm;
},
() =>
requestInverterTargetRpm({
machine_identification_unique: machineIdentification,
data: { SetInverterTargetRpm: rpm },
}),
);
};
const setInverterTargetPressure = (pressure: number) => {
updateStateOptimistically(
(current) => {
current.data.pressure_state.target_bar = pressure;
},
() =>
requestInverterTargetPressure({
machine_identification_unique: machineIdentification,
data: { SetInverterTargetPressure: pressure },
}),
);
};
const setNozzleHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.nozzle.target_temperature = temperature;
},
() =>
requestNozzleHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetNozzleHeatingTemperature: temperature },
}),
);
};
const setFrontHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.front.target_temperature = temperature;
},
() =>
requestFrontHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetFrontHeatingTargetTemperature: temperature },
}),
);
};
const setBackHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.back.target_temperature = temperature;
},
() =>
requestBackHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetBackHeatingTargetTemperature: temperature },
}),
);
};
const setMiddleHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.middle.target_temperature = temperature;
},
() =>
requestMiddleHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetMiddleHeatingTemperature: temperature },
}),
);
};
const setExtruderPressureLimit = (pressureLimit: number) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.pressure_limit = pressureLimit;
},
() =>
requestExtruderPressureLimit({
machine_identification_unique: machineIdentification,
data: { SetExtruderPressureLimit: pressureLimit },
}),
);
};
const setExtruderPressureLimitEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.pressure_limit_enabled = enabled;
},
() =>
requestExtruderPressureLimitEnabled({
machine_identification_unique: machineIdentification,
data: { SetExtruderPressureLimitIsEnabled: enabled },
}),
);
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.kp = kp;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.kp = kp;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setPressurePidKi = (ki: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.ki = ki;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.ki = ki;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setPressurePidKd = (kd: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.kd = kd;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.kd = kd;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setTemperaturePidValue = (
zone: "front" | "middle" | "back" | "nozzle",
key: "kp" | "ki" | "kd",
value: number,
) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.temperature[zone][key] = value;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.temperature[zone],
(draft) => {
draft[key] = value;
},
);
requestTemperaturePidSettings({
machine_identification_unique: machineIdentification,
data: { SetTemperaturePidSettings: settings },
});
}
},
);
};
const resetInverter = () => {
// No optimistic update needed for reset
requestResetInverter({
machine_identification_unique: machineIdentification,
data: { ResetInverter: true },
});
};
// Mutation hooks
const { request: requestInverterRotationDirection } = useMachineMutation(
z.object({ SetInverterRotationDirection: z.boolean() }),
);
const { request: requestExtruderMode } = useMachineMutation(
z.object({ SetExtruderMode: z.enum(["Heat", "Extrude", "Standby"]) }),
);
const { request: requestInverterRegulation } = useMachineMutation(
z.object({ SetInverterRegulation: z.boolean() }),
);
const { request: requestInverterTargetRpm } = useMachineMutation(
z.object({ SetInverterTargetRpm: z.number() }),
);
const { request: requestInverterTargetPressure } = useMachineMutation(
z.object({ SetInverterTargetPressure: z.number() }),
);
const { request: requestNozzleHeatingTemperature } = useMachineMutation(
z.object({ SetNozzleHeatingTemperature: z.number() }),
);
const { request: requestFrontHeatingTemperature } = useMachineMutation(
z.object({ SetFrontHeatingTargetTemperature: z.number() }),
);
const { request: requestBackHeatingTemperature } = useMachineMutation(
z.object({ SetBackHeatingTargetTemperature: z.number() }),
);
const { request: requestMiddleHeatingTemperature } = useMachineMutation(
z.object({ SetMiddleHeatingTemperature: z.number() }),
);
const { request: requestExtruderPressureLimit } = useMachineMutation(
z.object({ SetExtruderPressureLimit: z.number() }),
);
const { request: requestExtruderPressureLimitEnabled } = useMachineMutation(
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
}),
}),
);
const { request: requestTemperaturePidSettings } = useMachineMutation(
z.object({
SetTemperaturePidSettings: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
);
const { request: requestResetInverter } = useMachineMutation(
z.object({ ResetInverter: z.boolean() }),
);
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
// Action functions (verb-first)
setInverterRotationDirection,
setExtruderMode,
setInverterRegulation,
setInverterTargetRpm,
setInverterTargetPressure,
setNozzleHeatingTemperature,
setFrontHeatingTemperature,
setBackHeatingTemperature,
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
resetInverter,
};
}
@@ -3,6 +3,7 @@ import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
@@ -28,6 +29,12 @@ export function Laser1ControlPage() {
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
// Detect if this is a 2-axis laser
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
// Shared timeframe state (default 5 minutes)
const [timeframe, setTimeframe] = React.useState<number>(5 * 60 * 1000);
return (
<Page>
<ControlGrid columns={2}>
@@ -72,11 +79,64 @@ export function Laser1ControlPage() {
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Roundness"
unit="%"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
renderValue={(value) => (value * 100).toFixed(2)}
/>
</div>
)}
<div className="mt-4 border-t pt-4">
{isTwoAxis ? (
// For 2-axis lasers: show diameter and roundness min/max side by side
<div className="grid grid-cols-2 gap-4">
<MinMaxValue
label="Diameter Range"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
{roundness?.current && (
<MinMaxValue
label="Roundness Range"
unit="%"
timeseries={roundness}
renderValue={(value) => (value * 100).toFixed(2)}
timeframe={timeframe}
hideSelector
/>
)}
</div>
) : (
// For single-axis lasers: show only diameter min/max with shared selector
<MinMaxValue
label="Diameter Range"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
)}
{/* Shared timeframe selector for diameter/roundness */}
<div className="mt-3 flex flex-row flex-wrap gap-2">
{TIMEFRAME_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => setTimeframe(option.value)}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
timeframe === option.value
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{option.label}
</button>
))}
</div>
</div>
</ControlCard>
<ControlCard title="Settings">
<Label label="Set Target Diameter">
@@ -19,6 +19,32 @@ export function Laser1GraphsPage() {
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
// Transform roundness from ratio (0-1) to percentage (0-100)
const roundnessPercent = React.useMemo(() => {
if (!roundness) return null;
const transformValue = (
val: { value: number; timestamp: number } | null,
) => {
if (!val) return null;
return { value: val.value * 100, timestamp: val.timestamp };
};
const transformSeries = (series: any) => {
if (!series || !series.values) return series;
return {
...series,
values: series.values.map(transformValue),
};
};
return {
current: roundness.current ? transformValue(roundness.current) : null,
long: transformSeries(roundness.long),
short: transformSeries(roundness.short),
};
}, [roundness]);
const diameterColor = "#3b82f6";
const xDiameterColor = "#ef4444";
const yDiameterColor = "#22c55e";
@@ -121,13 +147,12 @@ export function Laser1GraphsPage() {
title: "Y-Diameter",
},
{
newData: roundness,
newData: roundnessPercent,
color: roundnessColor,
title: "Roundness",
title: "Roundness (%)",
},
]}
unit="mm"
renderValue={(value) => value.toFixed(3)}
renderValue={(value) => value.toFixed(2)}
config={config}
graphId="diameter-graph"
/>
+124
View File
@@ -103,6 +103,94 @@ export const winder2: MachineProperties = {
],
};
export const extruder3: MachineProperties = {
name: "Extruder V3",
version: "V3",
slug: "extruder3",
icon: "qi:Extruder",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0016,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "Inverter Interface",
allowed_devices: [
{
vendor_id: 2,
product_id: 394604626,
revision: 1376256,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x140000,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x160000,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x100000,
},
],
},
{
role: 2,
role_label: "Heating Elements",
allowed_devices: [
{
vendor_id: 2,
product_id: 131346514,
revision: 1179648,
},
],
},
{
role: 3,
role_label: "Pressure Sensor",
allowed_devices: [
{
vendor_id: 2,
product_id: 197996626,
revision: 1310720,
},
],
},
{
role: 4,
role_label: "Thermometers",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xc843052,
revision: 1441792,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x150000,
},
],
},
],
};
export const extruder2: MachineProperties = {
name: "Extruder",
version: "V2",
@@ -339,14 +427,50 @@ export const aquapath1: MachineProperties = {
},
],
};
export const testmachine: MachineProperties = {
name: "TestMachine",
version: "V1",
slug: "testmachine",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0033,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "EL2004",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x7d43052,
revision: 0x120000,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder3,
extruder2,
laser1,
mock1,
buffer1,
aquapath1,
testmachine,
];
export const getMachineProperties = (
@@ -0,0 +1,62 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroup } from "@/control/SelectionGroup";
import { Label } from "@/control/Label";
import { useTestMachine } from "./useTestMachine";
export function TestMachineControlPage() {
const { state, setLed, setAllLeds } = useTestMachine();
const safeState = state ?? { led_on: [false, false, false, false] };
return (
<Page>
<ControlGrid columns={2}>
{/* LED Controls */}
<ControlCard title="Machine LEDs">
<div className="grid grid-cols-2 gap-6">
{safeState.led_on.map((led, index) => (
<Label key={index} label={`LED ${index + 1}`}>
<SelectionGroup<"On" | "Off">
value={led ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => setLed(index, value === "On")}
/>
</Label>
))}
</div>
</ControlCard>
{/* Master LED Control */}
<ControlCard title="Master LED Control">
<SelectionGroup<"On" | "Off">
value={safeState.led_on.every(Boolean) ? "On" : "Off"}
orientation="horizontal"
options={{
Off: { children: "Turn All Off" },
On: { children: "Turn All On" },
}}
onChange={(value) => setAllLeds(value === "On")}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,20 @@
import { Topbar } from "@/components/Topbar";
import { testMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMachinePage() {
const { serial } = testMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -0,0 +1,76 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "@/client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
led_on: z.array(z.boolean()).length(4),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMachineNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineNamespaceStore =
(): StoreApi<TestMachineNamespaceStore> =>
create<TestMachineNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMachineMessageHandler(
store: StoreApi<TestMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (state: TestMachineNamespaceStore) => TestMachineNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useTestMachineNamespaceImplementation =
createNamespaceHookImplementation<TestMachineNamespaceStore>({
createStore: createTestMachineNamespaceStore,
createEventHandler: testMachineMessageHandler,
});
export function useTestMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMachineNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,95 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMachineSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMachineNamespace, StateEvent } from "./testMachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { testmachine } from "@/machines/properties";
import { z } from "zod";
export function useTestMachine() {
const { serial: serialString } = testMachineSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: testmachine.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMachineNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setLed = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on[index] = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetLed", value: { index, on } },
}),
);
};
const setAllLeds = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on = [on, on, on, on];
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAllLeds", value: { on } },
}),
);
};
return {
state: stateOptimistic.value,
setLed,
setAllLeds,
};
}
+83 -10
View File
@@ -1,46 +1,62 @@
import { createRoute, Outlet } from "@tanstack/react-router";
import { RootRoute } from "./__root";
import React from "react";
import { z } from "zod";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { SidebarLayout } from "@/components/SidebarLayout";
import { SetupPage } from "@/setup/SetupPage";
import { EthercatPage } from "@/setup/EthercatPage";
import { MachinesPage } from "@/setup/MachinesPage";
import { ChangelogPage } from "@/setup/ChangelogPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2Page } from "@/machines/winder/winder2/Winder2Page";
import { Winder2ControlPage } from "@/machines/winder/winder2/Winder2ControlPage";
import { Winder2ManualPage } from "@/machines/winder/winder2/Winder2Manual";
import { Winder2SettingPage } from "@/machines/winder/winder2/Winder2Settings";
import { Winder2GraphsPage } from "@/machines/winder/winder2/Winder2Graphs";
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Extruder2Page } from "@/machines/extruder/extruder2/Extruder2Page";
import { Extruder2ControlPage } from "@/machines/extruder/extruder2/Extruder2ControlPage";
import { Extruder2SettingsPage } from "@/machines/extruder/extruder2/Extruder2Settings";
import { ExtruderV2ManualPage } from "@/machines/extruder/extruder2/Extruder2Manual";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { Extruder2GraphsPage } from "@/machines/extruder/extruder2/Extruder2Graph";
import { Extruder2PresetsPage } from "@/machines/extruder/extruder2/Extruder2PresetsPage";
import { Extruder3Page } from "@/machines/extruder/extruder3/Extruder3Page";
import { Extruder3ControlPage } from "@/machines/extruder/extruder3/Extruder3ControlPage";
import { Extruder3SettingsPage } from "@/machines/extruder/extruder3/Extruder3Settings";
import { ExtruderV3ManualPage } from "@/machines/extruder/extruder3/Extruder3Manual";
import { Extruder3GraphsPage } from "@/machines/extruder/extruder3/Extruder3Graph";
import { Extruder3PresetsPage } from "@/machines/extruder/extruder3/Extruder3PresetsPage";
import { Buffer1ControlPage } from "@/machines/buffer/buffer1/Buffer1ControlPage";
import { Buffer1Page } from "@/machines/buffer/buffer1/Buffer1Page";
import { Buffer1SettingsPage } from "@/machines/buffer/buffer1/Buffer1Settings";
import { ChangelogPage } from "@/setup/ChangelogPage";
import { Extruder2GraphsPage } from "@/machines/extruder/extruder2/Extruder2Graph";
import { Extruder2PresetsPage } from "@/machines/extruder/extruder2/Extruder2PresetsPage";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { Laser1ControlPage } from "@/machines/laser/laser1/Laser1ControlPage";
import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { z } from "zod";
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
// _mainNavigation/configuration/a
@@ -48,6 +64,18 @@ import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
// the mainNavigation has a custom layout
// the winder2 winder2 and configuration also have a custom layout
// the leaf routes are just pages
export const testMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmachine/$serial",
component: () => <TestMachinePage />,
});
// Leaf route: control page
export const testMachineControlRoute = createRoute({
getParentRoute: () => testMachineSerialRoute,
path: "control",
component: () => <TestMachineControlPage />,
});
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
@@ -96,6 +124,42 @@ export const extruder2PresetsRoute = createRoute({
component: () => <Extruder2PresetsPage />,
});
export const extruder3Route = createRoute({
getParentRoute: () => machinesRoute,
path: "extruder3/$serial",
component: () => <Extruder3Page />,
});
export const extruder3ControlRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "control",
component: () => <Extruder3ControlPage />,
});
export const extruder3SettingsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "settings",
component: () => <Extruder3SettingsPage />,
});
export const extruder3ManualRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "manual",
component: () => <ExtruderV3ManualPage />,
});
export const extruder3GraphsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "graphs",
component: () => <Extruder3GraphsPage />,
});
export const extruder3PresetsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "presets",
component: () => <Extruder3PresetsPage />,
});
export const winder2SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "winder2/$serial",
@@ -313,6 +377,7 @@ export const rootTree = RootRoute.addChildren([
laser1GraphsRoute,
laser1PresetsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
@@ -335,6 +400,14 @@ export const rootTree = RootRoute.addChildren([
extruder2PresetsRoute,
]),
extruder3Route.addChildren([
extruder3ControlRoute,
extruder3SettingsRoute,
extruder3ManualRoute,
extruder3GraphsRoute,
extruder3PresetsRoute,
]),
mock1SerialRoute.addChildren([
mock1ControlRoute,
mock1GraphRoute,
+1 -1
View File
@@ -10,10 +10,10 @@ workspace = true
clap = { version = "4.5.40", features = ["derive"] }
ethercrab = "0.6.0"
interfaces = "0.0.9"
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
futures = "0.3.31"
anyhow = "1.0.100"
+3 -3
View File
@@ -7,7 +7,7 @@ use ethercat_hal::{
helpers::ethercrab_types::EthercrabSubDeviceGroupPreoperational,
};
use ethercrab::MainDevice;
use futures::executor::block_on;
use smol;
use crate::{MAX_SUBDEVICES, PDI_LEN, print::print_markdown_table};
@@ -34,7 +34,7 @@ pub fn ls(
let driver = device_from_subdevice_identity(&identity);
let identification_adresses = get_identification_addresses(&identity, device.name());
let identification = match identification_adresses {
Ok(_) => block_on(machine_device_identification(&device, maindevice)),
Ok(_) => smol::block_on(machine_device_identification(&device, maindevice)),
Err(_) => Err(anyhow!("")),
};
@@ -122,7 +122,7 @@ pub fn ls(
print_markdown_table(&table, true);
// Put group into op state
match block_on(group.into_op(maindevice)) {
match smol::block_on(group.into_op(maindevice)) {
Ok(_) => println!("Successfully put group into OP state"),
Err(e) => println!("Failed to put group into OP state: {}", e),
}
+7 -6
View File
@@ -2,18 +2,18 @@ use std::sync::Arc;
use dump::dump_eeprom;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, std::ethercat_now};
use futures::executor::block_on;
use read::read_eeprom;
use restore::restore_eeoprom;
use smol;
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
const MAX_SUBDEVICES: usize = 16;
/// Maximum PDU data payload size - set this to the max PDI size or higher.
const MAX_PDU_DATA: usize = PduStorage::element_size(256);
const MAX_PDU_DATA: usize = PduStorage::element_size(1024);
/// Maximum number of EtherCAT frames that can be in flight at any one time.
const MAX_FRAMES: usize = 16;
/// Maximum total PDI length.
const PDI_LEN: usize = 256;
const PDI_LEN: usize = 1024;
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
@@ -57,7 +57,7 @@ async fn main() {
.get_one::<usize>("SUBDEVICE")
.expect("subdevice index is required");
let file = sub_matches.get_one::<String>("file");
let result = block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
let result = smol::block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
if let Err(e) = result {
eprintln!("Error reading EEPROM: {}", e);
}
@@ -69,7 +69,8 @@ async fn main() {
let file = sub_matches
.get_one::<String>("file")
.expect("file is required");
let result = block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
let result =
smol::block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
if let Err(e) = result {
eprintln!("Error writing EEPROM: {}", e);
}
@@ -78,7 +79,7 @@ async fn main() {
let file = sub_matches
.get_one::<String>("file")
.expect("file is required");
let result = block_on(read_eeprom(file));
let result = smol::block_on(read_eeprom(file));
if let Err(e) = result {
eprintln!("Error parsing EEPROM: {}", e);
}
+21
View File
@@ -251,6 +251,27 @@ pub fn ethercat_device_derive(input: TokenStream) -> TokenStream {
fn as_any(&self) -> &dyn std::any::Any {
self
}
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
fn is_module(&self) -> bool {
false
}
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
fn get_module(&self) -> Option<crate::devices::Module> {
None
}
#[doc="Implemented by the ethercat_hal_derive::EthercatDevice derive macro"]
fn set_module(&mut self,module : crate::devices::Module){
()
}
}
impl crate::devices::EthercatDeviceUsed for #name {
-1
View File
@@ -14,7 +14,6 @@ units = { path = "../units" }
bitvec = { version = "1.0.1", features = ["alloc"] }
smol = "2.0.2"
rand = "0.9.2"
futures = "0.3.31"
tracing = "0.1.41"
[dev-dependencies]
@@ -0,0 +1,229 @@
use ethercrab::{SubDevice, SubDeviceRef};
use std::fmt;
const DIAGNOSIS_HISTORY_INDEX: u16 = 0x10f3;
const NEWEST_MESSAGE: u8 = 02;
const NEW_MESSAGE_AVAILABLE: u8 = 04;
const DIAG_MESSAGE_LENGTH: usize = 26;
pub struct SubdeviceDiagnosisEntry {
pub diag_code: u32,
pub flags: u16,
pub text_id: u16,
pub timestamp: u64,
pub data_type_p1: u16,
pub p1: u8,
pub flags_p2: u16,
pub p2: [u8; 5],
}
impl fmt::Display for SubdeviceDiagnosisEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SubdeviceDiagnosisEntry {{ diag_code: {}, flags: {}, text_id: {}, timestamp: {}, data_type_p1: {}, p1: {}, flags_p2: {}, p2: {:?} }}",
self.diag_code,
self.flags,
self.text_id,
self.timestamp,
self.data_type_p1,
self.p1,
self.flags_p2,
self.p2
)
}
}
impl SubdeviceDiagnosisEntry {
pub fn to_pretty_string(&self) -> String {
// Decode message type
let msg_type = match self.flags {
0x0000 => "Info",
0x0001 => "Warning",
0x0002 => "Error",
_ => "Unknown",
};
// Decode data type of P1
let p1_type = match self.data_type_p1 {
0x0005 => "UINT8",
_ => "Unknown",
};
// Decode data type of P2
let p2_type = match self.flags_p2 {
0x0005 => "BYTE[5]",
_ => "Unknown",
};
// Format P2 bytes as hex list
let p2_hex: Vec<String> = self.p2.iter().map(|b| format!("0x{:02X}", b)).collect();
format!(
"Diagnosis Entry:\n\
├─ diag_code: 0x{:08X}\n\
├─ message type: {} (0x{:04X})\n\
├─ text_id: {}{}\n\
├─ timestamp: {} (64-bit)\n\
├─ P1 type: {} (0x{:04X})\n\
├─ P1 value: {}\n\
├─ P2 type: {} (0x{:04X})\n\
└─ P2 data: [{}]",
self.diag_code,
msg_type,
self.flags,
self.text_id,
if self.text_id == 0 { " (no text)" } else { "" },
self.timestamp,
// P1
p1_type,
self.data_type_p1,
self.p1,
// P2
p2_type,
self.flags_p2,
p2_hex.join(", ")
)
}
}
/*
Could be tested with this:
00 E8 00 A0 02 00 00 00 5F FB D5 A5 A6 0B 00 00 05 00 11 05 10 08 2A 00 2A 00
*/
pub fn convert_raw_diagnosis_bytes(
message: [u8; DIAG_MESSAGE_LENGTH],
) -> Result<SubdeviceDiagnosisEntry, anyhow::Error> {
let diag_code_bytes = [message[0], message[1], message[2], message[3]];
let diag_code = u32::from_le_bytes(diag_code_bytes);
let flags_code_bytes = [message[4], message[5]];
let flags = u16::from_le_bytes(flags_code_bytes);
let text_id_bytes = [message[6], message[7]];
let text_id = u16::from_le_bytes(text_id_bytes);
let timestamp_bytes = [
message[8],
message[9],
message[10],
message[11],
message[12],
message[13],
message[14],
message[15],
];
let timestamp = u64::from_le_bytes(timestamp_bytes);
let data_type_p1_bytes = [message[16], message[17]];
let data_type_p1 = u16::from_le_bytes(data_type_p1_bytes);
// TODO: Find the Constants
// 0x0005 Apparently Means p1 variable is u8
// For now we only handle u8
let p1 = match data_type_p1 {
0x0005 => message[18],
_ => 0,
};
if p1 == 0 {
return Err(anyhow::anyhow!(
"convert_raw_diagnosis_bytes: Datatype_p1 is unknown {:?}",
data_type_p1
));
}
// flags_p2 is packed, whatever thats supposed to mean
let flags_p2_bytes = [message[19], message[20]];
let flags_p2 = u16::from_le_bytes(flags_p2_bytes);
let p2 = [
message[21],
message[22],
message[23],
message[24],
message[25],
];
return Ok(SubdeviceDiagnosisEntry {
diag_code,
flags,
text_id,
timestamp,
data_type_p1,
p1,
flags_p2,
p2,
});
}
/// Expects the SubdeviceRef to be the Coupler or any other device with a diag history index
pub async fn get_most_recent_diagnosis_message(
device: &SubDeviceRef<'_, &SubDevice>,
) -> Option<String> {
let new_message_available = device
.sdo_read::<bool>(DIAGNOSIS_HISTORY_INDEX, NEW_MESSAGE_AVAILABLE)
.await;
let new_message_available = match new_message_available {
Ok(m) => m,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to read new_message_available {:?}",
e
);
return None;
}
};
if !new_message_available {
tracing::info!("get_most_recent_diagnosis_message: No Diagnosis Messages found");
// No messages available
return None;
}
let newest_message_index = device
.sdo_read::<u8>(DIAGNOSIS_HISTORY_INDEX, NEWEST_MESSAGE)
.await;
let newest_message_index = match newest_message_index {
Ok(m) => m,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to read newest_message_index {:?}",
e
);
return None;
}
};
tracing::info!(
"get_most_recent_diagnosis_message: Reading newest message at index: {}",
newest_message_index
);
let res = device
.sdo_read::<[u8; DIAG_MESSAGE_LENGTH]>(DIAGNOSIS_HISTORY_INDEX, newest_message_index)
.await;
let message: [u8; DIAG_MESSAGE_LENGTH] = match res {
Ok(res) => res,
Err(_) => {
tracing::error!("get_most_recent_diagnosis_message: Failed to read Diagnosis Message");
return None;
}
};
let message = convert_raw_diagnosis_bytes(message);
let message = match message {
Ok(msg) => msg,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to convert Diagnosis Message bytes to struct {:?}",
e
);
return None;
}
};
tracing::info!(
"get_most_recent_diagnosis_message: {}",
message.to_pretty_string()
);
return Some(message.to_pretty_string());
}
+1
View File
@@ -0,0 +1 @@
pub mod diagnosis_history;
+40 -17
View File
@@ -20,7 +20,9 @@ pub mod el6021;
pub mod el7031;
pub mod el7031_0030;
pub mod el7041_0052;
// pub mod el4008;
pub mod wago_750_354;
pub mod wago_modules;
use super::devices::el1008::EL1008;
use crate::{
devices::{el2521::EL2521, el4002::EL4002},
@@ -41,18 +43,29 @@ use el3021::EL3021_IDENTITY_A;
use el3024::EL3024_IDENTITY_A;
use el3062_0030::EL3062_0030_IDENTITY_A;
use el3204::EL3204_IDENTITY_A;
use el3204::EL3204_IDENTITY_B;
use el4002::EL4002_IDENTITY_A;
use el5152::{EL5152, EL5152_IDENTITY_A};
use el6021::{EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D};
use el3204::EL3204_IDENTITY_B;
use el7031::{EL7031_IDENTITY_A, EL7031_IDENTITY_B};
use el7031_0030::EL7031_0030_IDENTITY_A;
use el7041_0052::EL7041_0052_IDENTITY_A;
use ethercrab::{MainDevice, SubDeviceIdentity};
use smol::lock::RwLock;
use std::{any::Any, fmt::Debug, sync::Arc};
use wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
#[derive(Debug, Clone, Copy)]
pub struct Module {
pub slot: u16,
pub belongs_to_addr: u16,
pub has_tx: bool,
pub has_rx: bool,
pub vendor_id: u32,
pub product_id: u32,
pub tx_offset: usize,
pub rx_offset: usize,
}
/// A trait for all devices
///
@@ -77,7 +90,7 @@ where
/// automatically validate input length, then calls input
fn input_checked(&mut self, input: &BitSlice<u8, Lsb0>) -> Result<(), anyhow::Error> {
// validate input has correct length
let expected = self.input_len();
/* let expected = self.input_len();
let actual = input.len();
if actual != expected {
return Err(anyhow::anyhow!(
@@ -88,7 +101,7 @@ where
expected,
expected / 8
));
}
}*/
self.input(input)
}
@@ -103,7 +116,7 @@ where
self.output(output)?;
// validate input has correct length
let expected = self.output_len();
/*let expected = self.output_len();
let actual = output.len();
if output.len() != expected {
return Err(anyhow::anyhow!(
@@ -114,12 +127,26 @@ where
expected,
expected / 8
));
}
}*/
Ok(())
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn is_module(&self) -> bool;
fn get_module(&self) -> Option<Module>;
fn set_module(&mut self, module: Module);
}
pub trait DynamicEthercatDevice: EthercatDevice + EthercatDynamicPDO {}
pub trait EthercatDynamicPDO {
fn get_tx_offset(&self) -> usize;
fn get_rx_offset(&self) -> usize;
fn set_tx_offset(&mut self, offset: usize);
fn set_rx_offset(&mut self, offset: usize);
}
/// A trait for devices that want to process input and output data
@@ -186,19 +213,14 @@ pub fn device_from_subdevice_identity_tuple(
subdevice_identity_tuple: SubDeviceIdentityTuple,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, anyhow::Error> {
match subdevice_identity_tuple {
WAGO_750_354_IDENTITY_A => Ok(Arc::new(RwLock::new(Wago750_354::new()))),
EK1100_IDENTITY_A => Ok(Arc::new(RwLock::new(EK1100::new()))),
EL1002_IDENTITY_A => Ok(Arc::new(RwLock::new(EL1002::new()))),
EL1008_IDENTITY_A => Ok(Arc::new(RwLock::new(EL1008::new()))),
EL2002_IDENTITY_A | EL2002_IDENTITY_B => Ok(Arc::new(RwLock::new(EL2002::new()))),
EL2004_IDENTITY_A => Ok(Arc::new(RwLock::new(EL2004::new()))),
EL2008_IDENTITY_A | EL2008_IDENTITY_B => Ok(Arc::new(RwLock::new(EL2008::new()))),
// TODO: implement EL2024 identity
// EL2024 => Ok(Arc::new(RwLock::new(EL2024::new()))),
EL2522_IDENTITY_A => Ok(Arc::new(RwLock::new(EL2522::new()))),
// TODO: implement EL2634 identity
// "EL2634" => Ok(Arc::new(RwLock::new(EL2634::new()))),
// TODO: implement EL2809 identity
// "EL2809" => Ok(Arc::new(RwLock::new(EL2809::new()))),
EL3001_IDENTITY_A => Ok(Arc::new(RwLock::new(el3001::EL3001::new()))),
EL3021_IDENTITY_A => Ok(Arc::new(RwLock::new(el3021::EL3021::new()))),
EL3024_IDENTITY_A => Ok(Arc::new(RwLock::new(el3024::EL3024::new()))),
@@ -209,9 +231,6 @@ pub fn device_from_subdevice_identity_tuple(
Ok(Arc::new(RwLock::new(el6021::EL6021::new())))
}
EL3204_IDENTITY_A | EL3204_IDENTITY_B => Ok(Arc::new(RwLock::new(el3204::EL3204::new()))),
// "EL4008" => Ok(Arc::new(RwLock::new(EL4008::new()))),
// TODO: implement EL3204 identity
// "EL3204" => Ok(Arc::new(RwLock::new(EL3204::new()))),
EL7031_IDENTITY_A | EL7031_IDENTITY_B => Ok(Arc::new(RwLock::new(el7031::EL7031::new()))),
EL7031_0030_IDENTITY_A => Ok(Arc::new(RwLock::new(el7031_0030::EL7031_0030::new()))),
EL7041_0052_IDENTITY_A => Ok(Arc::new(RwLock::new(el7041_0052::EL7041_0052::new()))),
@@ -265,6 +284,10 @@ pub async fn specific_device_from_devices<DEVICE: EthercatDevice>(
}
pub type SubDeviceIdentityTuple = (u32, u32, u32);
// Is vendor id at 0, and prodid at 1
pub type SubDeviceProductTuple = (u32, u32);
/// function that converts SubDeviceIdentity to tuple
pub const fn subdevice_identity_to_tuple(identity: &SubDeviceIdentity) -> SubDeviceIdentityTuple {
(identity.vendor_id, identity.product_id, identity.revision)
+341
View File
@@ -0,0 +1,341 @@
use super::{
EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed, NewEthercatDevice,
SubDeviceIdentityTuple,
};
use crate::devices::wago_modules::*;
use crate::{
devices::{
DynamicEthercatDevice, Module,
wago_modules::{
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_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},
},
},
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
};
use anyhow::Error;
use smol::lock::RwLock;
use std::sync::Arc;
const MODULE_COUNT_INDEX: (u16, u8) = (0xf050, 0x00);
const TX_MAPPING_INDEX: (u16, u8) = (0x1c13, 0x00);
const RX_MAPPING_INDEX: (u16, u8) = (0x1c12, 0x00);
// For both the rx and tx The Wago Coupler has 4 bytes, which we dont care about and skip
/// Wago750_354 bus coupler
/*
The "Modules" simply write at an offset into rx and read at an offset in tx
*/
pub struct Wago750_354 {
is_used: bool,
pub slots: [Option<Module>; 64],
pub slot_devices: [Option<Arc<RwLock<dyn DynamicEthercatDevice>>>; 64],
pub dev_count: usize,
pub module_count: usize,
rx_offsets: Vec<usize>,
tx_offsets: Vec<usize>,
tx_size: usize,
rx_size: usize,
}
impl EthercatDevice for Wago750_354 {
fn input(
&mut self,
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
for slot_device in &mut self.slot_devices {
match slot_device {
Some(device) => {
// Give all Modules access to the couplers input image and call their input func
let mut d = device.write_blocking();
let _ = d.input(input);
drop(d);
}
None => break,
}
}
Ok(())
}
fn input_len(&self) -> usize {
self.tx_size
}
fn output(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
for slot_device in &self.slot_devices {
match slot_device {
Some(device) => {
// Give all Modules access to the couplers Output image and call their output func
let d = device.read_blocking();
let _ = d.output(output);
drop(d);
}
None => break,
}
}
Ok(())
}
fn output_len(&self) -> usize {
self.rx_size
}
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 {
false
}
fn get_module(&self) -> Option<Module> {
None
}
fn set_module(&mut self, module: Module) {
self.slots[self.module_count] = Some(module);
self.module_count += 1;
self.tx_size += module.tx_offset;
self.rx_size += module.rx_offset;
}
}
impl EthercatDeviceUsed for Wago750_354 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl EthercatDeviceProcessing for Wago750_354 {}
impl NewEthercatDevice for Wago750_354 {
fn new() -> Self {
Self {
is_used: false,
slots: [const { None }; 64],
slot_devices: [const { None }; 64],
module_count: 0,
dev_count: 0,
tx_size: 0,
rx_size: 0,
rx_offsets: vec![],
tx_offsets: vec![],
}
}
}
impl std::fmt::Debug for Wago750_354 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago_750_354")
}
}
impl Wago750_354 {
pub async fn get_pdo_offsets<'a>(
&mut self,
device: &EthercrabSubDevicePreoperational<'a>,
get_tx: bool,
) -> Result<(), Error> {
let mut vec: Vec<usize> = vec![];
let mut bit_offset = 0;
let start_subindex = 0x1;
let index = match get_tx {
true => (TX_MAPPING_INDEX.0, TX_MAPPING_INDEX.1),
false => (RX_MAPPING_INDEX.0, RX_MAPPING_INDEX.1),
};
let count = device.sdo_read::<u8>(index.0, index.1).await?;
for i in 0..count {
vec.push(bit_offset);
let pdo_index = device.sdo_read(index.0, start_subindex + i).await?;
if pdo_index != 0 {
let pdo_map_count = device.sdo_read::<u8>(pdo_index, 0).await?;
// Iterate over every PDO Mapped entry and add it to the cumulative bit offset
for j in 0..pdo_map_count {
let pdo_mapping: u32 = device.sdo_read(pdo_index, 1 + j).await?;
// We only need / Want the bit len, which we extract with a bitmask extracting the lsb
let bit_length = (pdo_mapping & 0xFF) as u8;
bit_offset += bit_length as usize;
}
}
}
if get_tx {
self.tx_offsets = vec;
} else {
self.rx_offsets = vec;
}
Ok(())
}
pub async fn get_module_count<'a>(
device: &EthercrabSubDevicePreoperational<'a>,
) -> Result<usize, Error> {
match device
.sdo_read::<u8>(MODULE_COUNT_INDEX.0, MODULE_COUNT_INDEX.1)
.await
{
Ok(value) => Ok(value as usize),
Err(e) => Err(anyhow::anyhow!(
"Failed to read Module Count for Wago750_354: {:?}",
e
)),
}
}
// This should probably be a generic function instead
pub async fn get_modules<'a>(
device: &EthercrabSubDevicePreoperational<'a>,
module_count: usize,
) -> Result<Vec<crate::devices::Module>, Error> {
const MODULES_START_ADDR: u16 = 0x9000;
const MODULE_IDENT_SUBINDEX: u8 = 0x0a;
let mut modules: Vec<Module> = vec![];
for i in 0..module_count {
let module_addr = MODULES_START_ADDR + (i * 0x10) as u16;
let ident_iom = device
.sdo_read::<u32>(module_addr, MODULE_IDENT_SUBINDEX)
.await?;
// For Wago the IOM well be the product ID
let mut module = Module {
slot: i as u16,
belongs_to_addr: device.configured_address(),
vendor_id: device.identity().vendor_id,
product_id: ident_iom,
has_tx: false,
has_rx: false,
tx_offset: 0,
rx_offset: 0,
};
match ident_iom {
// Add Module idents here:
WAGO_750_1506_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
}
WAGO_750_501_PRODUCT_ID => {
module.has_tx = false;
module.has_rx = true;
}
WAGO_750_652_PRODUCT_ID => {
module.has_tx = true;
module.has_rx = true;
}
_ => println!(
"Wago-750-354 found Unknown/Unimplemented Module: {}",
ident_iom
),
}
modules.push(module);
}
Ok(modules)
}
/// Call after all modules have been added
pub fn init_slot_modules<'a>(&mut self, device: &EthercrabSubDevicePreoperational<'a>) {
// Already initialized
if self.dev_count != 0 {
return;
}
let mut tx_index = 1;
let mut rx_index = 1;
smol::block_on(async {
let _ = self.get_pdo_offsets(device, true).await;
let _ = self.get_pdo_offsets(device, false).await;
});
for module in self.slots {
match module {
Some(m) => {
// Map ModuleIdent's to Terminals
let dev: Arc<RwLock<dyn DynamicEthercatDevice>> = match (
m.vendor_id,
m.product_id,
) {
WAGO_750_501_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_501::Wago750_501::new()))
}
WAGO_750_1506_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_1506::Wago750_1506::new()))
}
WAGO_750_652_MODULE_IDENT => {
Arc::new(RwLock::new(wago_750_652::Wago750_652::new()))
}
_ => {
println!(
"{} Missing Implementation for Module Identification: vendor_id: {:?}, module ident: {:?} !",
module_path!(),
m.vendor_id,
m.product_id
);
return;
}
};
let tx_pdo_offset = self.tx_offsets.get(tx_index as usize);
if m.has_tx {
tx_index += 1;
}
let rx_pdo_offset = self.rx_offsets.get(rx_index as usize);
if m.has_rx {
rx_index += 1;
}
let mut dev_guard = dev.write_blocking();
match tx_pdo_offset {
Some(offset) => dev_guard.set_tx_offset(*offset),
None => (),
}
match rx_pdo_offset {
Some(offset) => dev_guard.set_rx_offset(*offset),
None => (),
}
drop(dev_guard);
self.slot_devices[self.dev_count] = Some(dev);
self.dev_count += 1;
}
None => break,
}
}
}
pub async fn initialize_modules<'a>(
device: &EthercrabSubDevicePreoperational<'a>,
) -> Result<Vec<Module>, Error> {
let count = match Wago750_354::get_module_count(device).await {
Ok(count) => count,
Err(e) => return Err(e),
};
if count == 0 {
return Ok(vec![]);
}
let modules = Wago750_354::get_modules(device, count).await?;
Ok(modules)
}
}
pub const WAGO_750_354_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_354_PRODUCT_ID: u32 = 0x07500354;
pub const WAGO_750_354_REVISION_A: u32 = 0x2;
pub const WAGO_750_354_IDENTITY_A: SubDeviceIdentityTuple = (
WAGO_750_354_VENDOR_ID,
WAGO_750_354_PRODUCT_ID,
WAGO_750_354_REVISION_A,
);
@@ -0,0 +1,3 @@
pub mod wago_750_1506;
pub mod wago_750_501;
pub mod wago_750_652;
@@ -0,0 +1,335 @@
use crate::{
devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
},
io::{
digital_input::{DigitalInputDevice, DigitalInputInput},
digital_output::{DigitalOutputDevice, DigitalOutputOutput},
},
};
#[derive(Debug, Clone)]
pub enum Wago750_1506OutputPort {
DO1,
DO2,
DO3,
DO4,
DO5,
DO6,
DO7,
DO8,
}
impl From<Wago750_1506OutputPort> for usize {
fn from(value: Wago750_1506OutputPort) -> Self {
match value {
Wago750_1506OutputPort::DO1 => 0,
Wago750_1506OutputPort::DO2 => 1,
Wago750_1506OutputPort::DO3 => 2,
Wago750_1506OutputPort::DO4 => 3,
Wago750_1506OutputPort::DO5 => 4,
Wago750_1506OutputPort::DO6 => 5,
Wago750_1506OutputPort::DO7 => 6,
Wago750_1506OutputPort::DO8 => 7,
}
}
}
#[derive(Debug, Clone)]
pub enum Wago750_1506InputPort {
DO1,
DO2,
DO3,
DO4,
DO5,
DO6,
DO7,
DO8,
}
impl From<Wago750_1506InputPort> for usize {
fn from(value: Wago750_1506InputPort) -> Self {
match value {
Wago750_1506InputPort::DO1 => 0,
Wago750_1506InputPort::DO2 => 1,
Wago750_1506InputPort::DO3 => 2,
Wago750_1506InputPort::DO4 => 3,
Wago750_1506InputPort::DO5 => 4,
Wago750_1506InputPort::DO6 => 5,
Wago750_1506InputPort::DO7 => 6,
Wago750_1506InputPort::DO8 => 7,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_1506RxPdo {
port1: bool,
port2: bool,
port3: bool,
port4: bool,
port5: bool,
port6: bool,
port7: bool,
port8: bool,
}
#[derive(Clone, Default)]
pub struct Wago750_1506TxPdo {
port1: bool,
port2: bool,
port3: bool,
port4: bool,
port5: bool,
port6: bool,
port7: bool,
port8: bool,
}
impl DigitalInputDevice<Wago750_1506InputPort> for Wago750_1506 {
fn get_input(&self, port: Wago750_1506InputPort) -> Result<DigitalInputInput, anyhow::Error> {
Ok(DigitalInputInput {
value: match port {
Wago750_1506InputPort::DO1 => self.tx_pdo.port1,
Wago750_1506InputPort::DO2 => self.tx_pdo.port2,
Wago750_1506InputPort::DO3 => self.tx_pdo.port3,
Wago750_1506InputPort::DO4 => self.tx_pdo.port4,
Wago750_1506InputPort::DO5 => self.tx_pdo.port5,
Wago750_1506InputPort::DO6 => self.tx_pdo.port6,
Wago750_1506InputPort::DO7 => self.tx_pdo.port7,
Wago750_1506InputPort::DO8 => self.tx_pdo.port8,
},
})
}
}
impl DigitalOutputDevice<Wago750_1506OutputPort> for Wago750_1506 {
/// Writes the new output value into the device's RXPDO structure (in-memory PDI).
fn set_output(&mut self, port: Wago750_1506OutputPort, value: DigitalOutputOutput) {
// The DigitalOutputOutput is converted to a bool using the From trait
let output_value: bool = value.into();
match port {
Wago750_1506OutputPort::DO1 => self.rx_pdo.port1 = output_value,
Wago750_1506OutputPort::DO2 => self.rx_pdo.port2 = output_value,
Wago750_1506OutputPort::DO3 => self.rx_pdo.port3 = output_value,
Wago750_1506OutputPort::DO4 => self.rx_pdo.port4 = output_value,
Wago750_1506OutputPort::DO5 => self.rx_pdo.port5 = output_value,
Wago750_1506OutputPort::DO6 => self.rx_pdo.port6 = output_value,
Wago750_1506OutputPort::DO7 => self.rx_pdo.port7 = output_value,
Wago750_1506OutputPort::DO8 => self.rx_pdo.port8 = output_value,
}
}
/// Reads the current output value from the device's RXPDO structure (in-memory PDI).
fn get_output(&self, port: Wago750_1506OutputPort) -> DigitalOutputOutput {
let current_value = match port {
Wago750_1506OutputPort::DO1 => self.rx_pdo.port1,
Wago750_1506OutputPort::DO2 => self.rx_pdo.port2,
Wago750_1506OutputPort::DO3 => self.rx_pdo.port3,
Wago750_1506OutputPort::DO4 => self.rx_pdo.port4,
Wago750_1506OutputPort::DO5 => self.rx_pdo.port5,
Wago750_1506OutputPort::DO6 => self.rx_pdo.port6,
Wago750_1506OutputPort::DO7 => self.rx_pdo.port7,
Wago750_1506OutputPort::DO8 => self.rx_pdo.port8,
};
// Wrap the bool back into the type-safe wrapper
DigitalOutputOutput(current_value)
}
}
#[derive(Clone)]
pub struct Wago750_1506 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
// Should always have on but not when calling constructor
module: Option<Module>,
rx_pdo: Wago750_1506RxPdo,
tx_pdo: Wago750_1506TxPdo,
}
impl DynamicEthercatDevice for Wago750_1506 {}
impl EthercatDynamicPDO for Wago750_1506 {
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 EthercatDeviceUsed for Wago750_1506 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl Wago750_1506 {}
impl EthercatDevice for Wago750_1506 {
// 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_1506InputPort::DO1);
self.tx_pdo.port1 = *input.get(idx1).expect("Bit 1 out of bounds");
let idx2 = base + Into::<usize>::into(Wago750_1506InputPort::DO2);
self.tx_pdo.port2 = *input.get(idx2).expect("Bit 2 out of bounds");
let idx3 = base + Into::<usize>::into(Wago750_1506InputPort::DO3);
self.tx_pdo.port3 = *input.get(idx3).expect("Bit 3 out of bounds");
let idx4 = base + Into::<usize>::into(Wago750_1506InputPort::DO4);
self.tx_pdo.port4 = *input.get(idx4).expect("Bit 4 out of bounds");
let idx5 = base + Into::<usize>::into(Wago750_1506InputPort::DO5);
self.tx_pdo.port5 = *input.get(idx5).expect("Bit 5 out of bounds");
let idx6 = base + Into::<usize>::into(Wago750_1506InputPort::DO6);
self.tx_pdo.port6 = *input.get(idx6).expect("Bit 6 out of bounds");
let idx7 = base + Into::<usize>::into(Wago750_1506InputPort::DO7);
self.tx_pdo.port7 = *input.get(idx7).expect("Bit 7 out of bounds");
let idx8 = base + Into::<usize>::into(Wago750_1506InputPort::DO8);
self.tx_pdo.port8 = *input.get(idx8).expect("Bit 8 out of bounds");
Ok(())
}
fn input_len(&self) -> usize {
8
}
fn output(
&self,
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
) -> Result<(), anyhow::Error> {
// println!("output: {} {}",self.rx_bit_offset,_output);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO1),
self.rx_pdo.port1,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO2),
self.rx_pdo.port2,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO3),
self.rx_pdo.port3,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO4),
self.rx_pdo.port4,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO5),
self.rx_pdo.port5,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO6),
self.rx_pdo.port6,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO7),
self.rx_pdo.port7,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_1506OutputPort::DO8),
self.rx_pdo.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<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_1506 {
fn input_post_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
fn output_pre_process(&mut self) -> Result<(), anyhow::Error> {
Ok(())
}
}
impl NewEthercatDevice for Wago750_1506 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rx_pdo: Wago750_1506RxPdo::default(),
tx_pdo: Wago750_1506TxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_1506 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_1506")
}
}
pub const WAGO_750_1506_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_1506_PRODUCT_ID: u32 = 2147483779;
pub const WAGO_750_1506_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_1506_VENDOR_ID, WAGO_750_1506_PRODUCT_ID);
@@ -0,0 +1,188 @@
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_501 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
pub rxpdo: Wago750_501RxPdo,
module: Option<Module>,
}
#[derive(Debug, Clone)]
pub enum Wago750_501Port {
Port1,
Port2,
}
impl From<Wago750_501Port> for usize {
fn from(value: Wago750_501Port) -> Self {
match value {
Wago750_501Port::Port1 => 0,
Wago750_501Port::Port2 => 1,
}
}
}
#[derive(Clone, Default)]
pub struct Wago750_501RxPdo {
pub port1: bool,
pub port2: bool,
}
impl DigitalOutputDevice<Wago750_501Port> for Wago750_501 {
/// Writes the new output value into the device's RXPDO structure (in-memory PDI).
fn set_output(&mut self, port: Wago750_501Port, value: DigitalOutputOutput) {
// The DigitalOutputOutput is converted to a bool using the From trait
let output_value: bool = value.into();
match port {
Wago750_501Port::Port1 => {
// Modify the internal PDO representation
self.rxpdo.port1 = output_value;
}
Wago750_501Port::Port2 => {
// Modify the internal PDO representation
self.rxpdo.port2 = output_value;
}
}
}
/// Reads the current output value from the device's RXPDO structure (in-memory PDI).
fn get_output(&self, port: Wago750_501Port) -> DigitalOutputOutput {
let current_value = match port {
Wago750_501Port::Port1 => self.rxpdo.port1,
Wago750_501Port::Port2 => self.rxpdo.port2,
};
// Wrap the bool back into the type-safe wrapper
DigitalOutputOutput(current_value)
}
}
impl EthercatDeviceUsed for Wago750_501 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_501 {}
impl EthercatDynamicPDO for Wago750_501 {
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_501 {
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_501Port::Port1),
self.rxpdo.port1,
);
output.set(
self.rx_bit_offset + Into::<usize>::into(Wago750_501Port::Port2),
self.rxpdo.port2,
);
Ok(())
}
fn output_len(&self) -> usize {
2
}
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_501 {}
impl NewEthercatDevice for Wago750_501 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
rxpdo: Wago750_501RxPdo::default(),
}
}
}
impl std::fmt::Debug for Wago750_501 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_501")
}
}
pub const WAGO_750_501_VENDOR_ID: u32 = 0x00000021;
pub const WAGO_750_501_PRODUCT_ID: u32 = 2147483682;
pub const WAGO_750_501_MODULE_IDENT: SubDeviceProductTuple =
(WAGO_750_501_VENDOR_ID, WAGO_750_501_PRODUCT_ID);
@@ -0,0 +1,159 @@
use crate::devices::{
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
SubDeviceProductTuple,
};
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
#[derive(Clone)]
pub struct Wago750_652 {
is_used: bool,
tx_bit_offset: usize,
rx_bit_offset: usize,
module: Option<Module>,
}
impl EthercatDeviceUsed for Wago750_652 {
fn is_used(&self) -> bool {
self.is_used
}
fn set_used(&mut self, used: bool) {
self.is_used = used;
}
}
impl DynamicEthercatDevice for Wago750_652 {}
impl EthercatDynamicPDO for Wago750_652 {
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_652 {
/*
OK so with ethercrab we receive the bitslice of the current subdevice in our Loop
*/
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> {
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> {
// 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)
}
fn output_checked(
&self,
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(())
}
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_652 {}
impl NewEthercatDevice for Wago750_652 {
fn new() -> Self {
Self {
is_used: false,
tx_bit_offset: 0,
rx_bit_offset: 0,
module: None,
}
}
}
impl std::fmt::Debug for Wago750_652 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Wago750_652")
}
}
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 =
(WAGO_750_652_VENDOR_ID, WAGO_750_652_PRODUCT_ID);
+1 -2
View File
@@ -1,7 +1,6 @@
use std::fmt;
use std::sync::Arc;
use futures::executor::block_on;
use physical::{AnalogInputRange, AnalogInputValue};
use smol::lock::RwLock;
@@ -40,7 +39,7 @@ impl AnalogInput {
let get_input = Box::new(move || -> AnalogInputInput {
let device2 = Arc::clone(&device);
let port_clone = port.clone();
block_on(async move {
smol::block_on(async move {
let device = device2.read().await;
device.get_input(port_clone)
})
+1 -2
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Digital Input (DI) device
@@ -27,7 +26,7 @@ impl DigitalInput {
let port2 = port;
let device2 = device.clone();
let get_input = Box::new(move || {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_input(port2.clone())
});
+2 -3
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Digital Output (DO) device
@@ -29,7 +28,7 @@ impl DigitalOutput {
let port1 = port.clone();
let device1 = device.clone();
let set_output = Box::new(move |value: DigitalOutputOutput| {
let mut device = block_on(device1.write());
let mut device = smol::block_on(device1.write());
device.set_output(port1.clone(), value);
});
@@ -37,7 +36,7 @@ impl DigitalOutput {
let port2 = port;
let device2 = device.clone();
let get_output = Box::new(move || -> DigitalOutputOutput {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_output(port2.clone())
});
+4 -5
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Encoder Input device
@@ -34,7 +33,7 @@ impl EncoderInput {
let port_counter = port.clone();
let device_counter = device.clone();
let get_counter = Box::new(move || {
let device = block_on(device_counter.read());
let device = smol::block_on(device_counter.read());
device.get_counter_value(port_counter.clone())
});
@@ -42,7 +41,7 @@ impl EncoderInput {
let port_frequency = port.clone();
let device_frequency = device.clone();
let get_frequency = Box::new(move || {
let device = block_on(device_frequency.read());
let device = smol::block_on(device_frequency.read());
device.get_frequency(port_frequency.clone())
});
@@ -50,7 +49,7 @@ impl EncoderInput {
let port_period = port.clone();
let device_period = device.clone();
let get_period = Box::new(move || {
let device = block_on(device_period.read());
let device = smol::block_on(device_period.read());
device.get_period(port_period.clone())
});
@@ -58,7 +57,7 @@ impl EncoderInput {
let port_set = port;
let device_set = device;
let set_counter = Box::new(move |value: u32| {
let mut device = block_on(device_set.write());
let mut device = smol::block_on(device_set.write());
device.set_counter(port_set.clone(), value)
});
+3 -4
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Pulse Train Output (PTO) device
@@ -29,21 +28,21 @@ impl<'device> PulseTrainOutput {
// build sync write closure
let device1 = device.clone();
let set_output = Box::new(move |value: PulseTrainOutputOutput| {
let mut device = block_on(device1.write());
let mut device = smol::block_on(device1.write());
device.set_output(port, value);
});
// build sync get closure
let device2 = device.clone();
let get_output = Box::new(move || -> PulseTrainOutputOutput {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_output(port)
});
// build sync get closure
let device2 = device;
let get_input = Box::new(move || -> PulseTrainOutputInput {
let device = block_on(device2.read());
let device = smol::block_on(device2.read());
device.get_input(port)
});
@@ -2,7 +2,6 @@ use std::{fmt, sync::Arc};
use crate::helpers::el70xx_velocity_converter::EL70x1VelocityConverter;
use anyhow::Error;
use futures::executor::block_on;
use smol::lock::RwLock;
/// Pulse Train Output (PTO) device
@@ -47,7 +46,7 @@ impl<'device> StepperVelocityEL70x1 {
// build sync get closure
let device2 = device.clone();
let get_input = Box::new(move || -> Result<StepperVelocityEL70x1Input, Error> {
block_on(async {
smol::block_on(async {
let device = device2.read().await;
device.get_input(port)
})
@@ -55,7 +54,7 @@ impl<'device> StepperVelocityEL70x1 {
let device3 = device.clone();
let get_output = Box::new(move || -> Result<StepperVelocityEL70x1Output, Error> {
block_on(async {
smol::block_on(async {
let device = device3.read().await;
device.get_output(port)
})
@@ -65,7 +64,7 @@ impl<'device> StepperVelocityEL70x1 {
let device3 = device;
let get_speed_range = Box::new(
move || -> Result<crate::shared_config::el70x1::EL70x1SpeedRange, Error> {
block_on(async {
smol::block_on(async {
let device = device3.read().await;
Ok(device.get_speed_range(port))
})
+3 -5
View File
@@ -1,4 +1,3 @@
use futures::executor::block_on;
use smol::lock::RwLock;
use crate::pdo::basic::Limit;
@@ -29,10 +28,9 @@ impl TemperatureInput {
let get_input = Box::new(move || {
let device2 = device2.clone();
let port_clone = port2.clone();
block_on(async move {
let device = device2.read().await;
device.get_input(port_clone)
})
let device = device2.read_blocking();
device.get_input(port_clone)
});
Self { get_input }
}
+1
View File
@@ -1,4 +1,5 @@
pub mod coe;
pub mod debugging;
pub mod devices;
pub mod helpers;
pub mod io;
-1
View File
@@ -19,7 +19,6 @@ euclid = "0.22.11"
# concurrency
smol = "2.0.2"
futures = "0.3.31"
# web
serde_json = "1.0.143"
+2 -2
View File
@@ -45,12 +45,12 @@ impl MachineAct for AquaPathV1 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
+2 -2
View File
@@ -33,12 +33,12 @@ impl MachineAct for BufferV1 {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Macine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
+1 -1
View File
@@ -63,7 +63,7 @@ impl MachineAct for ExtruderV2 {
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
+10 -4
View File
@@ -251,8 +251,11 @@ impl ExtruderV2 {
pub fn set_regulation(&mut self, uses_rpm: bool) {
if !self.screw_speed_controller.get_uses_rpm() && uses_rpm {
self.screw_speed_controller
.set_target_screw_rpm(self.screw_speed_controller.target_rpm);
self.screw_speed_controller.set_target_screw_rpm(
self.screw_speed_controller.target_rpm,
AngularVelocity::new::<revolution_per_minute>(1500.0),
4,
);
self.screw_speed_controller.set_uses_rpm(uses_rpm);
}
@@ -270,8 +273,11 @@ impl ExtruderV2 {
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_speed_controller
.set_target_screw_rpm(AngularVelocity::new::<revolution_per_minute>(rpm));
self.screw_speed_controller.set_target_screw_rpm(
AngularVelocity::new::<revolution_per_minute>(rpm),
AngularVelocity::new::<revolution_per_minute>(1500.0),
4,
);
self.emit_state();
}
@@ -443,7 +443,6 @@ impl MitsubishiCS80 {
pub fn set_frequency_target(&mut self, frequency: Frequency) {
let mut request: MitsubishiCS80Request =
MitsubishiCS80Requests::WriteRunningFrequency.into();
let result = self.convert_frequency_to_word(frequency);
let bytes = result.to_le_bytes();
request.request.data[2] = bytes[1];
+9 -4
View File
@@ -59,14 +59,12 @@ use super::{
impl MachineNewTrait for ExtruderV2 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
use crate::{
MachineNewHardware, MachineNewHardwareEthercat, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
let device_identification = params.device_group.to_vec();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
@@ -85,6 +83,8 @@ impl MachineNewTrait for ExtruderV2 {
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// Role 0 - Buscoupler EK1100
use control_core::transmission::fixed::FixedTransmission;
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
@@ -214,8 +214,13 @@ impl MachineNewTrait for ExtruderV2 {
let target_pressure = Pressure::new::<bar>(0.0);
let target_rpm = AngularVelocity::new::<revolution_per_minute>(0.0);
let screw_speed_controller =
ScrewSpeedController::new(inverter, target_pressure, target_rpm, pressure_sensor);
let screw_speed_controller = ScrewSpeedController::new(
inverter,
target_pressure,
target_rpm,
pressure_sensor,
FixedTransmission::new(1.0 / 34.0),
);
let (sender, receiver) = smol::channel::unbounded();
let mut extruder: ExtruderV2 = Self {
@@ -6,7 +6,6 @@ use control_core::{
transmission::{Transmission, fixed::FixedTransmission},
};
use ethercat_hal::io::analog_input::AnalogInput;
use futures::executor::block_on;
use units::angular_velocity::revolution_per_minute;
use units::electric_current::milliampere;
use units::f64::*;
@@ -42,6 +41,7 @@ impl ScrewSpeedController {
target_pressure: Pressure,
target_rpm: AngularVelocity,
pressure_sensor: AnalogInput,
transmission: FixedTransmission,
) -> Self {
let now = Instant::now();
Self {
@@ -54,7 +54,8 @@ impl ScrewSpeedController {
pressure_sensor,
uses_rpm: true,
forward_rotation: true,
transmission: FixedTransmission::new(1.0 / 34.0),
transmission: transmission,
//FixedTransmission::new(1.0 / 34.0),
motor_on: false,
nozzle_pressure_limit: Pressure::new::<bar>(100.0),
nozzle_pressure_limit_enabled: true,
@@ -108,15 +109,22 @@ impl ScrewSpeedController {
self.target_pressure = target_pressure;
}
pub fn set_target_screw_rpm(&mut self, target_rpm: AngularVelocity) {
pub fn set_target_screw_rpm(
&mut self,
target_rpm: AngularVelocity,
_motor_rpm_rating: AngularVelocity,
motor_poles: usize,
) {
// Use uom here and perhaps clamp it
let target_motor_rpm = self
.transmission
.calculate_angular_velocity_input(target_rpm);
self.target_rpm = target_rpm;
let target_frequency =
Frequency::new::<cycle_per_minute>(target_motor_rpm.get::<revolution_per_minute>());
let target_frequency: Frequency = Frequency::new::<hertz>(
target_motor_rpm.get::<revolution_per_minute>() as f64 / 120.0 * motor_poles as f64,
);
self.inverter.set_frequency_target(target_frequency);
}
@@ -206,15 +214,8 @@ impl ScrewSpeedController {
pub fn update(&mut self, now: Instant, is_extruding: bool) {
// TODO: move this logic elsewhere or make non async
block_on(self.inverter.act(now));
let wiring_error = self.get_wiring_error();
if wiring_error {
// emit in act
}
smol::block_on(self.inverter.act(now));
let measured_pressure = self.get_pressure();
if !self.uses_rpm && !is_extruding && self.motor_on {
let frequency = Frequency::new::<hertz>(0.0);
self.inverter.set_frequency_target(frequency);
+76
View File
@@ -0,0 +1,76 @@
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineAct, MachineMessage};
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for ExtruderV3 {
fn act(&mut self, now: Instant) {
use std::time::Duration;
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
self.temperature_controller_back.update(now);
self.temperature_controller_nozzle.update(now);
self.temperature_controller_front.update(now);
self.temperature_controller_middle.update(now);
if self.mode == super::ExtruderV3Mode::Extrude {
self.screw_speed_controller.update(now, true);
} else {
self.screw_speed_controller.update(now, false);
}
if self.mode == super::ExtruderV3Mode::Standby {
self.turn_heating_off();
}
if self.mode == super::ExtruderV3Mode::Extrude
&& !self.screw_speed_controller.get_motor_enabled()
{
self.switch_to_heat();
}
let now = Instant::now();
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{
()
}
}
}
}
+240
View File
@@ -0,0 +1,240 @@
use std::sync::Arc;
use crate::extruder1::{
api::{
ExtruderSettingsState, HeatingStates, InverterStatusState, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
},
mitsubishi_cs80::MotorStatus,
};
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use serde_json::Value;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use tracing::instrument;
use units::{
angular_velocity::revolution_per_minute, electric_current::ampere, electric_potential::volt,
frequency::hertz,
};
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
use super::ExtruderV3Mode;
#[derive(Debug, Clone, Default, Serialize)]
pub struct MotorStatusValues {
pub screw_rpm: f64, // rpm of motor
pub frequency: f64, // frequency of motor
pub voltage: f64, // volt used for motor
pub current: f64, // current used for the motor
pub power: f64, // power in watts
}
impl From<MotorStatus> for MotorStatusValues {
fn from(status: MotorStatus) -> Self {
let voltage = status.voltage.get::<volt>();
let current = status.current.get::<ampere>();
Self {
screw_rpm: status.rpm.get::<revolution_per_minute>(),
frequency: status.frequency.get::<hertz>(),
voltage,
current,
power: voltage * current,
}
}
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
/// screw rpm
pub motor_status: MotorStatusValues,
/// pressure in bar
pub pressure: f64,
/// nozzle temperature in celsius
pub nozzle_temperature: f64,
/// front temperature in celsius
pub front_temperature: f64,
/// back temperature in celsius
pub back_temperature: f64,
/// middle temperature in celsius
pub middle_temperature: f64,
/// nozzle heating power in watts
pub nozzle_power: f64,
/// front heating power in watts
pub front_power: f64,
/// back heating power in watts
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
/// total energy consumption in kWh
pub total_energy_kwh: f64,
}
impl LiveValuesEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, PartialEq, BuildEvent)]
pub struct StateEvent {
pub is_default_state: bool,
/// rotation state
pub rotation_state: RotationState,
/// mode state
pub mode_state: ModeState,
/// regulation state
pub regulation_state: RegulationState,
/// pressure state
pub pressure_state: PressureState,
/// screw state
pub screw_state: ScrewState,
/// heating states
pub heating_states: HeatingStates,
/// extruder settings state
pub extruder_settings_state: ExtruderSettingsState,
/// inverter status state
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ModeState {
pub mode: ExtruderV3Mode,
}
pub enum ExtruderV3Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
#[derive(Deserialize, Serialize)]
pub enum Mutation {
/// INVERTER
/// Frequency Control
// Set Rotation also starts the motor
SetInverterRotationDirection(bool),
SetInverterTargetPressure(f64),
SetInverterTargetRpm(f64),
SetInverterRegulation(bool),
//Mode
SetExtruderMode(ExtruderV3Mode),
SetFrontHeatingTargetTemperature(f64),
SetBackHeatingTargetTemperature(f64),
SetMiddleHeatingTemperature(f64),
SetNozzleHeatingTemperature(f64),
// SetPressure
SetExtruderPressureLimit(f64),
SetExtruderPressureLimitIsEnabled(bool),
// Pid Configure
SetPressurePidSettings(PidSettings),
SetTemperaturePidSettings(TemperaturePid),
// Reset
ResetInverter(bool),
}
#[derive(Debug)]
pub struct ExtruderV3Namespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<ExtruderV3Events> for ExtruderV3Namespace {
#[instrument(skip_all)]
fn emit(&mut self, events: ExtruderV3Events) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
impl CacheableEvents<Self> for ExtruderV3Events {
fn event_value(&self) -> GenericEvent {
match self {
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for ExtruderV3 {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
use crate::extruder1::HeatingType;
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetExtruderMode(mode) => self.set_mode_state(mode),
Mutation::SetInverterRotationDirection(forward) => self.set_rotation_state(forward),
Mutation::SetInverterRegulation(uses_rpm) => self.set_regulation(uses_rpm),
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
Mutation::SetInverterTargetRpm(rpm) => self.set_target_rpm(rpm),
Mutation::ResetInverter(_) => self.reset_inverter(),
Mutation::SetFrontHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Front)
}
Mutation::SetMiddleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Middle)
}
Mutation::SetBackHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
}
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+355
View File
@@ -0,0 +1,355 @@
#[cfg(not(feature = "mock-machine"))]
// Contains Implementations for All functions that use emit_state
use crate::extruder1::{
HeatingType,
api::{
ExtruderSettingsState, HeatingState, HeatingStates, InverterStatusState, PidSettings,
PidSettingsStates, PressureState, RegulationState, RotationState, ScrewState,
TemperaturePid,
},
};
#[cfg(not(feature = "mock-machine"))]
use control_core::helpers::hasher_serializer::hash_with_serde_model;
#[cfg(not(feature = "mock-machine"))]
use control_core::socketio::event::BuildEvent;
#[cfg(not(feature = "mock-machine"))]
use control_core::socketio::namespace::NamespaceCacheingLogic;
#[cfg(not(feature = "mock-machine"))]
use units::angular_velocity::AngularVelocity;
#[cfg(not(feature = "mock-machine"))]
use units::pressure::{Pressure, bar};
#[cfg(not(feature = "mock-machine"))]
use units::thermodynamic_temperature::ThermodynamicTemperature;
#[cfg(not(feature = "mock-machine"))]
use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use super::{ExtruderV3, ExtruderV3Mode, api::StateEvent};
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
pub fn build_state_event(&mut self) -> StateEvent {
use crate::{
extruder1::api::{TemperaturePid, TemperaturePidStates},
extruder2::api::ModeState,
};
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: RotationState {
forward: self.screw_speed_controller.get_rotation_direction(),
},
mode_state: ModeState {
mode: self.mode.clone(),
},
regulation_state: RegulationState {
uses_rpm: self.screw_speed_controller.get_uses_rpm(),
},
pressure_state: PressureState {
target_bar: self
.screw_speed_controller
.get_target_pressure()
.get::<bar>(),
wiring_error: self.screw_speed_controller.get_wiring_error(),
},
screw_state: ScrewState {
target_rpm: self
.screw_speed_controller
.get_target_rpm()
.get::<revolution_per_minute>(),
},
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: self
.temperature_controller_nozzle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_nozzle.heating.wiring_error,
},
front: HeatingState {
target_temperature: self
.temperature_controller_front
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_front.heating.wiring_error,
},
back: HeatingState {
target_temperature: self
.temperature_controller_back
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_back.heating.wiring_error,
},
middle: HeatingState {
target_temperature: self
.temperature_controller_middle
.heating
.target_temperature
.get::<degree_celsius>(),
wiring_error: self.temperature_controller_middle.heating.wiring_error,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: self
.screw_speed_controller
.get_nozzle_pressure_limit()
.get::<bar>(),
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
forward_running: self.screw_speed_controller.inverter.status.forward_running,
reverse_running: self.screw_speed_controller.inverter.status.reverse_running,
up_to_frequency: self.screw_speed_controller.inverter.status.su,
overload_warning: self.screw_speed_controller.inverter.status.ol,
no_function: self.screw_speed_controller.inverter.status.no_function,
output_frequency_detection: self.screw_speed_controller.inverter.status.fu,
abc_fault: self.screw_speed_controller.inverter.status.abc_,
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
},
pid_settings: PidSettingsStates {
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: self.temperature_controller_front.pid.get_ki(),
kp: self.temperature_controller_front.pid.get_kp(),
kd: self.temperature_controller_front.pid.get_kd(),
zone: String::from("front"),
},
middle: TemperaturePid {
ki: self.temperature_controller_middle.pid.get_ki(),
kp: self.temperature_controller_middle.pid.get_kp(),
kd: self.temperature_controller_middle.pid.get_kd(),
zone: String::from("middle"),
},
back: TemperaturePid {
ki: self.temperature_controller_back.pid.get_ki(),
kp: self.temperature_controller_back.pid.get_kp(),
kd: self.temperature_controller_back.pid.get_kd(),
zone: String::from("back"),
},
nozzle: TemperaturePid {
ki: self.temperature_controller_nozzle.pid.get_ki(),
kp: self.temperature_controller_nozzle.pid.get_kp(),
kd: self.temperature_controller_nozzle.pid.get_kd(),
zone: String::from("nozzle"),
},
},
pressure: PidSettings {
ki: self.screw_speed_controller.pid.get_ki(),
kp: self.screw_speed_controller.pid.get_kp(),
kd: self.screw_speed_controller.pid.get_kd(),
},
},
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
pub fn emit_state(&mut self) {
use super::api::ExtruderV3Events;
let state = self.build_state_event();
let hash = hash_with_serde_model(self.screw_speed_controller.get_inverter_status());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV3Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let status = self.screw_speed_controller.get_inverter_status();
let new_status_hash = hash_with_serde_model(status);
if new_status_hash != old_status_hash {
self.emit_state();
}
}
pub fn emit_live_values(&mut self) {
use std::time::Instant;
use crate::extruder2::api::{ExtruderV3Events, LiveValuesEvent};
let now = Instant::now();
let combined_power = self.calculate_combined_power();
self.update_total_energy(combined_power, now);
let live_values = LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
nozzle_temperature: self
.temperature_controller_nozzle
.heating
.temperature
.get::<degree_celsius>(),
front_temperature: self
.temperature_controller_front
.heating
.temperature
.get::<degree_celsius>(),
back_temperature: self
.temperature_controller_back
.heating
.temperature
.get::<degree_celsius>(),
middle_temperature: self
.temperature_controller_middle
.heating
.temperature
.get::<degree_celsius>(),
nozzle_power: self
.temperature_controller_nozzle
.get_heating_element_wattage(),
front_power: self
.temperature_controller_front
.get_heating_element_wattage(),
back_power: self
.temperature_controller_back
.get_heating_element_wattage(),
middle_power: self
.temperature_controller_middle
.get_heating_element_wattage(),
combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
.set_nozzle_pressure_limit_is_enabled(enabled);
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.screw_speed_controller
.set_nozzle_pressure_limit(Pressure::new::<bar>(pressure));
self.emit_state();
}
pub fn enable_heating(&mut self) {
self.temperature_controller_back.allow_heating();
self.temperature_controller_front.allow_heating();
self.temperature_controller_middle.allow_heating();
self.temperature_controller_nozzle.allow_heating();
self.emit_state();
}
pub fn set_rotation_state(&mut self, forward: bool) {
self.screw_speed_controller.set_rotation_direction(forward);
self.emit_state();
}
pub fn set_mode_state(&mut self, mode: ExtruderV3Mode) {
self.switch_mode(mode);
self.emit_state();
}
pub fn set_regulation(&mut self, uses_rpm: bool) {
if !self.screw_speed_controller.get_uses_rpm() && uses_rpm {
self.screw_speed_controller.set_target_screw_rpm(
self.screw_speed_controller.target_rpm,
AngularVelocity::new::<revolution_per_minute>(3000.0),
2,
);
self.screw_speed_controller.set_uses_rpm(uses_rpm);
}
if self.screw_speed_controller.get_uses_rpm() && !uses_rpm {
self.screw_speed_controller.set_uses_rpm(uses_rpm);
self.screw_speed_controller.start_pressure_regulation();
}
self.emit_state();
}
pub fn set_target_pressure(&mut self, pressure: f64) {
self.screw_speed_controller
.set_target_pressure(Pressure::new::<bar>(pressure));
self.emit_state();
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_speed_controller.set_target_screw_rpm(
AngularVelocity::new::<revolution_per_minute>(rpm),
AngularVelocity::new::<revolution_per_minute>(3000.0),
2,
);
self.emit_state();
}
pub fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
let target_temp = ThermodynamicTemperature::new::<degree_celsius>(target_temperature);
match heating_type {
HeatingType::Nozzle => self
.temperature_controller_nozzle
.set_target_temperature(target_temp),
HeatingType::Front => self
.temperature_controller_front
.set_target_temperature(target_temp),
HeatingType::Back => self
.temperature_controller_back
.set_target_temperature(target_temp),
HeatingType::Middle => self
.temperature_controller_middle
.set_target_temperature(target_temp),
}
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.screw_speed_controller
.pid
.configure(settings.ki, settings.kp, settings.kd);
self.emit_state();
}
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
match settings.zone.as_str() {
"front" => {
self.temperature_controller_front.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"middle" => {
self.temperature_controller_middle.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"back" => {
self.temperature_controller_back.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"nozzle" => {
self.temperature_controller_nozzle.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
}
}
+45
View File
@@ -0,0 +1,45 @@
use super::ExtruderV2;
use crate::MachineAct;
use crate::MachineMessage;
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use super::ExtruderV2;
use crate::{
MachineApi,
extruder1::{HeatingType, api::Mutation},
};
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetExtruderMode(mode) => self.set_mode_state(mode),
Mutation::SetInverterRotationDirection(forward) => self.set_rotation_state(forward),
Mutation::SetInverterRegulation(uses_rpm) => self.set_regulation(uses_rpm),
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
Mutation::SetInverterTargetRpm(rpm) => self.set_target_rpm(rpm),
Mutation::ResetInverter(_) => (),
Mutation::SetFrontHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Front)
}
Mutation::SetMiddleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Middle)
}
Mutation::SetBackHeatingTargetTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
}
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
}
Ok(())
}
fn api_event_namespace(
&mut self,
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
}
+164
View File
@@ -0,0 +1,164 @@
use crate::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
mock::ExtruderV2,
};
use control_core::{
helpers::hasher_serializer::hash_with_serde_model,
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
};
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
// bad performance wise, but doesnt matter its only a mock machine
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: self.rotation_state.clone(),
mode_state: self.mode_state.clone(),
regulation_state: self.regulation_state.clone(),
pressure_state: self.pressure_state.clone(),
screw_state: self.screw_state.clone(),
heating_states: self.heating_states.clone(),
extruder_settings_state: self.extruder_settings_state.clone(),
inverter_status_state: self.inverter_status_state.clone(),
pid_settings: self.pid_settings.clone(),
}
}
}
impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let hash = hash_with_serde_model(self.inverter_status_state.clone());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let status = self.inverter_status_state.clone();
let new_status_hash = hash_with_serde_model(status);
if new_status_hash != old_status_hash {
self.emit_state();
}
}
pub fn emit_live_values(&mut self) {
let live_values = LiveValuesEvent {
motor_status: self.motor_status.clone(),
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
front_temperature: self.front_temperature,
back_temperature: self.back_temperature,
middle_temperature: self.middle_temperature,
nozzle_power: self.nozzle_power,
front_power: self.front_power,
back_power: self.back_power,
middle_power: self.middle_power,
combined_power: self.combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.extruder_settings_state.pressure_limit_enabled = enabled;
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.extruder_settings_state.pressure_limit = pressure;
self.emit_state();
}
pub fn enable_heating(&mut self) {
self.back_heating_allowed = true;
self.front_heating_allowed = true;
self.nozzle_heating_allowed = true;
self.back_heating_allowed = true;
self.emit_state();
}
pub fn set_rotation_state(&mut self, forward: bool) {
self.rotation_state.forward = forward;
self.emit_state();
}
pub fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.mode_state = ModeState { mode };
self.emit_state();
}
pub fn set_regulation(&mut self, uses_rpm: bool) {
self.regulation_state.uses_rpm = uses_rpm;
self.emit_state();
}
pub fn set_target_pressure(&mut self, pressure: f64) {
self.target_pressure = pressure;
self.emit_state();
}
pub fn set_target_rpm(&mut self, rpm: f64) {
self.screw_state.target_rpm = rpm;
self.emit_state();
}
pub fn set_target_temperature(&mut self, target_temperature: f64, heating_type: HeatingType) {
match heating_type {
HeatingType::Nozzle => {
self.heating_states.nozzle.target_temperature = target_temperature
}
HeatingType::Front => self.heating_states.front.target_temperature = target_temperature,
HeatingType::Back => self.heating_states.back.target_temperature = target_temperature,
HeatingType::Middle => {
self.heating_states.middle.target_temperature = target_temperature
}
}
self.emit_state();
}
pub fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.pid_settings.pressure.ki = settings.ki;
self.pid_settings.pressure.kp = settings.kp;
self.pid_settings.pressure.kd = settings.kd;
self.emit_state();
}
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
match settings.zone.as_str() {
"front" => {
self.pid_settings.temperature.front.ki = settings.ki;
self.pid_settings.temperature.front.kp = settings.kp;
self.pid_settings.temperature.front.kd = settings.kd;
}
"middle" => {
self.pid_settings.temperature.middle.ki = settings.ki;
self.pid_settings.temperature.middle.kp = settings.kp;
self.pid_settings.temperature.middle.kd = settings.kd;
}
"back" => {
self.pid_settings.temperature.back.ki = settings.ki;
self.pid_settings.temperature.back.kp = settings.kp;
self.pid_settings.temperature.back.kd = settings.kd;
}
"nozzle" => {
self.pid_settings.temperature.nozzle.ki = settings.ki;
self.pid_settings.temperature.nozzle.kp = settings.kp;
self.pid_settings.temperature.nozzle.kd = settings.kd;
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
}
}
+129
View File
@@ -0,0 +1,129 @@
#[cfg(feature = "mock-machine")]
use smol::channel::Receiver;
#[cfg(feature = "mock-machine")]
use smol::channel::Sender;
#[cfg(feature = "mock-machine")]
use crate::{AsyncThreadMessage, Machine};
#[cfg(feature = "mock-machine")]
use crate::{
MachineIdentificationUnique, MachineMessage, machine_identification::MachineIdentification,
};
#[cfg(feature = "mock-machine")]
use std::time::Instant;
#[cfg(feature = "mock-machine")]
use crate::{
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingStates, InverterStatusState,
ModeState, MotorStatusValues, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState,
},
},
};
// Just checking mock-machine feature here to exclude these modules from compilation entirely
#[cfg(feature = "mock-machine")]
pub mod act;
#[cfg(feature = "mock-machine")]
pub mod api;
#[cfg(feature = "mock-machine")]
pub mod mock_emit;
#[cfg(feature = "mock-machine")]
pub mod new;
#[cfg(feature = "mock-machine")]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(feature = "mock-machine")]
#[derive(Debug)]
pub struct ExtruderV2 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
/// Energy tracking for total consumption calculation
pub total_energy_kwh: f64,
pub last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
pub target_pressure: f64,
pub is_default_state: bool,
/// rotation state
pub rotation_state: RotationState,
/// mode state
pub mode_state: ModeState,
/// regulation state
pub regulation_state: RegulationState,
/// pressure state
pub pressure_state: PressureState,
/// screw state
pub screw_state: ScrewState,
/// heating states
pub heating_states: HeatingStates,
/// extruder settings state
pub extruder_settings_state: ExtruderSettingsState,
/// inverter status state
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
pub motor_status: MotorStatusValues,
/// pressure in bar
pub pressure: f64,
/// nozzle temperature in celsius
pub nozzle_temperature: f64,
/// front temperature in celsius
pub front_temperature: f64,
/// back temperature in celsius
pub back_temperature: f64,
/// middle temperature in celsius
pub middle_temperature: f64,
/// nozzle heating power in watts
pub nozzle_power: f64,
/// front heating power in watts
pub front_power: f64,
/// back heating power in watts
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
pub nozzle_heating_allowed: bool,
pub front_heating_allowed: bool,
pub back_heating_allowed: bool,
pub middle_heating_allowed: bool,
}
#[cfg(feature = "mock-machine")]
impl ExtruderV2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_EXTRUDER_V1,
};
}
+157
View File
@@ -0,0 +1,157 @@
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
},
mock::ExtruderV2,
},
};
impl MachineNewTrait for ExtruderV2 {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
// Mock machine can work with either Serial or Ethercat hardware
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let (sender, receiver) = smol::channel::unbounded();
let mut extruder_mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
mode: ExtruderV2Mode::Standby, // Start in standby mode
emitted_default_state: false,
last_status_hash: None,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
is_default_state: false,
rotation_state: RotationState { forward: false },
mode_state: ModeState {
mode: ExtruderV2Mode::Standby,
},
regulation_state: RegulationState { uses_rpm: true },
pressure_state: PressureState {
target_bar: 0.0,
wiring_error: false,
},
screw_state: ScrewState { target_rpm: 0.0 },
heating_states: HeatingStates {
nozzle: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
front: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
back: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
middle: HeatingState {
target_temperature: 0.0,
wiring_error: false,
},
},
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
},
inverter_status_state: InverterStatusState {
running: false,
forward_running: true,
reverse_running: true,
up_to_frequency: false,
overload_warning: false,
no_function: false,
output_frequency_detection: false,
abc_fault: false,
fault_occurence: false,
},
pid_settings: PidSettingsStates {
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("front"),
},
middle: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("middle"),
},
back: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("back"),
},
nozzle: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("nozzle"),
},
},
pressure: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
},
},
motor_status: MotorStatusValues {
screw_rpm: 0.0,
frequency: 0.0,
voltage: 0.0,
current: 0.0,
power: 0.0,
},
pressure: 0.0,
nozzle_temperature: 0.0,
front_temperature: 0.0,
back_temperature: 0.0,
middle_temperature: 0.0,
nozzle_power: 0.0,
front_power: 0.0,
back_power: 0.0,
middle_power: 0.0,
combined_power: 0.0,
nozzle_heating_allowed: false,
front_heating_allowed: false,
back_heating_allowed: false,
middle_heating_allowed: false,
target_pressure: 0.0,
};
extruder_mock_machine.emit_state();
Ok(extruder_mock_machine)
}
}
+215
View File
@@ -0,0 +1,215 @@
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use api::ExtruderV3Namespace;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Receiver;
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use units::electric_current::ampere;
#[cfg(not(feature = "mock-machine"))]
use units::electric_potential::volt;
#[cfg(not(feature = "mock-machine"))]
use crate::MACHINE_EXTRUDER_V2;
use crate::{AsyncThreadMessage, Machine};
#[cfg(not(feature = "mock-machine"))]
use crate::{
MachineMessage, VENDOR_QITECH,
extruder1::{
screw_speed_controller::ScrewSpeedController, temperature_controller::TemperatureController,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
pub mod act;
pub mod api;
pub mod emit;
pub mod mock;
pub mod new;
pub mod temperature_controller;
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub enum ExtruderV3Mode {
Standby,
Heat,
Extrude,
}
pub enum HeatingType {
Nozzle,
Front,
Back,
Middle,
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug)]
pub struct ExtruderV3 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV3Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV3Mode,
screw_speed_controller: ScrewSpeedController,
temperature_controller_front: TemperatureController,
temperature_controller_middle: TemperatureController,
temperature_controller_back: TemperatureController,
temperature_controller_nozzle: TemperatureController,
/// Energy tracking for total consumption calculation
total_energy_kwh: f64,
last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl Machine for ExtruderV3 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for ExtruderV3 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ExtruderV3")
}
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_EXTRUDER_V2,
};
}
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV3 {
/// Calculate combined power consumption in watts
fn calculate_combined_power(&mut self) -> f64 {
let motor_power = {
let motor_status = &self.screw_speed_controller.inverter.motor_status;
let voltage = motor_status.voltage.get::<volt>();
let current = motor_status.current.get::<ampere>();
voltage * current
};
let nozzle_power = self
.temperature_controller_nozzle
.get_heating_element_wattage();
let front_power = self
.temperature_controller_front
.get_heating_element_wattage();
let back_power = self
.temperature_controller_back
.get_heating_element_wattage();
let middle_power = self
.temperature_controller_middle
.get_heating_element_wattage();
motor_power + nozzle_power + front_power + back_power + middle_power
}
/// Update total energy consumption in kWh
fn update_total_energy(&mut self, current_power_watts: f64, now: Instant) {
if let Some(last_time) = self.last_energy_calculation_time {
let time_delta_hours = now.duration_since(last_time).as_secs_f64() / 3600.0;
let energy_delta_kwh = (current_power_watts / 1000.0) * time_delta_hours;
self.total_energy_kwh += energy_delta_kwh;
}
self.last_energy_calculation_time = Some(now);
}
// Funktionen ohne emit_state bleiben hier
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
fn turn_heating_off(&mut self) {
self.temperature_controller_back.disable();
self.temperature_controller_front.disable();
self.temperature_controller_middle.disable();
self.temperature_controller_nozzle.disable();
}
fn switch_to_standby(&mut self) {
match self.mode {
ExtruderV3Mode::Standby => (),
ExtruderV3Mode::Heat => {
self.turn_heating_off();
self.screw_speed_controller.reset_pid();
}
ExtruderV3Mode::Extrude => {
self.turn_heating_off();
self.screw_speed_controller.turn_motor_off();
self.screw_speed_controller.reset_pid();
}
};
self.mode = ExtruderV3Mode::Standby;
}
fn switch_to_heat(&mut self) {
match self.mode {
ExtruderV3Mode::Standby => self.enable_heating(),
ExtruderV3Mode::Heat => (),
ExtruderV3Mode::Extrude => {
self.screw_speed_controller.turn_motor_off();
self.screw_speed_controller.reset_pid();
}
}
self.mode = ExtruderV3Mode::Heat;
}
fn switch_to_extrude(&mut self) {
match self.mode {
ExtruderV3Mode::Standby => {
self.screw_speed_controller.turn_motor_on();
self.enable_heating();
self.screw_speed_controller.reset_pid();
}
ExtruderV3Mode::Heat => {
self.screw_speed_controller.turn_motor_on();
self.enable_heating();
self.screw_speed_controller.reset_pid();
}
ExtruderV3Mode::Extrude => (),
}
self.mode = ExtruderV3Mode::Extrude;
}
fn switch_mode(&mut self, mode: ExtruderV3Mode) {
if self.mode == mode {
return;
}
match mode {
ExtruderV3Mode::Standby => self.switch_to_standby(),
ExtruderV3Mode::Heat => self.switch_to_heat(),
ExtruderV3Mode::Extrude => self.switch_to_extrude(),
}
}
fn reset_inverter(&mut self) {
self.screw_speed_controller.inverter.reset_inverter();
}
}
+249
View File
@@ -0,0 +1,249 @@
#[cfg(not(feature = "mock-machine"))]
use crate::{MachineNewParams, MachineNewTrait, get_ethercat_device};
#[cfg(not(feature = "mock-machine"))]
use anyhow::Error;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::coe::ConfigurableDevice;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::devices::{
EthercatDeviceUsed,
ek1100::EK1100,
ek1100::EK1100_IDENTITY_A,
el6021::EL6021,
el6021::EL6021Configuration,
el6021::EL6021Port,
el6021::{EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D},
};
#[cfg(not(feature = "mock-machine"))]
use std::time::Duration;
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use units::angular_velocity::AngularVelocity;
#[cfg(not(feature = "mock-machine"))]
use units::angular_velocity::revolution_per_minute;
#[cfg(not(feature = "mock-machine"))]
use units::pressure::Pressure;
#[cfg(not(feature = "mock-machine"))]
use units::pressure::bar;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::{
devices::{
el2004::{EL2004, EL2004_IDENTITY_A, EL2004Port},
el3021::{EL3021, EL3021_IDENTITY_A, EL3021Port},
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
},
io::{
analog_input::AnalogInput, digital_output::DigitalOutput,
serial_interface::SerialInterface, temperature_input::TemperatureInput,
},
};
#[cfg(not(feature = "mock-machine"))]
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use crate::extruder1::temperature_controller::TemperatureController;
#[cfg(not(feature = "mock-machine"))]
use super::ExtruderV3;
#[cfg(not(feature = "mock-machine"))]
impl MachineNewTrait for ExtruderV3 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
use crate::{
MachineNewHardware, MachineNewHardwareEthercat, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
let device_identification = params.device_group.to_vec();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware: &&MachineNewHardwareEthercat<'_, '_, '_> = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
// using block_on because making this funciton async creates a lifetime issue
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// Role 0 - Buscoupler EK1100
use control_core::transmission::fixed::FixedTransmission;
use crate::{
extruder1::{
Heating, mitsubishi_cs80::MitsubishiCS80,
screw_speed_controller::ScrewSpeedController,
},
extruder2::{ExtruderV3Mode, api::ExtruderV3Namespace},
};
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
let el6021 = {
let identities = [
EL6021_IDENTITY_A,
EL6021_IDENTITY_B,
EL6021_IDENTITY_C,
EL6021_IDENTITY_D,
]
.to_vec();
let device = get_ethercat_device::<EL6021>(hardware, params, 1, identities).await?;
device
.0
.write()
.await
.write_config(&device.1, &EL6021Configuration::default())
.await?;
{
let mut device_guard = device.0.write().await;
device_guard.set_used(true);
}
device.0
};
let el2004 =
get_ethercat_device::<EL2004>(hardware, params, 2, [EL2004_IDENTITY_A].to_vec())
.await?
.0;
let el3021 =
get_ethercat_device::<EL3021>(hardware, params, 3, [EL3021_IDENTITY_A].to_vec())
.await?
.0;
let el3204 = get_ethercat_device::<EL3204>(
hardware,
params,
4,
[EL3204_IDENTITY_A, EL3204_IDENTITY_B].to_vec(),
)
.await?
.0;
let t1 = TemperatureInput::new(el3204.clone(), EL3204Port::T1);
let t2 = TemperatureInput::new(el3204.clone(), EL3204Port::T2);
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t4 = TemperatureInput::new(el3204, EL3204Port::T4);
// For the Relais
let digital_out_1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let digital_out_2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let digital_out_3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let digital_out_4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
// The Extruders temparature Controllers should disable the relais when the max_temperature is reached
let extruder_max_temperature = ThermodynamicTemperature::new::<degree_celsius>(300.0);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_front = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t1,
digital_out_1,
Heating::default(),
Duration::from_millis(500),
700.0,
1.0,
);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_middle = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t2,
digital_out_2,
Heating::default(),
Duration::from_millis(500),
700.0,
1.0,
);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_back = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t3,
digital_out_3,
Heating::default(),
Duration::from_millis(500),
700.0,
1.0,
);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_nozzle = TemperatureController::new(
0.16,
0.0,
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t4,
digital_out_4,
Heating::default(),
Duration::from_millis(500),
200.0,
0.95,
);
let inverter = MitsubishiCS80::new(SerialInterface::new(el6021, EL6021Port::SI1));
let target_pressure = Pressure::new::<bar>(0.0);
let target_rpm = AngularVelocity::new::<revolution_per_minute>(0.0);
let screw_speed_controller = ScrewSpeedController::new(
inverter,
target_pressure,
target_rpm,
pressure_sensor,
FixedTransmission::new(1.0 / 30.0),
);
let (sender, receiver) = smol::channel::unbounded();
let mut extruder: ExtruderV3 = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV3Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: Instant::now(),
mode: ExtruderV3Mode::Standby,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
temperature_controller_front,
temperature_controller_middle,
temperature_controller_back,
temperature_controller_nozzle,
screw_speed_controller,
emitted_default_state: false,
last_status_hash: None,
};
extruder.emit_state();
Ok(extruder)
})
}
}
@@ -0,0 +1,120 @@
use crate::extruder1::Heating;
use control_core::controllers::pid::PidController;
use ethercat_hal::io::{digital_output::DigitalOutput, temperature_input::TemperatureInput};
use std::time::{Duration, Instant};
use units::f64::*;
use units::thermodynamic_temperature::degree_celsius;
#[derive(Debug)]
pub struct TemperatureController {
pub pid: PidController,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
pub heating: Heating,
pub target_temp: ThermodynamicTemperature,
window_start: Instant,
heating_allowed: bool,
pwm_period: Duration,
max_temperature: ThermodynamicTemperature,
temperature_pid_output: f64,
heating_element_wattage: f64,
max_clamp: f64,
}
impl TemperatureController {
pub fn disable(&mut self) {
self.relais.set(false);
self.heating.heating = false;
self.disallow_heating();
}
pub fn new(
kp: f64,
ki: f64,
kd: f64,
target_temp: ThermodynamicTemperature,
max_temperature: ThermodynamicTemperature,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
heating: Heating,
pwm_duration: Duration,
heating_element_wattage: f64,
max_clamp: f64,
) -> Self {
Self {
pid: PidController::new(kp, ki, kd),
target_temp,
window_start: Instant::now(),
temperature_sensor,
relais,
heating,
heating_allowed: false,
pwm_period: pwm_duration,
max_temperature,
temperature_pid_output: 0.0,
heating_element_wattage,
max_clamp,
}
}
pub fn set_target_temperature(&mut self, temp: ThermodynamicTemperature) {
self.heating.target_temperature = temp;
}
pub const fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
pub const fn allow_heating(&mut self) {
self.heating_allowed = true;
}
pub fn get_heating_element_wattage(&mut self) -> f64 {
self.temperature_pid_output * self.heating_element_wattage
}
pub fn update(&mut self, now: Instant) {
self.temperature_pid_output = 0.0;
let temperature = self.temperature_sensor.get_temperature();
let temperature_celsius = ThermodynamicTemperature::new::<degree_celsius>(
temperature.as_ref().unwrap_or(&0.0).to_owned(),
);
self.heating.wiring_error = temperature.is_err();
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the relais and return
self.relais.set(false);
self.heating.heating = false;
return;
}
if self.heating_allowed {
let error: f64 = self.heating.target_temperature.get::<degree_celsius>()
- self.heating.temperature.get::<degree_celsius>();
let control = self.pid.update(error, now); // PID output
// Clamp PID output to 0.0 – 1.0 (as duty cycle)
let duty = control.clamp(0.0, self.max_clamp);
self.temperature_pid_output = duty;
let elapsed = now.duration_since(self.window_start);
// Restart window if needed
if elapsed >= self.pwm_period {
self.window_start = now;
}
// Compare duty cycle to elapsed time
let on_time = self.pwm_period.mul_f64(duty);
// Relay is ON if within duty cycle window
let on = elapsed < on_time;
self.relais.set(on);
self.heating.heating = on;
}
}
}
+2 -2
View File
@@ -57,12 +57,12 @@ impl MachineAct for LaserMachine {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Macine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
+4
View File
@@ -19,11 +19,13 @@ pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod extruder1;
pub mod extruder2;
pub mod laser;
pub mod machine_identification;
pub mod mock;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod winder2;
pub const VENDOR_QITECH: u16 = 0x0001;
@@ -34,6 +36,8 @@ pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_EXTRUDER_V2: u16 = 0x0016;
pub const TEST_MACHINE: u16 = 0x0033;
use serde_json::Value;
use smol::lock::RwLock;
+2
View File
@@ -1,5 +1,6 @@
use std::fmt::Display;
use ethercat_hal::devices::wago_750_354::WAGO_750_354_IDENTITY_A;
use serde::Deserialize;
use serde::Serialize;
@@ -345,6 +346,7 @@ pub fn get_identification_addresses(
let identity_tuple = subdevice_identity_to_tuple(subdevice_identity);
Ok(match identity_tuple {
WAGO_750_354_IDENTITY_A => MachineIdentificationAddresses::default(),
EK1100_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1002_IDENTITY_A => MachineIdentificationAddresses::default(),
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
+2 -2
View File
@@ -48,12 +48,12 @@ impl MachineAct for MockMachine {
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Macine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
/*Doesnt connect to any Machine so do nothing*/
{
()
}
+9 -3
View File
@@ -1,11 +1,13 @@
#[cfg(feature = "mock-machine")]
use crate::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
use crate::{Machine, MachineNewParams, machine_identification::MachineIdentification};
use crate::{
Machine, MachineNewParams, extruder1::ExtruderV2, machine_identification::MachineIdentification,
};
#[cfg(not(feature = "mock-machine"))]
use crate::{
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
winder2::Winder2,
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine,
test_machine::TestMachine, winder2::Winder2,
};
use lazy_static::lazy_static;
@@ -85,8 +87,11 @@ lazy_static! {
pub static ref MACHINE_REGISTRY: MachineRegistry = {
let mut mc = MachineRegistry::new();
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV3>(ExtruderV3::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
@@ -98,6 +103,7 @@ lazy_static! {
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc.register::<TestMachine>(TestMachine::MACHINE_IDENTIFICATION);
mc
};
}
+13 -22
View File
@@ -1,6 +1,5 @@
use serialport::UsbPortInfo;
use smol::Task;
use std::{collections::HashMap, time::Duration};
use std::collections::HashMap;
/*
*@author: Alisher Darmenov
*@company: QiTech
@@ -75,38 +74,30 @@ impl SerialDetection {
}
}
// Async function that runs until at least one serial device is found.
pub async fn find_serial() -> Option<HashMap<String, UsbPortInfo>> {
smol::Timer::after(Duration::from_secs(1)).await;
/*
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
return Some(devices);
} else {
return None;
pub async fn start_serial_discovery(app_state: Arc<>) {
loop {
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
handle_serial_device_hotplug(app_state.clone(), devices).await;
}
smol::Timer::after(Duration::from_secs(1)).await;
}
}
// Returns a smol::Task that resolves when atleast one device is found.
pub fn start_serial_discovery() -> Task<Option<HashMap<String, UsbPortInfo>>> {
smol::spawn(find_serial())
}
/*
pub async fn handle_serial_device_hotplug(
app_state: Arc<SharedState>,
map: Option<HashMap<String, UsbPortInfo>>,
map: HashMap<String, UsbPortInfo>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
let laser = SerialDetection::get_path_by_id(laser_ident, map);
let mut unique_ident: Option<MachineIdentificationUnique> = None;
+36
View File
@@ -0,0 +1,36 @@
use super::TestMachine;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for TestMachine {
fn act(&mut self, now: Instant) {
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_state();
self.last_state_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
MachineMessage::DisconnectMachine(_machine_connection) => {
// Does not connect to any Machine; do nothing
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
use super::TestMachine;
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub led_on: [bool; 4],
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
pub enum TestMachineEvents {
State(Event<StateEvent>),
}
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetLed { index: usize, on: bool },
SetAllLeds { on: bool },
}
#[derive(Debug, Clone)]
pub struct TestMachineNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<TestMachineEvents> for TestMachineNamespace {
fn emit(&mut self, events: TestMachineEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<TestMachineEvents> for TestMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
TestMachineEvents::State(event) => event.clone().into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
cache_first_and_last_event()
}
}
impl MachineApi for TestMachine {
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetLed { index, on } => self.set_led(index, on),
Mutation::SetAllLeds { on } => self.set_all_leds(on),
}
for (led, &on) in self.douts.iter().zip(self.led_on.iter()) {
led.set(on);
}
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
+65
View File
@@ -0,0 +1,65 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine::api::{StateEvent, TestMachineEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_output::DigitalOutput;
use smol::channel::{Receiver, Sender};
use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine::api::TestMachineNamespace;
use crate::{TEST_MACHINE, VENDOR_QITECH};
#[derive(Debug)]
pub struct TestMachine {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub namespace: TestMachineNamespace,
pub last_state_emit: Instant,
pub led_on: [bool; 4],
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub douts: [DigitalOutput; 4],
}
impl Machine for TestMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl TestMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: TEST_MACHINE,
};
}
impl TestMachine {
pub fn emit_state(&mut self) {
let event = StateEvent {
led_on: self.led_on,
}
.build();
self.namespace.emit(TestMachineEvents::State(event));
}
/// Set the state of a specific LED
pub fn set_led(&mut self, index: usize, on: bool) {
if index < self.led_on.len() {
self.led_on[index] = on;
self.emit_state();
}
}
/// Set all LEDs at once
pub fn set_all_leds(&mut self, on: bool) {
self.led_on = [on; 4];
self.emit_state();
}
}
+99
View File
@@ -0,0 +1,99 @@
use crate::test_machine::TestMachine;
use crate::test_machine::api::TestMachineNamespace;
use smol::block_on;
use std::time::Instant;
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use anyhow::Error;
use ethercat_hal::devices::el2004::{EL2004, EL2004_IDENTITY_A, EL2004Port};
use ethercat_hal::io::digital_output::DigitalOutput;
//Imports For Wago
/*
use ethercat_hal::devices::wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
use ethercat_hal::devices::{EthercatDevice, downcast_device};
use smol::lock::RwLock;
use std::sync::Arc;
*/
impl MachineNewTrait for TestMachine {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params
.device_group
.iter()
.map(|device_identification| device_identification.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/TestMachine::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
/*
// Example usage of a Wago Coupler and a 750-1506 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(1).unwrap().clone().unwrap();
let wago750_1506: Arc<RwLock<Wago750_1506>> =
downcast_device::<Wago750_1506>(dev).await?;
let do1 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO1);
let do2 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO2);
let do3 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO3);
let do4 = DigitalOutput::new(wago750_1506.clone(), Wago750_1506OutputPort::DO4);
drop(coupler);
*/
let el2004 =
get_ethercat_device::<EL2004>(hardware, params, 1, [EL2004_IDENTITY_A].to_vec())
.await?
.0;
let do1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let (sender, receiver) = smol::channel::unbounded();
let mut my_test = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: TestMachineNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
led_on: [false; 4],
main_sender: params.main_thread_channel.clone(),
douts: [do1, do2, do3, do4],
};
my_test.emit_state();
Ok(my_test)
})
}
}
+1 -6
View File
@@ -23,7 +23,7 @@ bitvec = "1.0.1"
# concurrency
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
futures = "0.3.31"
regex = "1.11.3"
# web
serde_json = "1.0.143"
@@ -37,14 +37,12 @@ serialport = "4.7.3"
#tracing
tracing = { version = "0.1.41", features = ["attributes"] }
tracing-journald = { version = "0.3.1", optional = true }
tracing-futures = "0.2.5"
tracing-subscriber = { version = "0.3.20", features = [
"env-filter",
"chrono",
"registry",
] }
utils = { version = "0.1.0", path="../utils" }
tonic = { version = "0.13.1", optional = true }
spin_sleep = "1.3.3"
dhat = { version = "0.3.3", optional = true }
ctrlc = { version = "3.3", optional = true }
@@ -71,6 +69,3 @@ heap-profile = ["dhat"]
[profile.release]
codegen-units = 1
lto = "fat"
[package.metadata.cargo-machete]
ignored = ["tonic"]
+2 -2
View File
@@ -3,8 +3,8 @@ use ethercrab::PduStorage;
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
pub const MAX_SUBDEVICES: usize = 16;
/// Maximum PDU data payload size - set this to the max PDI size or higher.
pub const MAX_PDU_DATA: usize = PduStorage::element_size(256);
pub const MAX_PDU_DATA: usize = PduStorage::element_size(512);
/// Maximum number of EtherCAT frames that can be in flight at any one time.
pub const MAX_FRAMES: usize = 16;
/// Maximum total PDI length.
pub const PDI_LEN: usize = 256;
pub const PDI_LEN: usize = 512;
@@ -17,6 +17,7 @@ pub async fn send_ethercat_discovering(app_state: Arc<SharedState>) {
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
}
@@ -31,6 +32,7 @@ pub async fn send_ethercat_found(app_state: Arc<SharedState>, interface: &str) {
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
}
+26 -7
View File
@@ -1,13 +1,18 @@
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::{Task, Timer};
use std::time::Duration;
use smol::Timer;
use std::{sync::Arc, time::Duration};
pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
use crate::{
app_state::SharedState,
ethercat::{ethercat_discovery_info::send_ethercat_found, setup::setup_loop},
};
pub async fn find_ethercat_interface() -> String {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found EtherCAT Interface at: {}", interface);
return Ok(interface);
return interface;
}
Err(e) => {
tracing::warn!("No working interface found: {}. Retrying...", e);
@@ -17,7 +22,21 @@ pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
}
}
// Returns a Future to the interface
pub fn start_interface_discovery() -> Task<Result<String, anyhow::Error>> {
smol::spawn(find_ethercat_interface())
pub async fn start_interface_discovery(app_state: Arc<SharedState>) {
let interface = find_ethercat_interface().await;
tracing::info!("Calling setup_loop");
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
Err(e) => {
tracing::error!(
"[{}::main] Failed to initialize EtherCAT network \n{:?}",
module_path!(),
e
);
}
}
send_ethercat_found(app_state, &interface).await;
}
+83 -27
View File
@@ -6,18 +6,22 @@ use crate::{
app_state::SharedState,
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
};
use control_core::realtime::set_core_affinity;
use control_core::socketio::namespace::NamespaceCacheingLogic;
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
use control_core::irq_handling::set_irq_affinity;
use control_core::{irq_handling::set_irq_affinity, realtime::set_realtime_priority};
use ethercat_hal::debugging::diagnosis_history::get_most_recent_diagnosis_message;
use ethercat_hal::devices::devices_from_subdevices;
use ethercat_hal::devices::wago_750_354::{
WAGO_750_354_PRODUCT_ID, WAGO_750_354_VENDOR_ID, Wago750_354,
};
use ethercrab::std::ethercat_now;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use machines::machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
DeviceIdentificationIdentified, MachineIdentificationUnique, read_device_identifications,
};
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use machines::registry::{MACHINE_REGISTRY, MachineRegistry};
use machines::{Machine, MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams};
use smol::channel::Sender;
@@ -109,9 +113,7 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
shared_state: Arc<SharedState>,
socket_queue_tx: Sender<(SocketRef, Arc<control_core::socketio::event::GenericEvent>)>,
) -> Result<(), anyhow::Error> {
tracing::info!("set_ethercat_devices");
let device_grouping_result = group_devices_by_identification(device_identifications);
tracing::info!("{:?}", device_grouping_result.unidentified_devices);
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
let mut machines: Vec<Box<dyn Machine>> = vec![];
@@ -153,11 +155,11 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
}),
}
}
let _ = shared_state
.rt_machine_creation_channel
.send(crate::app_state::HotThreadMessage::AddMachines(machines))
.await;
shared_state.add_machines_if_not_exists(machine_objs).await;
shared_state.clone().send_machines_event().await;
@@ -194,10 +196,9 @@ pub async fn setup_loop(
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
{
use ethercrab::std::tx_rx_task;
use futures::executor::block_on;
let rt = smol::LocalExecutor::new();
let _ = block_on(rt.run(async {
let _ = smol::block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
.expect("spawn TX/RX task")
.await
@@ -245,6 +246,7 @@ pub async fn setup_loop(
.write()
.await
.main_namespace;
let event = EthercatDevicesEventBuilder().initializing();
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
}
@@ -298,7 +300,7 @@ pub async fn setup_loop(
// filter devices and if Option<DeviceMachineIdentification> is Some
// return identified_devices, identified_device_identifications, identified_subdevices
let (identified_device_identifications, identified_devices, identified_subdevices): (
let (identified_device_identifications,identified_devices, identified_subdevices): (
Vec<_>,
Vec<_>,
Vec<_>,
@@ -326,18 +328,55 @@ pub async fn setup_loop(
acc.0.push(identified_device_identification.clone());
acc.1.push(identified_device);
acc.2.push(identified_subdevice);
ethercat_meta_devices.push(EtherCatDeviceMetaData::from_subdevice(identified_subdevice,identified_device_identification));
let meta = EtherCatDeviceMetaData::from_subdevice(identified_subdevice,identified_device_identification);
ethercat_meta_devices.push(meta);
acc
},
);
tracing::info!("Found Devices: {:?}", ethercat_meta_devices);
// We always need to have atleast one subdevice anyways
let coupler = subdevices.get(0).unwrap();
let _resp = get_most_recent_diagnosis_message(coupler).await;
/*
handle special edge cases,
Wago couplers handle all subdevices as part of the coupler instead of their own devices,
meaning we need to convert the PDO Mappings to seperate SubDevices to enable more ease of use
OR alternatively you could show it like TwinCAT with "slots" on the Coupler
*/
match (coupler.identity().vendor_id, coupler.identity().product_id) {
(WAGO_750_354_VENDOR_ID, WAGO_750_354_PRODUCT_ID) => {
let r = Wago750_354::initialize_modules(coupler).await?;
for module in r {
if coupler.configured_address() == module.belongs_to_addr {
match ethercat_meta_devices.get(0) {
Some(meta) => {
let meta_data = EtherCatDeviceMetaData {
configured_address: module.slot,
name: "wago module".to_owned(),
vendor_id:module.vendor_id,
product_id: module.product_id,
revision: 0x2,
device_identification: DeviceIdentification{
device_machine_identification: meta.device_identification.device_machine_identification.clone(),
device_hardware_identification: machines::machine_identification::DeviceHardwareIdentification::Ethercat(DeviceHardwareIdentificationEthercat{ subdevice_index: module.slot as usize }) }
};
ethercat_meta_devices.push(meta_data);
}
None => break,
}
}
}
}
_ => (),
};
drop(ethercat_meta_devices);
for subdevice in subdevices.iter() {
// Hack so El5152 goes into OP
if subdevice.name() == "EL5152" {
subdevice.sdo_write(SM_OUTPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
subdevice.sdo_write(SM_INPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
subdevice.sdo_write(SM_OUTPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
}
}
@@ -359,17 +398,35 @@ pub async fn setup_loop(
app_state.clone().socketio_setup.socket_queue_tx.clone(),
)
.await?;
/*let main_namespace = &mut app_state_clone
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_clone.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));*/
let group_safe = match group_preop.into_safe_op(&maindevice).await {
Ok(group_op) => {
tracing::info!("Group in Safe-OP state");
group_op
}
Err(err) => Err(anyhow::anyhow!(
"[{}::setup_loop] Failed to put group in Safe-OP state: {:?}",
module_path!(),
err
))?,
};
/*
Make DC Slaves Happy
Does this potentially cause issues with Non DC-Sync devices?
*/
let res = group_safe.tx_rx_sync_system_time(&maindevice).await;
match res {
Ok(_) => (),
Err(e) => tracing::error!(
"[{}::setup_loop] Failed to sync dc time: {:?}",
e,
module_path!()
),
}
// Put group in operational state
let group_op = match group_preop.into_op(&maindevice).await {
let group_op = match group_safe.into_op(&maindevice).await {
Ok(group_op) => {
tracing::info!("Group in OP state");
group_op
@@ -394,7 +451,6 @@ pub async fn setup_loop(
.await;
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
}
tracing::info!("DONE WITH INIT");
return Ok(EthercatSetup {
devices,
+1
View File
@@ -68,6 +68,7 @@ pub fn start_loop_thread(
match msg {
HotThreadMessage::NoMsg => {}
HotThreadMessage::AddEtherCatSetup(ethercat_setup) => {
println!("EthercatSetup: {:?}", ethercat_setup.devices);
rt_loop_inputs.ethercat_setup = Some(Box::new(ethercat_setup));
}
HotThreadMessage::WriteMachineDeviceInfo(info_request) => {
+74 -68
View File
@@ -1,4 +1,3 @@
use futures::{FutureExt, select};
use machines::{
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
@@ -8,19 +7,15 @@ use machines::{
DeviceIdentification, DeviceIdentificationIdentified, MachineIdentificationUnique,
},
registry::{MACHINE_REGISTRY, MachineRegistry},
serial::{
devices::laser::Laser,
init::{SerialDetection, start_serial_discovery},
},
serial::{devices::laser::Laser, init::SerialDetection},
winder2::api::GenericEvent,
};
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
use app_state::{HotThreadMessage, SharedState};
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
init::start_interface_discovery,
setup::setup_loop,
};
use ethercat::ethercat_discovery_info::send_ethercat_discovering;
use r#loop::start_loop_thread;
use panic::init_panic_handling;
@@ -28,15 +23,24 @@ use rest::init::start_api_thread;
use serialport::UsbPortInfo;
use smol::{
channel::{Receiver, Sender},
future,
lock::RwLock,
};
use socketio::{main_namespace::machines_event::MachineObj, queue::start_socketio_queue};
use socketio::main_namespace::machines_event::MachineObj;
use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
#[cfg(feature = "mock-machine")]
use mock_init::init_mock;
use crate::{
ethercat::{
ethercat_discovery_info::send_ethercat_found, init::find_ethercat_interface,
setup::setup_loop,
},
socketio::queue::socketio_queue_worker,
};
#[cfg(feature = "mock-machine")]
pub mod mock_init;
@@ -110,17 +114,30 @@ pub async fn add_serial_device(
shared_state.clone().send_machines_event().await;
}
async fn setup_ethercat(
interface: &str,
pub async fn start_serial_discovery(app_state: Arc<SharedState>) {
loop {
let devices = SerialDetection::detect_devices();
// This allows detection of disconnected devices
handle_serial_device_hotplug(app_state.clone(), devices).await;
smol::Timer::after(Duration::from_secs(1)).await;
}
}
pub async fn start_interface_discovery(
app_state: Arc<SharedState>,
sender: Sender<HotThreadMessage>,
) {
tracing::info!("Calling setup_loop");
let interface = find_ethercat_interface().await;
tracing::info!("Inferface found {}, setting up EtherCAT loop", interface);
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(setup) => {
let _ = sender.send(HotThreadMessage::AddEtherCatSetup(setup)).await;
tracing::info!("Successfully initialized EtherCAT network");
tracing::info!("Successfully initialized EtherCAT devices");
}
Err(e) => {
@@ -131,23 +148,20 @@ async fn setup_ethercat(
);
}
}
send_ethercat_found(app_state.clone(), &interface).await;
}
pub async fn handle_serial_device_hotplug(
app_state: Arc<SharedState>,
map: Option<HashMap<String, UsbPortInfo>>,
map: HashMap<String, UsbPortInfo>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
let laser = SerialDetection::get_path_by_id(laser_ident, map);
let mut unique_ident: Option<MachineIdentificationUnique> = None;
{
@@ -267,10 +281,18 @@ async fn handle_async_requests(recv: Receiver<AsyncThreadMessage>, shared_state:
}
}
}
tracing::warn!("Async handler task finished");
}
#[cfg(feature = "development-build")]
use std::sync::atomic::{AtomicBool, Ordering};
pub async fn start_socketio_queue(app_state: Arc<SharedState>) {
let app_state = app_state.as_ref();
loop {
let res = socketio_queue_worker(app_state).await;
tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
tracing::error!("Restarting SocketIO...");
}
}
#[cfg(feature = "development-build")]
fn setup_ctrlc_handler() -> Arc<AtomicBool> {
@@ -301,23 +323,25 @@ fn main() {
#[cfg(feature = "development-build")]
let running = setup_ctrlc_handler();
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(300);
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(700);
// for the "hot thread"
let (sender, receiver) = smol::channel::unbounded();
let (main_sender, main_receiver) = smol::channel::unbounded();
let shared_state = SharedState::new(sender.clone(), main_sender.clone());
let shared_state = SharedState::new(sender.clone(), main_sender);
let app_state = Arc::new(shared_state);
let _loop_thread = start_loop_thread(receiver, CYCLE_TARGET_TIME);
let _ = start_api_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
let mut socketio_task = smol::spawn(start_socketio_queue(app_state.clone()));
let mut serial_task = smol::spawn(start_serial_discovery(app_state.clone()));
let mut async_machine_task = smol::spawn(handle_async_requests(
main_receiver.clone(),
app_state.clone(),
));
#[cfg(not(feature = "mock-machine"))]
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
let mut handle_async_machine_requests =
smol::spawn(handle_async_requests(main_receiver, app_state.clone())).fuse();
smol::spawn(start_interface_discovery(app_state.clone(), sender)).detach();
smol::block_on(async {
send_empty_machines_event(app_state.clone()).await;
@@ -334,47 +358,29 @@ fn main() {
tracing::info!("Shutdown signal received, exiting main loop.");
break;
}
// lets the async runtime decide which future to run next
#[cfg(not(feature = "mock-machine"))]
select! {
res = ethercat_fut => {
tracing::info!("EtherCAT task finished: {:?}", res);
match res {
Ok(interface) =>
{
setup_ethercat(&interface,app_state.clone(),sender.clone()).await;
},
Err(_) => (),
};
},
res = serial_fut => {
let _ = handle_serial_device_hotplug(app_state.clone(),res).await;
serial_fut = start_serial_discovery().fuse();
},
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
res = handle_async_machine_requests => {
tracing::warn!("Async handler task finished: {:?}", res);
},
if serial_task.is_finished() {
tracing::warn!("Serial task died! Restarting...");
serial_task.cancel().await;
serial_task = smol::spawn(start_serial_discovery(app_state.clone()));
}
#[cfg(feature = "mock-machine")]
select! {
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
res = handle_async_machine_requests => {
tracing::warn!("Async handler task finished: {:?}", res);
},
_ = smol::Timer::after(Duration::from_millis(1)).fuse() => {
}
if socketio_task.is_finished() {
tracing::warn!("SocketIO task died! Restarting...");
socketio_task.cancel().await;
socketio_task = smol::spawn(start_socketio_queue(app_state.clone()));
}
if async_machine_task.is_finished() {
tracing::warn!("Async handler task died! Restarting...");
async_machine_task.cancel().await;
async_machine_task = smol::spawn(handle_async_requests(
main_receiver.clone(),
app_state.clone(),
));
}
future::yield_now().await;
}
});
}
+2 -4
View File
@@ -1,13 +1,11 @@
use super::namespace_id::NamespaceId;
use crate::app_state::SharedState;
use socketioxide::ParserConfig;
use socketioxide::extract::SocketRef;
use socketioxide::layer::SocketIoLayer;
use std::str::FromStr;
use std::sync::Arc;
use crate::app_state::SharedState;
use super::namespace_id::NamespaceId;
pub async fn init_socketio(app_state: Arc<SharedState>) -> SocketIoLayer {
// create
let (socketio_layer, io) = socketioxide::SocketIoBuilder::new()
+9 -7
View File
@@ -1,9 +1,7 @@
use smol::Task;
use crate::app_state::SharedState;
use std::{sync::Arc, time::Instant};
use tracing::{debug, info, instrument, trace};
use crate::app_state::SharedState;
/// Send a single event with retry logic
#[instrument(skip_all)]
async fn send_event_with_retry(
@@ -60,7 +58,7 @@ async fn send_event_with_retry(
}
}
async fn socketio_queue_worker(app_state: Arc<SharedState>) {
pub async fn socketio_queue_worker(app_state: &SharedState) {
tracing::info!("SocketIO global queue listener started");
let mut event_count = 0;
let mut batch_start = Instant::now();
@@ -89,7 +87,11 @@ async fn socketio_queue_worker(app_state: Arc<SharedState>) {
info!("SocketIO global queue listener stopped");
}
/// Returns a spawned async task handling the Socket.IO event queue
pub fn start_socketio_queue(app_state: Arc<SharedState>) -> Task<()> {
smol::spawn(socketio_queue_worker(app_state))
pub async fn start_socketio_queue(app_state: Arc<SharedState>) {
let app_state = app_state.as_ref();
loop {
let res = socketio_queue_worker(app_state).await;
tracing::error!("SocketIO task finished, but should never finish: {:?}", res);
tracing::error!("Restarting SocketIO...");
}
}