Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6c0744efa | ||
|
|
8c7e810359 | ||
|
|
e4d0b3c5be | ||
|
|
dad029ab0d | ||
|
|
9c780799d1 | ||
|
|
27a33f9def | ||
|
|
f44cd029f9 | ||
|
|
9abb7629fb | ||
|
|
a161d85a63 | ||
|
|
9df9e6df4a | ||
|
|
29d465c13e | ||
|
|
622c94f2d9 | ||
|
|
f31a2789ca | ||
|
|
e7632e8d1b | ||
|
|
2f4f3de32a | ||
|
|
c9f49198c7 | ||
|
|
e2c12a0168 | ||
|
|
cb6e066512 | ||
|
|
1dd0421f96 | ||
|
|
64855f2e7a | ||
|
|
855ae7b3cf | ||
|
|
5f9a8605c9 | ||
|
|
567730a8d4 | ||
|
|
6b2164b88b | ||
|
|
930ea10409 | ||
|
|
24b703e262 | ||
|
|
152dd3efa0 | ||
|
|
ebc5bae55f | ||
|
|
88214c46a8 | ||
|
|
c9485a92e9 | ||
|
|
9df42c4d13 | ||
|
|
fc9a439ae8 | ||
|
|
e451153406 | ||
|
|
500e2fe481 | ||
|
|
a5884d93e7 | ||
|
|
d2e32cdd78 | ||
|
|
f66c6d4f65 | ||
|
|
fb85cc6db7 | ||
|
|
4e128ded36 | ||
|
|
f50388b141 | ||
|
|
4025a68fe1 | ||
|
|
7a05fa206f | ||
|
|
8594b06c3b | ||
|
|
17523574d8 | ||
|
|
7971797178 | ||
|
|
a8b0837e58 | ||
|
|
e419974844 | ||
|
|
773d0f6c15 | ||
|
|
afc4e0d8b4 |
No files matched your search
@@ -4,4 +4,4 @@ dist/
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
shell.nix
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 3.2 EK1100 Wiring
|
||||
|
||||
This wiring configuration powers the EL2004 and prepares it for LED control.
|
||||
It is not the only possible wiring but is the **simplest functional setup**.
|
||||
|
||||
### ⚠️ Safety Warning
|
||||
Always disconnect power before wiring.
|
||||
Working on live EtherCAT terminals can cause serious damage or electrical shock.
|
||||
|
||||
---
|
||||
|
||||
## 3.2.1 Safe Wiring Procedure (Beckhoff Recommended)
|
||||
|
||||
1. Insert a screwdriver **straight** into the square release hole.
|
||||
2. Insert the stripped wire into the round opening.
|
||||
3. Remove the screwdriver — the spring clamp locks the wire.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 3.2.2 Wiring Steps (Used in This Example)
|
||||
|
||||
We supply power using a **DC hollow-plug adapter**, like this one:
|
||||
https://www.amazon.de/dp/B093FTFZ8Q
|
||||
|
||||
Perform the following wiring on the EK1100:
|
||||
|
||||
1. Red wire **(+24 V)** → Terminal **2**
|
||||
2. Black wire **(0 V)** → Terminal **3**
|
||||
3. Jumper wire from **Terminal 1 → Terminal 6**
|
||||
4. Jumper wire from **Terminal 5 → Terminal 7**
|
||||
|
||||
After wiring, your module should look like **Figure 1**.
|
||||
|
||||
---
|
||||
|
||||
### **Figure 1 — EK1100 Minimal Wiring**
|
||||
<img src="./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
|
||||

|
||||
|
||||
Steps:
|
||||
|
||||
- Click Assign on the EK1100
|
||||
|
||||
- Select TestMachine V1
|
||||
|
||||

|
||||
|
||||
|
||||
- Enter a serial number (use the same for EK1100 + EL2004)
|
||||
|
||||

|
||||
|
||||
- Click Write
|
||||
|
||||
- Repeat for the EL2004
|
||||
|
||||
## 5.2 Testing LED Control
|
||||
Navigate to:
|
||||
|
||||
Machines → TestMachine
|
||||

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

|
||||
|
||||
**You can now toggle the four digital outputs of the EL2004.**
|
||||
# 6. References
|
||||
|
||||
This guide incorporates information from official Beckhoff documentation.
|
||||
All diagrams, product names, and figures belong to Beckhoff Automation GmbH & Co. KG and are used here solely for educational purposes.
|
||||
|
||||
Referenced Manuals
|
||||
|
||||
[Beckhoff EK1100 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/ek110x_ek15xx_en.pdf)
|
||||
|
||||
[Beckhoff EL2004 Documentation](https://download.beckhoff.com/download/Document/io/ethercat-terminals/el20xx_el2124_de.pdf)
|
||||
|
||||
# 7. Acknowledgements
|
||||
|
||||
This tutorial is inspired by the clarity and educational quality of Beckhoff manuals.
|
||||
All wiring illustrations and hardware descriptions in this guide are provided for demonstration purposes only and do not replace official Beckhoff installation guidelines.
|
||||
|
||||
Special thanks to the QiTech engineering team for providing the backend architecture, EtherCAT HAL abstraction, and the TestMachine framework that makes this example possible.
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 74 KiB |
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { OutsideCorner } from "@/components/OutsideCorner";
|
||||
import { useMachines } from "@/client/useMachines";
|
||||
import { useOnSubpath } from "@/lib/useOnSubpath";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from "@tanstack/react-router";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { Icon, IconName } from "./Icon";
|
||||
import { useMainNamespace } from "@/client/mainNamespace";
|
||||
|
||||
type SidebarItemContent = {
|
||||
link: string;
|
||||
@@ -57,7 +63,51 @@ export function useSidebarlessWidth() {
|
||||
|
||||
export function SidebarLayout() {
|
||||
const machines = useMachines();
|
||||
const router = useRouter();
|
||||
const routerState = useRouterState();
|
||||
const { machines: machinesEvent } = useMainNamespace();
|
||||
const [contentWidth, setContentWidth] = React.useState<number>(0);
|
||||
|
||||
// Machine connection guard: redirect to setup if selected machine disconnects
|
||||
useEffect(() => {
|
||||
// Wait for machines data to be loaded
|
||||
if (!machinesEvent?.data) return;
|
||||
|
||||
// Extract serial from current path (e.g., /machines/laser1/12345/control)
|
||||
const pathMatch = routerState.location.pathname.match(
|
||||
/\/machines\/[^/]+\/(\d+)/,
|
||||
);
|
||||
if (!pathMatch) return; // Not on a machine page
|
||||
|
||||
// Safety check: ensure pathMatch has at least 2 elements (full match + capture group)
|
||||
if (pathMatch.length < 2) {
|
||||
console.warn("[ConnectionGuard] Invalid pathMatch structure:", pathMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse serial number with error handling
|
||||
const serialString = pathMatch[1];
|
||||
const serialNumber = parseInt(serialString, 10);
|
||||
|
||||
// Check if parseInt succeeded (NaN means parsing failed)
|
||||
if (isNaN(serialNumber)) {
|
||||
console.warn(
|
||||
`[ConnectionGuard] Failed to parse serial number from "${serialString}"`,
|
||||
{ pathname: routerState.location.pathname },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if machine still exists in the raw machines data
|
||||
const machineExists = machinesEvent.data.machines.some(
|
||||
(m) => m.machine_identification_unique.serial === serialNumber,
|
||||
);
|
||||
|
||||
if (!machineExists) {
|
||||
router.navigate({ to: "/_sidebar/setup/machines" });
|
||||
}
|
||||
}, [machines, machinesEvent, routerState.location.pathname, router]);
|
||||
|
||||
const items: SidebarItemContent[] = [
|
||||
...machines.map((machine) => ({
|
||||
link: `/_sidebar/machines/${machine.slug}/${machine.machine_identification_unique.serial}/control`,
|
||||
|
||||
@@ -63,7 +63,11 @@ export type DataSeries = SeriesData | SeriesData[];
|
||||
export type BigGraphProps = {
|
||||
newData: DataSeries;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
renderValue?: (
|
||||
value: number,
|
||||
seriesIndex?: number,
|
||||
seriesTitle?: string,
|
||||
) => string;
|
||||
config: GraphConfig;
|
||||
graphId: string;
|
||||
syncGraph?: PropGraphSync;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod diagnosis_history;
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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())
|
||||
});
|
||||
|
||||
|
||||
@@ -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())
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
pub mod coe;
|
||||
pub mod debugging;
|
||||
pub mod devices;
|
||||
pub mod helpers;
|
||||
pub mod io;
|
||||
|
||||
@@ -19,7 +19,6 @@ euclid = "0.22.11"
|
||||
|
||||
# concurrency
|
||||
smol = "2.0.2"
|
||||
futures = "0.3.31"
|
||||
|
||||
# web
|
||||
serde_json = "1.0.143"
|
||||
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 ¶ms.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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*/
|
||||
{
|
||||
()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 ¶ms.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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...");
|
||||
}
|
||||
}
|
||||