Compare commits

..
Author SHA1 Message Date
EinsPommes 4e233ae718 refactor(extruder): improve code readability and structure in API and mock implementations 2026-02-24 22:55:07 +01:00
EinsPommes f35ba7862e refactor(extruder): streamline heating fault management and enhance UI interactions 2026-02-24 22:47:50 +01:00
ᴏxᴏ.ᴍɪᴋᴀ 80048c0bbb Merge branch 'master' into 972-extruder-v3-heat-problem 2026-02-24 22:07:11 +01:00
EinsPommes b8bb8053c7 refactor(extruder): remove temperature target enabled state from Extruder3Settings 2026-02-21 13:39:19 +01:00
EinsPommes 0305867394 feat(extruder): introduce temperature target enable/disable functionality for nozzle control 2026-02-21 13:37:51 +01:00
EinsPommes 09f56ed990 feat(extruder): add target temperature control feature 2026-02-21 13:37:39 +01:00
Christian 8beb24cef0 Update electron/src/machines/extruder/extruder3/Extruder3ControlPage.tsx 2026-02-21 13:37:16 +01:00
EinsPommes ed84982c8e refactor(extruder): enhance heating fault handling and watchdog timeout 2026-02-21 13:37:13 +01:00
EinsPommes e792ed1b4d feat(extruder): implement heating fault acknowledgment for v3 2026-02-21 13:37:06 +01:00
EinsPommes 132ee383ee Update comments 2026-02-21 13:36:59 +01:00
EinsPommes 34a8c54098 Add heating fault handling and safeguard features to Extruder v3
- Introduced heating fault state management with UI notifications in HeatingZone component.
- Implemented heating safeguard feature to monitor temperature increases and automatically set to standby if conditions are not met.
- Added retry functionality for heating faults, allowing users to attempt to reheat after a fault.
- Updated state schemas and API to accommodate new heating fault and safeguard states.
- Enhanced Extruder v3 control page to integrate new features and improve user experience.
2026-02-21 13:36:48 +01:00
41 changed files with 950 additions and 1578 deletions

No files matched your search

-14
View File
@@ -1,14 +0,0 @@
## What does this PR do / add / change?
<!-- Describe the purpose of this PR. What problem does it solve or what feature does it add? -->
## Screenshots
<!-- Include screenshots for any UI changes. Remove this section if not applicable. -->
## Checklist before opening the pr
- [ ] Tested locally
- [ ] Tested on the machine (if hardware interaction is involved)
- [ ] No format errors (`cargo fmt` / `npm run format`)
- [ ] No build errors (`cargo build` / `npm run build`)
-3
View File
@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
defaults:
run:
working-directory: electron
-3
View File
@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
-3
View File
@@ -12,9 +12,6 @@ env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.86"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
+12 -52
View File
@@ -1,8 +1,8 @@
# REST API `/api/v2`
The panel behaves like any other NetworkManager-managed linux and is able to either receive an IP-Address over DHCP or a statically defined one. Where the static IP is most likely the easiest to work with.
The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at:
- **Base URL:** `https://IP_ADDRESS`
- **Base URL:** `http://10.10.10.1:3001`
> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below).
@@ -10,57 +10,17 @@ The panel behaves like any other NetworkManager-managed linux and is able to eit
## Authentication
The panel performs Basic-Auth authentication.
For EVERY request made over HTTPS, Basic Auth is required.
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. It is therefore suggested to run the entire production line in isolated operation.
To set the password on the Panel for Basic Auth, do the following:
1. Open a Terminal
2. cd /home/qitech/control
3. ./credentials.sh
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity. To let the outside world communicate with the production line, a router should be placed at the network bondary bridging the production line's subnet and the rest of the network. The router can run in **client/bridge mode** on the line's subnet and expose the panel's API via **port forwading** or through a **reverse proxy** where one can add authentication, logging, rate limiting, etc.
The credentials.sh script sets and prints the username and password for the api ONCE in the console.
Afterwards its accessible in machine_password in /tmp/ until the machine shuts down.
If you ever forget your password you can simply run ./credentials.sh again, resetting the password used, however the username will always be machine
The following graphic depicts the suggested network topology for a single production line. Many lines could potentially share the same router.
Example request with curl:
```
# username=machine
# password=VoMZKZK4K4ZIvON+OU2ZcCXy
# With -k you can ignore curl's self signed certificate error
curl https://192.168.130.195/api/v2/machine -k -u machine:VoMZKZK4K4ZIvON+OU2ZcCXy
```
## Handling Self signed Certificate Warnings
For HTTPS we use self-signed certificates.
These are untrusted by default in browsers,tools like curl and https libraries.
For most programs you can either ignore this warning or you can manually trust the certificate on your system.
How these are installed depends on your system.
On the panel pc, the root Certificate is located at: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
You need to copy this file to an usb drive or copy it some other way and add it as a trusted root certificate in your OS.
Please refer to your OS' documentation or search the Internet about how to install a trusted SSL certificate.
## Bypassing HTTPS and Authentication
If you have an isolated network and dont wish to perform Authentication, then you can achieve this by:
1. Forking our repository
2. Changing the Open Ports in nixos/os/configuration.nix:
```
# All the way at the Bottom
networking.firewall.allowedUDPPorts = [ 53 67 69 ];
# Change this
networking.firewall.allowedTCPPorts = [ 443 ];
# To this
networking.firewall.allowedTCPPorts = [ 3001 ];
```
Again, you should only ever do this if you have your own reverse proxy with authentication or you have an isolated network.
<p align="center">
<img width="769" height="528" alt="Suggested network topology" src="./assets/subnet.png" />
</p>
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
---
@@ -78,7 +38,7 @@ Each machine also includes a `legacy_id` to support older **v1** workflows. If a
### Example request
```bash
curl -X GET "https://IP_ADDRESS/api/v2/machine" -u machine:YOUR_PASSWORD
curl -X GET "http://10.10.10.1:3001/api/v2/machine"
```
### Example response
@@ -159,7 +119,7 @@ To receive continuous updates (via WebSockets), subscribe to the machine namespa
### Example request (mock machine)
```bash
curl -X GET "https://IP_ADDRESS/api/v2/machine/mock/57922" -u machine:YOUR_PASSWORD
curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922"
```
### Example response
@@ -214,7 +174,7 @@ The API does **not** return the newly-applied state in the POST response. The pa
curl -X POST \
-d \
-H "Content-Type: application/json" \
"https://IP_ADDRESS/api/v1/machine/mock/57922" -u machine:YOUR_PASSWORD
"http://10.10.10.1:3001/api/v1/machine/mock/57922"
```
### Example response
+22 -5
View File
@@ -71,7 +71,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^40.6.1",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
@@ -5196,15 +5196,15 @@
}
},
"node_modules/electron": {
"version": "40.6.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-40.6.1.tgz",
"integrity": "sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ==",
"version": "37.10.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^24.9.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -5285,6 +5285,16 @@
"global-agent": "^3.0.0"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "22.19.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/electron/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -5320,6 +5330,13 @@
"semver": "bin/semver.js"
}
},
"node_modules/electron/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/electron/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+1 -1
View File
@@ -34,7 +34,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^40.6.1",
"electron": "^37.3.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
+3 -12
View File
@@ -20,7 +20,6 @@ import { TouchButton } from "../touch/TouchButton";
import { seriesToUPlotData } from "@/lib/timeseries";
import { ControlCard } from "@/control/ControlCard";
import { getAllTimeSeries } from "./createChart";
import { TargetDashOverlay } from "./TargetDashOverlay";
// Collects lines connected to visible series for rendering
const getVisibleLines = (data: any, visibleSeries: boolean[]) => {
@@ -596,18 +595,10 @@ export function BigGraph({
<div className="flex-1 overflow-hidden rounded-b-3xl pt-4">
<div
className={`relative h-full w-full overflow-hidden transition-opacity duration-100`}
ref={containerRef}
className={`h-full w-full overflow-hidden transition-opacity duration-100`}
style={{ backgroundColor: colors.background }}
>
<div ref={containerRef} className="h-full w-full overflow-hidden" />
<TargetDashOverlay
uplotRef={uplotRef}
newData={newData}
config={enhancedConfig}
selectedTimeWindow={selectedTimeWindow}
isLiveMode={isLiveMode}
/>
</div>
/>
</div>
</div>
</div>
@@ -1,411 +0,0 @@
import React, { useEffect, useMemo, useState, useId } from "react";
import uPlot from "uplot";
import { BigGraphProps, GraphConfig } from "./types";
import { getAllTimeSeries } from "./createChart";
type OverlayLine = {
key: string;
color: string;
width: number;
dash: number[];
d: string;
};
type ClipRect = {
x: number;
y: number;
width: number;
height: number;
};
function findFirstGe(arr: number[], value: number): number {
let lo = 0;
let hi = arr.length - 1;
let ans = arr.length;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] >= value) {
ans = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return ans;
}
function findLastLe(arr: number[], value: number): number {
let lo = 0;
let hi = arr.length - 1;
let ans = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] <= value) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
function buildSteppedPath(
u: uPlot,
xData: number[],
yData: Array<number | null>,
): string {
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xData.length < 2 || xMin === undefined || xMax === undefined) {
return "";
}
const firstVisibleIdx = Math.max(0, findFirstGe(xData, xMin) - 1);
const lastVisibleIdx = Math.min(
xData.length - 1,
findLastLe(xData, xMax) + 1,
);
if (lastVisibleIdx < firstVisibleIdx) {
return "";
}
const parts: string[] = [];
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = firstVisibleIdx; i <= lastVisibleIdx; i++) {
const value = yData[i];
if (value === null || value === undefined) continue;
const x = u.valToPos(xData[i], "x", true);
const y = u.valToPos(value, "y", true);
if (!started) {
parts.push(`M ${x} ${y}`);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
parts.push(`L ${x} ${prevY}`);
}
if (y !== prevY) {
parts.push(`L ${x} ${y}`);
}
prevX = x;
prevY = y;
}
return parts.join(" ");
}
function buildFlatLatestValuePath(
u: uPlot,
yData: Array<number | null>,
): string {
let latestValue: number | null = null;
for (let i = yData.length - 1; i >= 0; i--) {
const value = yData[i];
if (value !== null && value !== undefined) {
latestValue = value;
break;
}
}
if (latestValue === null) {
return "";
}
const y = u.valToPos(latestValue, "y", true);
const left = u.bbox.left;
const right = u.bbox.left + u.bbox.width;
return `M ${left} ${y} L ${right} ${y}`;
}
function getHistoricalDashTargets(
data: BigGraphProps["newData"],
config: GraphConfig,
): Array<{
dataIndex: number;
dash: number[];
color: string;
width: number;
}> {
const allOriginalSeries = getAllTimeSeries(data);
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
let visibleLineIndex = 0;
const targets: Array<{
dataIndex: number;
dash: number[];
color: string;
width: number;
}> = [];
config.lines?.forEach((line) => {
if (line.show === false) return;
const dash = line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
targets.push({
dataIndex: firstConfigLineDataIndex + visibleLineIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
visibleLineIndex++;
});
return targets;
}
function areDashArraysEqual(a: number[], b: number[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function areOverlayLinesEqual(a: OverlayLine[], b: OverlayLine[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (
a[i].key !== b[i].key ||
a[i].color !== b[i].color ||
a[i].width !== b[i].width ||
a[i].d !== b[i].d ||
!areDashArraysEqual(a[i].dash, b[i].dash)
) {
return false;
}
}
return true;
}
function isSameClipRect(a: ClipRect | null, b: ClipRect): boolean {
return (
!!a &&
a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height
);
}
export function TargetDashOverlay({
uplotRef,
newData,
config,
selectedTimeWindow,
isLiveMode,
}: {
uplotRef: React.RefObject<uPlot | null>;
newData: BigGraphProps["newData"];
config: GraphConfig;
selectedTimeWindow: number | "all";
isLiveMode: boolean;
}) {
const targetMeta = useMemo(
() => getHistoricalDashTargets(newData, config),
[newData, config],
);
const clipPathId = useId().replace(/:/g, "");
const [lines, setLines] = useState<OverlayLine[]>([]);
const [clipRect, setClipRect] = useState<ClipRect | null>(null);
useEffect(() => {
if (targetMeta.length === 0) {
setLines([]);
setClipRect(null);
}
}, [targetMeta]);
useEffect(() => {
let rafId: number | null = null;
let bootstrapRafId: number | null = null;
let recalcScheduled = false;
let hooksAttached = false;
const removeHookFns: Array<() => void> = [];
let resizeObserver: ResizeObserver | null = null;
const recalc = () => {
recalcScheduled = false;
const u = uplotRef.current;
if (!u || targetMeta.length === 0) {
return;
}
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length < 2) {
return;
}
const nextLines = targetMeta
.map((meta, index) => {
const yData = u.data[meta.dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length < 2) return null;
const useFlatShortWindowPath =
isLiveMode &&
selectedTimeWindow !== "all" &&
selectedTimeWindow <= 60_000;
const d = useFlatShortWindowPath
? buildFlatLatestValuePath(u, yData)
: buildSteppedPath(u, xData, yData);
if (!d) return null;
return {
key: `${meta.dataIndex}-${index}`,
color: meta.color,
width: meta.width,
dash: meta.dash,
d,
} as OverlayLine;
})
.filter((line): line is OverlayLine => !!line);
if (nextLines.length > 0) {
setLines((prev) =>
areOverlayLinesEqual(prev, nextLines) ? prev : nextLines,
);
const nextClipRect: ClipRect = {
x: u.bbox.left,
y: u.bbox.top,
width: u.bbox.width,
height: u.bbox.height,
};
setClipRect((prev) =>
isSameClipRect(prev, nextClipRect) ? prev : nextClipRect,
);
}
};
const scheduleRecalc = () => {
if (recalcScheduled) return;
recalcScheduled = true;
rafId = window.requestAnimationFrame(recalc);
};
scheduleRecalc();
if (targetMeta.length === 0) {
return;
}
const addHook = (u: any, hookName: string, fn: () => void) => {
const hooks = u.hooks?.[hookName];
if (!Array.isArray(hooks)) return;
hooks.push(fn);
removeHookFns.push(() => {
const idx = hooks.indexOf(fn);
if (idx >= 0) hooks.splice(idx, 1);
});
};
const onWindowResize = () => scheduleRecalc();
window.addEventListener("resize", onWindowResize);
const attachHooks = (): boolean => {
if (hooksAttached) return true;
const u = uplotRef.current as any;
if (!u) return false;
addHook(u, "draw", scheduleRecalc);
addHook(u, "setScale", scheduleRecalc);
addHook(u, "setData", scheduleRecalc);
const rootElement = u.root as HTMLElement | undefined;
if (rootElement && typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(() => scheduleRecalc());
resizeObserver.observe(rootElement);
}
hooksAttached = true;
scheduleRecalc();
return true;
};
if (!attachHooks()) {
const tryAttach = () => {
if (attachHooks()) return;
bootstrapRafId = window.requestAnimationFrame(tryAttach);
};
bootstrapRafId = window.requestAnimationFrame(tryAttach);
}
return () => {
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
if (bootstrapRafId !== null) {
window.cancelAnimationFrame(bootstrapRafId);
}
removeHookFns.forEach((fn) => fn());
window.removeEventListener("resize", onWindowResize);
resizeObserver?.disconnect();
};
}, [uplotRef, targetMeta, isLiveMode, selectedTimeWindow]);
if (lines.length === 0) {
return null;
}
return (
<svg
className="pointer-events-none absolute inset-0 h-full w-full"
aria-hidden
>
{clipRect && (
<defs>
<clipPath id={clipPathId}>
<rect
x={clipRect.x}
y={clipRect.y}
width={clipRect.width}
height={clipRect.height}
/>
</clipPath>
</defs>
)}
<g clipPath={clipRect ? `url(#${clipPathId})` : undefined}>
{lines.map((line) => {
return (
<path
key={line.key}
d={line.d}
fill="none"
stroke={line.color}
strokeWidth={line.width}
strokeLinecap="butt"
strokeDasharray={line.dash.join(" ")}
strokeDashoffset={0}
/>
);
})}
</g>
</svg>
);
}
+152 -1
View File
@@ -12,6 +12,15 @@ import {
import { BigGraphProps, CreateChartParams } from "./types";
import { normalizeDataSeries } from "./animation";
type CustomDashedTargetLine = {
dataIndex: number;
dash: number[];
color: string;
width: number;
};
const DASH_FLOW_PX_PER_SEC = 180;
export function createChart({
containerRef,
uplotRef,
@@ -152,6 +161,8 @@ export function createChart({
// Build series configuration for ALL series (but control visibility)
const seriesConfig: uPlot.Series[] = [{ label: "Time" }];
const customDashedTargetLines: CustomDashedTargetLine[] = [];
const dashFlowStartTime = performance.now();
// Add ALL data series with visibility control
const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"];
@@ -177,21 +188,35 @@ export function createChart({
});
// Add config lines
let visibleLineIndex = 0;
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
config.lines?.forEach((line) => {
if (line.show !== false) {
const dash =
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const lineDataIndex = firstConfigLineDataIndex + visibleLineIndex;
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
customDashedTargetLines.push({
dataIndex: lineDataIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
seriesConfig.push({
label: line.label,
// Historical dashed targets are rendered in an SVG overlay.
// Historical dashed targets are rendered manually in draw hook to
// avoid dash shimmer while the live x-axis slides.
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
width: line.width ?? 1,
dash: isHistoricalDashedTarget ? undefined : dash,
show: true,
points: { show: false },
});
visibleLineIndex++;
}
});
@@ -321,6 +346,119 @@ export function createChart({
}
},
],
draw: [
(u) => {
if (customDashedTargetLines.length === 0) return;
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length < 2) return;
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xMin === undefined || xMax === undefined) return;
const ctx = u.ctx;
const left = u.bbox.left;
const top = u.bbox.top;
const snap = (v: number) => Math.round(v) + 0.5;
const findFirstGe = (arr: number[], value: number): number => {
let lo = 0;
let hi = arr.length - 1;
let ans = arr.length;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] >= value) {
ans = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return ans;
};
const findLastLe = (arr: number[], value: number): number => {
let lo = 0;
let hi = arr.length - 1;
let ans = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] <= value) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
};
const firstVisibleIdx = Math.max(0, findFirstGe(xData, xMin) - 1);
const lastVisibleIdx = Math.min(
xData.length - 1,
findLastLe(xData, xMax) + 1,
);
if (lastVisibleIdx < firstVisibleIdx) return;
ctx.save();
ctx.beginPath();
ctx.rect(left, top, u.bbox.width, u.bbox.height);
ctx.clip();
ctx.lineCap = "butt";
customDashedTargetLines.forEach(
({ dataIndex, dash, color, width }) => {
const yData = u.data[dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length < 2) return;
const dashPeriod = dash.reduce((acc, curr) => acc + curr, 0);
ctx.setLineDash(dash);
const elapsedMs = performance.now() - dashFlowStartTime;
const dashTravelPx = (elapsedMs * DASH_FLOW_PX_PER_SEC) / 1000;
ctx.lineDashOffset =
dashPeriod > 0 ? -(dashTravelPx % dashPeriod) : 0;
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = firstVisibleIdx; i <= lastVisibleIdx; i++) {
const value = yData[i];
if (value === null || value === undefined) continue;
const x = snap(u.valToPos(xData[i], "x", true));
const y = snap(u.valToPos(value, "y", true));
if (!started) {
ctx.moveTo(x, y);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
ctx.lineTo(x, prevY);
}
if (y !== prevY) {
ctx.lineTo(x, y);
}
prevX = x;
prevY = y;
}
if (started) {
ctx.stroke();
}
},
);
ctx.restore();
},
],
},
scales: {
x: {
@@ -522,12 +660,25 @@ export function createChart({
const cleanup = attachEventHandlers(containerRef.current, handlers);
let dashFlowAnimationFrame: number | null = null;
if (customDashedTargetLines.length > 0) {
const animateDashFlow = () => {
if (!uplotRef.current) return;
uplotRef.current.redraw();
dashFlowAnimationFrame = requestAnimationFrame(animateDashFlow);
};
dashFlowAnimationFrame = requestAnimationFrame(animateDashFlow);
}
animationRefs.lastRenderedData.current = {
timestamps: [...timestamps],
values: [...primaryValues],
};
return () => {
if (dashFlowAnimationFrame !== null) {
cancelAnimationFrame(dashFlowAnimationFrame);
}
cleanup();
};
}
+23 -2
View File
@@ -16,7 +16,9 @@ type Props = {
min: number;
max: number;
onChangeTargetTemp?: (temperature: number) => void;
targetTemperatureEnabled: boolean;
heatingFault?: boolean;
onRetryHeating?: () => void;
targetTemperatureEnabled?: boolean;
};
export function HeatingZone({
title,
@@ -26,7 +28,9 @@ export function HeatingZone({
heatingTimeSeries,
heatingPower,
onChangeTargetTemp,
targetTemperatureEnabled,
heatingFault,
onRetryHeating,
targetTemperatureEnabled = true,
}: Props) {
const targetTemperature = heatingState?.target_temperature ?? 150;
@@ -73,6 +77,23 @@ export function HeatingZone({
Cant Read Temperature! Check Temperature Sensor Wiring!
</StatusBadge>
)}
{heatingFault && (
<div className="flex flex-col gap-2">
<StatusBadge variant="error">
Heating Fault: Temperature not increasing. Extruder set to standby.
</StatusBadge>
{onRetryHeating && (
<button
onClick={onRetryHeating}
className="inline-block w-fit max-w-max rounded bg-blue-600 px-4 py-2 text-base whitespace-nowrap text-white hover:bg-blue-700"
style={{ minWidth: "auto", width: "fit-content" }}
>
Retry Heating
</button>
)}
</div>
)}
</ControlCard>
);
}
@@ -13,6 +13,7 @@ import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { StatusBadge } from "@/control/StatusBadge";
import { GlobalHeatingFaultToastManager } from "./GlobalHeatingFaultToastManager";
export function Extruder3ControlPage() {
const {
@@ -45,8 +46,17 @@ export function Extruder3ControlPage() {
isLoading,
isDisabled,
retryHeating,
acknowledgeHeatingFault,
} = useExtruder3();
// Helper to check if a zone has a heating fault
const getZoneFault = (zone: "front" | "middle" | "back" | "nozzle") => {
const faultZone = state?.heating_fault_state?.fault_zone;
const faultAcknowledged = state?.heating_fault_state?.fault_acknowledged;
return faultZone === zone && !faultAcknowledged;
};
function isZoneReadyForExtrusion(
temperature: number,
targetTemperature: number,
@@ -80,6 +90,10 @@ export function Extruder3ControlPage() {
return (
<Page>
<GlobalHeatingFaultToastManager
state={state}
onAcknowledgeHeatingFault={acknowledgeHeatingFault}
/>
<ControlGrid>
<HeatingZone
title={"Heating Front"}
@@ -89,7 +103,9 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
heatingFault={getZoneFault("front")}
onRetryHeating={retryHeating}
targetTemperatureEnabled
/>
<HeatingZone
title={"Heating Middle"}
@@ -99,7 +115,9 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
heatingFault={getZoneFault("middle")}
onRetryHeating={retryHeating}
targetTemperatureEnabled
/>
<HeatingZone
title={"Heating Back"}
@@ -109,7 +127,9 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
heatingFault={getZoneFault("back")}
onRetryHeating={retryHeating}
targetTemperatureEnabled
/>
<HeatingZone
title={"Heating Nozzle"}
@@ -119,6 +139,8 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
heatingFault={getZoneFault("nozzle")}
onRetryHeating={retryHeating}
targetTemperatureEnabled={
state?.extruder_settings_state.nozzle_temperature_target_enabled ??
true
@@ -16,11 +16,12 @@ export function Extruder3SettingsPage() {
resetInverter,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setHeatingSafeguardEnabled,
setTemperatureTargetEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -69,10 +70,26 @@ export function Extruder3SettingsPage() {
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Heating Safeguard">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.heating_safeguard_enabled ?? true
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setHeatingSafeguardEnabled}
/>
<p className="text-muted-foreground mt-1 text-sm">
When enabled, monitors heating progress and automatically sets to
standby if temperature doesn&apos;t increase by at least 5°C within
60 seconds.
</p>
</Label>
<Label label="Nozzle Temperature Target Enabled">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.nozzle_temperature_target_enabled
state?.extruder_settings_state
.nozzle_temperature_target_enabled ?? true
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
@@ -0,0 +1,157 @@
import React, { useEffect, useRef } from "react";
import { toast } from "sonner";
import { Icon } from "@/components/Icon";
type HeatingFaultState = {
heating_fault_state?: {
fault_zone?: string | null;
fault_acknowledged?: boolean;
};
is_default_state?: boolean;
};
type HeatingFaultToastProps = {
zoneName: string;
onAcknowledge: () => void;
children?: React.ReactNode;
};
function HeatingFaultToast({
zoneName,
onAcknowledge,
children,
}: HeatingFaultToastProps) {
return (
<div className="flex w-100 flex-col gap-3 rounded-xl border border-red-400 bg-red-600 p-4 text-white shadow-xl backdrop-blur-sm transition-all duration-300">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon name="lu:TriangleAlert" className="h-5 w-5 text-yellow-200" />
<strong className="text-lg font-semibold tracking-wide">
Heating Fault
</strong>
</div>
<button
className="rounded-md p-1 text-2xl font-bold text-white/80 hover:bg-red-500 hover:text-white focus:ring-2 focus:ring-white/30 focus:outline-none"
onClick={onAcknowledge}
aria-label="Close"
>
×
</button>
</div>
{children ?? (
<p className="text-base leading-snug text-red-50">
The <strong>{zoneName}</strong> heating zone did not increase in
temperature by at least 5°C within 60 seconds. The extruder has been
automatically set to standby mode.
</p>
)}
</div>
);
}
function toZoneName(faultZone: string | null): string {
switch (faultZone) {
case "front":
return "Front";
case "middle":
return "Middle";
case "back":
return "Back";
case "nozzle":
return "Nozzle";
default:
return "Unknown";
}
}
function useHeatingFaultToastEffect({
state,
onAcknowledgeHeatingFault,
}: {
state: HeatingFaultState | null | undefined;
onAcknowledgeHeatingFault: () => void;
}) {
// Deduplicate by active fault zone
const activeFaultZone = useRef<string | null>(null);
const currentToastId = useRef<string | null>(null);
useEffect(() => {
if (!state) return;
const faultZone = state.heating_fault_state?.fault_zone ?? null;
const faultAcknowledged =
state.heating_fault_state?.fault_acknowledged ?? false;
const isDefault = !!state.is_default_state;
if (isDefault) return;
const faultZoneStr = faultZone ?? "unknown";
const zoneName = toZoneName(faultZone);
if (
faultZone &&
!faultAcknowledged &&
activeFaultZone.current !== faultZone
) {
activeFaultZone.current = faultZone;
const toastId = `heating-fault-${faultZoneStr}`;
currentToastId.current = toastId;
toast(
<HeatingFaultToast
zoneName={zoneName}
onAcknowledge={() => {
onAcknowledgeHeatingFault();
toast.dismiss(toastId);
activeFaultZone.current = null;
currentToastId.current = null;
}}
/>,
{
id: toastId,
duration: Infinity,
position: "top-center",
style: {
background: "transparent",
padding: 0,
boxShadow: "none",
border: "none",
},
},
);
}
if (faultAcknowledged || !faultZone) {
if (currentToastId.current) {
toast.dismiss(currentToastId.current);
currentToastId.current = null;
}
activeFaultZone.current = null;
}
}, [state, onAcknowledgeHeatingFault]);
}
/**
* Watches StateEvent for heating faults and shows toasts.
* Component is non-visual.
*/
export function GlobalHeatingFaultToastManager({
state,
onAcknowledgeHeatingFault,
}: {
state:
| {
heating_fault_state?: {
fault_zone?: string | null;
fault_acknowledged?: boolean;
};
is_default_state?: boolean;
}
| null
| undefined;
onAcknowledgeHeatingFault: () => void;
}) {
useHeatingFaultToastEffect({ state, onAcknowledgeHeatingFault });
return null;
}
@@ -103,13 +103,22 @@ export const heatingStatesSchema = z.object({
middle: heatingStateSchema,
});
/**
* Heating fault state schema
*/
export const heatingFaultStateSchema = z.object({
fault_zone: z.string().nullable(),
fault_acknowledged: z.boolean(),
});
/**
* Extruder settings state schema
*/
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean(),
heating_safeguard_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean().optional(),
});
/**
@@ -178,6 +187,7 @@ export const stateEventDataSchema = z.object({
extruder_settings_state: extruderSettingsStateSchema,
inverter_status_state: inverterStatusStateSchema,
pid_settings: pidSettingsSchema,
heating_fault_state: heatingFaultStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -300,7 +310,6 @@ export function extruder3MessageHandler(
try {
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
const nextTargetPressure = stateEvent.data.pressure_state.target_bar;
@@ -232,18 +232,39 @@ export function useExtruder3() {
);
};
const { request: requestHeatingSafeguardEnabled } = useMachineMutation(
z.object({ SetHeatingSafeguardEnabled: z.boolean() }),
);
const setHeatingSafeguardEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.heating_safeguard_enabled =
enabled;
},
() =>
requestHeatingSafeguardEnabled({
machine_identification_unique: machineIdentification,
data: { SetHeatingSafeguardEnabled: enabled },
}),
);
};
const setTemperatureTargetEnabled = (enabled: boolean) => {
const res = enabled
? true
: confirm(
"This will disable the ability to set a temperature target for the nozzle heating element and will any wiring error message. Only proceed if you understand the risks.",
"This will disable the ability to set a temperature target for the nozzle heating element. Only proceed if you understand the risks.",
);
if (res) {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.nozzle_temperature_target_enabled =
enabled;
(
current.data.extruder_settings_state as {
nozzle_temperature_target_enabled?: boolean;
}
).nozzle_temperature_target_enabled = enabled;
},
() =>
requestNozzleTemperatureTargetEnabled({
@@ -432,6 +453,28 @@ export function useExtruder3() {
z.object({ ResetInverter: z.boolean() }),
);
const { request: requestRetryHeating } = useMachineMutation(
z.object({ RetryHeating: z.literal(true) }),
);
const retryHeating = () => {
requestRetryHeating({
machine_identification_unique: machineIdentification,
data: { RetryHeating: true },
});
};
const { request: requestAcknowledgeHeatingFault } = useMachineMutation(
z.object({ AcknowledgeHeatingFault: z.literal(true) }),
);
const acknowledgeHeatingFault = () => {
requestAcknowledgeHeatingFault({
machine_identification_unique: machineIdentification,
data: { AcknowledgeHeatingFault: true },
});
};
return {
// Consolidated state
state: stateOptimistic.value?.data,
@@ -481,11 +524,14 @@ export function useExtruder3() {
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setHeatingSafeguardEnabled,
setTemperatureTargetEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
resetInverter,
retryHeating,
acknowledgeHeatingFault,
};
}
Generated
+12 -111
View File
@@ -1,21 +1,6 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1771796463,
"narHash": "sha256-9bCDuUzpwJXcHMQYMS1yNuzYMmKO/CCwCexpjWOl62I=",
"owner": "ipetkov",
"repo": "crane",
"rev": "3d3de3313e263e04894f284ac18177bd26169bad",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"crane_2": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -30,7 +15,7 @@
"type": "github"
}
},
"crane_3": {
"crane_2": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -117,51 +102,12 @@
"type": "github"
}
},
"flake-utils_5": {
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1771851181,
"narHash": "sha256-gFgE6mGUftwseV3DUENMb0k0EiHd739lZexPo5O/sdQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "9a4b494b1aa1b93d8edf167f46dc8e0c0011280c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"nixpkgs"
]
},
"locked": {
"lastModified": 1750973805,
"narHash": "sha256-BZXgag7I0rnL/HMHAsBz3tQrfKAibpY2vovexl2lS+Y=",
@@ -176,10 +122,9 @@
"type": "github"
}
},
"home-manager_3": {
"home-manager_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
]
@@ -198,10 +143,9 @@
"type": "github"
}
},
"home-manager_4": {
"home-manager_3": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -223,11 +167,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"lastModified": 1750969886,
"narHash": "sha256-zW/OFnotiz/ndPFdebpo3X0CrbVNf22n4DjN2vxlb58=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"rev": "a676066377a2fe7457369dd37c31fd2263b662f4",
"type": "github"
},
"original": {
@@ -263,31 +207,6 @@
],
"qitech-control": "qitech-control_2"
},
"locked": {
"lastModified": 1772023797,
"narHash": "sha256-RipiUlH5UKiofTRquREluUFOwEfNjLFEvyDol/VWDRg=",
"owner": "qitechgmbh",
"repo": "control",
"rev": "3e3e3f1c0bc2b26fe0fc1e7d49b65e3fd3d7f368",
"type": "github"
},
"original": {
"owner": "qitechgmbh",
"repo": "control",
"type": "github"
}
},
"qitech-control_2": {
"inputs": {
"crane": "crane_3",
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"nixpkgs": [
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_3"
},
"locked": {
"lastModified": 1751047510,
"narHash": "sha256-SZ+DxQvFJKJOro1AVUwWltLQO1L9TIc6AI4tnM9V4tk=",
@@ -302,16 +221,15 @@
"type": "github"
}
},
"qitech-control_3": {
"qitech-control_2": {
"inputs": {
"flake-utils": "flake-utils_4",
"home-manager": "home-manager_4",
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_4",
"qitech-control": "qitech-control_3",
"rust-overlay": "rust-overlay_2"
},
"locked": {
@@ -328,11 +246,10 @@
"type": "github"
}
},
"qitech-control_4": {
"qitech-control_3": {
"inputs": {
"flake-utils": "flake-utils_5",
"flake-utils": "flake-utils_4",
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -383,7 +300,6 @@
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -462,21 +378,6 @@
"repo": "default",
"type": "github"
}
},
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+1 -1
View File
@@ -2,7 +2,7 @@
description = "QiTech Control";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
# Crane for Rust builds with dependency caching
crane = { url = "github:ipetkov/crane"; };
+2
View File
@@ -152,6 +152,8 @@ pub struct HeatingState {
pub struct ExtruderSettingsState {
pub pressure_limit: f64,
pub pressure_limit_enabled: bool,
/// Heating safeguard watchdog enabled (default: true)
pub heating_safeguard_enabled: bool,
pub nozzle_temperature_target_enabled: bool,
}
+3 -1
View File
@@ -94,6 +94,8 @@ impl ExtruderV2 {
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
// For ExtruderV2 the heating safeguard is always enabled and not user-configurable
heating_safeguard_enabled: true,
nozzle_temperature_target_enabled: self
.temperature_controller_nozzle
.get_temperature_target_enabled(),
@@ -215,7 +217,7 @@ impl ExtruderV2 {
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
// === Control functions with emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
+1
View File
@@ -81,6 +81,7 @@ impl MachineNewTrait for ExtruderV2 {
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
heating_safeguard_enabled: true,
nozzle_temperature_target_enabled: true,
},
inverter_status_state: InverterStatusState {
+3 -3
View File
@@ -166,10 +166,10 @@ impl ExtruderV2 {
self.last_energy_calculation_time = Some(now);
}
// Funktionen ohne emit_state bleiben hier
// Functions without emit_state remain here
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
// Set all relays to ZERO
// We don't 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();
+2 -2
View File
@@ -140,14 +140,14 @@ impl MachineNewTrait for ExtruderV2 {
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t4 = TemperatureInput::new(el3204, EL3204Port::T4);
// For the Relais
// For the Relays
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
// The Extruder's temperature controllers should disable the relays 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(
@@ -20,7 +20,8 @@ pub struct TemperatureController {
temperature_pid_output: f64,
heating_element_wattage: f64,
max_clamp: f64,
target_temp_enabled: bool, // Sets whether the frontend should display a target temperature setter for this temp controller
/// When false, target temperature cannot be set (nozzle only; other zones always true)
temperature_target_enabled: bool,
}
impl TemperatureController {
@@ -56,22 +57,22 @@ impl TemperatureController {
temperature_pid_output: 0.0,
heating_element_wattage,
max_clamp,
target_temp_enabled: true,
temperature_target_enabled: true,
}
}
pub fn get_temperature_target_enabled(&self) -> bool {
self.temperature_target_enabled
}
pub fn set_temperature_target_enabled(&mut self, enabled: bool) {
self.temperature_target_enabled = enabled;
}
pub fn set_target_temperature(&mut self, temp: ThermodynamicTemperature) {
self.heating.target_temperature = temp;
}
pub fn set_temperature_target_enabled(&mut self, enabled: bool) {
self.target_temp_enabled = enabled;
}
pub fn get_temperature_target_enabled(&self) -> bool {
self.target_temp_enabled
}
pub const fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
@@ -96,7 +97,7 @@ impl TemperatureController {
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the relais and return
// Disable the relays and return
self.relais.set(false);
self.heating.heating = false;
return;
+4 -1
View File
@@ -41,7 +41,10 @@ impl MachineAct for ExtruderV3 {
self.switch_to_heat();
}
let now = Instant::now();
// Monitor heating watchdog if safeguard is enabled
if self.heating_safeguard_enabled {
self.update_heating_watchdog(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) {
+35 -3
View File
@@ -110,6 +110,8 @@ pub struct StateEvent {
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
/// heating fault state
pub heating_fault_state: HeatingFaultState,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
@@ -117,6 +119,15 @@ pub struct ModeState {
pub mode: ExtruderV3Mode,
}
/// Heating fault information
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct HeatingFaultState {
/// Zone that has a heating fault (None if no fault)
pub fault_zone: Option<String>,
/// Whether a fault has been detected and acknowledged
pub fault_acknowledged: bool,
}
pub enum ExtruderV3Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
@@ -143,6 +154,9 @@ pub enum Mutation {
SetExtruderPressureLimit(f64),
SetExtruderPressureLimitIsEnabled(bool),
// Heating Safeguard
SetHeatingSafeguardEnabled(bool),
// ToggleTemperatureTarget
SetNozzleTemperatureTargetEnabled(bool),
@@ -152,6 +166,10 @@ pub enum Mutation {
// Reset
ResetInverter(bool),
// Heating Fault Handling
RetryHeating,
AcknowledgeHeatingFault,
}
#[derive(Debug)]
@@ -216,7 +234,12 @@ impl MachineApi for ExtruderV3 {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
if self
.temperature_controller_nozzle
.get_temperature_target_enabled()
{
self.set_target_temperature(temp, HeatingType::Nozzle)
}
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
@@ -224,14 +247,23 @@ impl MachineApi for ExtruderV3 {
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetHeatingSafeguardEnabled(enabled) => {
self.set_heating_safeguard_enabled(enabled);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
Mutation::RetryHeating => {
self.retry_heating();
}
Mutation::AcknowledgeHeatingFault => {
self.acknowledge_heating_fault();
}
}
Ok(())
+35 -1
View File
@@ -102,6 +102,7 @@ impl ExtruderV3 {
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
heating_safeguard_enabled: self.heating_safeguard_enabled,
nozzle_temperature_target_enabled: self
.temperature_controller_nozzle
.get_temperature_target_enabled(),
@@ -150,6 +151,7 @@ impl ExtruderV3 {
kd: self.screw_speed_controller.pid.get_kd(),
},
},
heating_fault_state: self.heating_fault_state.clone(),
}
}
}
@@ -235,7 +237,7 @@ impl ExtruderV3 {
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
// === Control functions with emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
@@ -243,9 +245,41 @@ impl ExtruderV3 {
self.emit_state();
}
pub fn set_heating_safeguard_enabled(&mut self, enabled: bool) {
self.heating_safeguard_enabled = enabled;
tracing::info!(
"Heating safeguard {}",
if enabled { "enabled" } else { "disabled" }
);
// Reset watchdog state when toggling
self.heating_watchdog = super::HeatingWatchdog::default();
self.emit_state();
}
pub fn set_nozzle_temperature_target_is_enabled(&mut self, enabled: bool) {
self.temperature_controller_nozzle
.set_temperature_target_enabled(enabled);
tracing::info!(
"Nozzle temperature target {}",
if enabled { "enabled" } else { "disabled" }
);
self.emit_state();
}
pub fn retry_heating(&mut self) {
tracing::info!(
"Retrying heating after fault - resetting watchdog and switching to Heat mode"
);
self.heating_fault_state.fault_zone = None;
self.heating_fault_state.fault_acknowledged = false;
self.heating_watchdog = super::HeatingWatchdog::default();
self.switch_to_heat();
self.emit_state();
}
pub fn acknowledge_heating_fault(&mut self) {
tracing::info!("Heating fault acknowledged by user");
self.heating_fault_state.fault_acknowledged = true;
self.emit_state();
}
+21 -2
View File
@@ -1,15 +1,25 @@
use super::ExtruderV2;
use crate::{
MachineApi,
extruder1::{HeatingType, api::Mutation},
extruder1::{ExtruderV2Mode, HeatingType},
extruder2::ExtruderV3Mode,
extruder2::api::Mutation,
};
fn map_v3_mode_to_v2(mode: ExtruderV3Mode) -> ExtruderV2Mode {
match mode {
ExtruderV3Mode::Standby => ExtruderV2Mode::Standby,
ExtruderV3Mode::Heat => ExtruderV2Mode::Heat,
ExtruderV3Mode::Extrude => ExtruderV2Mode::Extrude,
}
}
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::SetExtruderMode(mode) => self.set_mode_state(map_v3_mode_to_v2(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),
@@ -41,9 +51,18 @@ impl MachineApi for ExtruderV2 {
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetHeatingSafeguardEnabled(_enabled) => {
// Mock implementation - just acknowledge
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
Mutation::RetryHeating => {
// Mock implementation - just acknowledge
}
Mutation::AcknowledgeHeatingFault => {
// Mock implementation - just acknowledge
}
}
Ok(())
}
+32 -6
View File
@@ -1,6 +1,10 @@
use crate::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
api::{ModeState as ModeStateV2, PidSettings, TemperaturePid},
};
use crate::extruder2::ExtruderV3Mode;
use crate::extruder2::api::{
ExtruderV3Events, HeatingFaultState, LiveValuesEvent, ModeState, MotorStatusValues, StateEvent,
};
use crate::extruder2::mock::ExtruderV2;
@@ -14,7 +18,13 @@ impl ExtruderV2 {
StateEvent {
is_default_state: !self.emitted_default_state,
rotation_state: self.rotation_state.clone(),
mode_state: self.mode_state.clone(),
mode_state: ModeState {
mode: match self.mode_state.mode {
ExtruderV2Mode::Standby => ExtruderV3Mode::Standby,
ExtruderV2Mode::Heat => ExtruderV3Mode::Heat,
ExtruderV2Mode::Extrude => ExtruderV3Mode::Extrude,
},
},
regulation_state: self.regulation_state.clone(),
pressure_state: self.pressure_state.clone(),
screw_state: self.screw_state.clone(),
@@ -22,6 +32,10 @@ impl ExtruderV2 {
extruder_settings_state: self.extruder_settings_state.clone(),
inverter_status_state: self.inverter_status_state.clone(),
pid_settings: self.pid_settings.clone(),
heating_fault_state: HeatingFaultState {
fault_zone: None,
fault_acknowledged: false,
},
}
}
@@ -31,7 +45,7 @@ impl ExtruderV2 {
self.last_status_hash = Some(hash);
self.emitted_default_state = true;
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
self.namespace.emit(ExtruderV3Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
@@ -51,7 +65,13 @@ impl ExtruderV2 {
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
motor_status: self.motor_status.clone(),
motor_status: MotorStatusValues {
screw_rpm: self.motor_status.screw_rpm,
frequency: self.motor_status.frequency,
voltage: self.motor_status.voltage,
current: self.motor_status.current,
power: self.motor_status.power,
},
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
front_temperature: self.front_temperature,
@@ -68,7 +88,7 @@ impl ExtruderV2 {
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
@@ -76,6 +96,12 @@ impl ExtruderV2 {
self.emit_state();
}
pub fn set_nozzle_temperature_target_enabled(&mut self, enabled: bool) {
self.extruder_settings_state
.nozzle_temperature_target_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();
@@ -95,7 +121,7 @@ impl ExtruderV2 {
}
pub fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.mode_state = ModeState { mode };
self.mode_state = ModeStateV2 { mode };
self.emit_state();
}
+5 -4
View File
@@ -20,11 +20,12 @@ use crate::{
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingStates, InverterStatusState,
ModeState, MotorStatusValues, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState,
ExtruderSettingsState, HeatingStates, InverterStatusState, ModeState,
MotorStatusValues, PidSettingsStates, PressureState, RegulationState, RotationState,
ScrewState,
},
},
extruder2::api::ExtruderV3Namespace,
};
// Just checking mock-machine feature here to exclude these modules from compilation entirely
@@ -57,7 +58,7 @@ pub struct ExtruderV2 {
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
namespace: ExtruderV3Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
+6 -6
View File
@@ -3,13 +3,12 @@ use crate::{
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
ExtruderSettingsState, HeatingState, HeatingStates, InverterStatusState, ModeState,
MotorStatusValues, PidSettings, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState, TemperaturePid, TemperaturePidStates,
},
},
extruder2::mock::ExtruderV2,
extruder2::{api::ExtruderV3Namespace, mock::ExtruderV2},
};
impl MachineNewTrait for ExtruderV2 {
@@ -39,7 +38,7 @@ impl MachineNewTrait for ExtruderV2 {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: ExtruderV3Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
@@ -81,6 +80,7 @@ impl MachineNewTrait for ExtruderV2 {
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
heating_safeguard_enabled: true,
nozzle_temperature_target_enabled: true,
},
inverter_status_state: InverterStatusState {
+259 -3
View File
@@ -52,6 +52,151 @@ pub enum HeatingType {
Middle,
}
/// Watchdog state for monitoring heating progress
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Clone)]
pub struct HeatingWatchdogZone {
/// Temperature when heating started for this zone
start_temperature: Option<f64>,
/// Timestamp when heating started
heating_start_time: Option<Instant>,
/// Whether a fault has been detected
fault_detected: bool,
/// Last second that was logged
last_logged_second: Option<u64>,
}
#[cfg(not(feature = "mock-machine"))]
impl Default for HeatingWatchdogZone {
fn default() -> Self {
Self {
start_temperature: None,
heating_start_time: None,
fault_detected: false,
last_logged_second: None,
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl HeatingWatchdogZone {
fn check(
&mut self,
controller: &TemperatureController,
zone_name: &str,
now: Instant,
min_temperature_delta_c: f64,
watchdog_timeout_secs: u64,
) -> bool {
use std::time::Duration;
use units::thermodynamic_temperature::degree_celsius;
let current_temp = controller.heating.temperature.get::<degree_celsius>();
let target_temp = controller
.heating
.target_temperature
.get::<degree_celsius>();
let is_heating_active = controller.heating.heating;
// Check if heating should be active
let should_be_heating = target_temp > current_temp;
// Start monitoring when heating becomes active
if should_be_heating && is_heating_active {
if self.start_temperature.is_none() {
self.start_temperature = Some(current_temp);
self.heating_start_time = Some(now);
self.fault_detected = false;
tracing::info!(
"Heating watchdog started for {}: start_temp={:.1}°C, target_temp={:.1}°C",
zone_name,
current_temp,
target_temp
);
}
} else {
// Reset if heating is no longer active
if self.start_temperature.is_some() {
tracing::info!(
"Heating watchdog reset for {}: heating no longer active",
zone_name
);
*self = HeatingWatchdogZone::default();
}
return false; // No fault when not monitoring
}
// Check for fault if we're monitoring
if let (Some(start_temp), Some(start_time)) =
(self.start_temperature, self.heating_start_time)
{
let elapsed = now.duration_since(start_time);
let temp_increase = current_temp - start_temp;
// Check if timeout exceeded without sufficient temperature increase
if elapsed >= Duration::from_secs(watchdog_timeout_secs)
&& temp_increase < min_temperature_delta_c
&& !self.fault_detected
{
self.fault_detected = true;
tracing::warn!(
"Heating watchdog fault detected for {}: elapsed={:.1}s, temp_increase={:.2}°C, start_temp={:.1}°C, current_temp={:.1}°C, target_temp={:.1}°C",
zone_name,
elapsed.as_secs_f64(),
temp_increase,
start_temp,
current_temp,
target_temp
);
return true; // Fault detected
}
// Log progress periodically
let current_second = elapsed.as_secs();
if current_second > 0 && current_second % 10 == 0 {
let should_log = match self.last_logged_second {
Some(last) => current_second > last,
None => true,
};
if should_log {
tracing::debug!(
"Heating watchdog progress for {}: elapsed={:.1}s, temp_increase={:.2}°C",
zone_name,
elapsed.as_secs_f64(),
temp_increase
);
self.last_logged_second = Some(current_second);
}
}
}
false // No fault
}
}
/// Watchdog state for all heating zones
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Clone)]
pub struct HeatingWatchdog {
pub front: HeatingWatchdogZone,
pub middle: HeatingWatchdogZone,
pub back: HeatingWatchdogZone,
pub nozzle: HeatingWatchdogZone,
}
#[cfg(not(feature = "mock-machine"))]
impl Default for HeatingWatchdog {
fn default() -> Self {
Self {
front: HeatingWatchdogZone::default(),
middle: HeatingWatchdogZone::default(),
back: HeatingWatchdogZone::default(),
nozzle: HeatingWatchdogZone::default(),
}
}
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug)]
pub struct ExtruderV3 {
@@ -79,6 +224,15 @@ pub struct ExtruderV3 {
/// 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,
/// Heating safeguard configuration
heating_safeguard_enabled: bool,
/// Heating watchdog state for each zone
heating_watchdog: HeatingWatchdog,
/// Current heating fault state
heating_fault_state: crate::extruder2::api::HeatingFaultState,
}
#[cfg(not(feature = "mock-machine"))]
@@ -143,10 +297,10 @@ impl ExtruderV3 {
self.last_energy_calculation_time = Some(now);
}
// Funktionen ohne emit_state bleiben hier
// Functions without emit_state remain here
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
// Set all relays to ZERO
// We don't 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();
@@ -214,4 +368,106 @@ impl ExtruderV3 {
fn reset_inverter(&mut self) {
self.screw_speed_controller.inverter.reset_inverter();
}
/// Update heating watchdog for all zones
/// Monitors temperature increases and detects heating faults
fn update_heating_watchdog(&mut self, now: Instant) {
// Watchdog parameters
// These defaults are intentionally conservative to avoid false positives
// on older machines or in cold environments. If we do not see at least
// ~5°C increase within one minute, we treat this as a fault.
const MIN_TEMPERATURE_DELTA_C: f64 = 5.0; // Minimum temperature increase in °C
const WATCHDOG_TIMEOUT_SECS: u64 = 60; // Timeout in seconds
// Only monitor when in Heat or Extrude mode
if self.mode == ExtruderV3Mode::Standby {
// Reset all watchdog states when in standby
self.heating_watchdog.front = HeatingWatchdogZone::default();
self.heating_watchdog.middle = HeatingWatchdogZone::default();
self.heating_watchdog.back = HeatingWatchdogZone::default();
self.heating_watchdog.nozzle = HeatingWatchdogZone::default();
return;
}
// Check all zones
let mut fault_detected = false;
if self.heating_watchdog.front.check(
&self.temperature_controller_front,
"front",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
if self.heating_watchdog.middle.check(
&self.temperature_controller_middle,
"middle",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
if self.heating_watchdog.back.check(
&self.temperature_controller_back,
"back",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
if self.heating_watchdog.nozzle.check(
&self.temperature_controller_nozzle,
"nozzle",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
// If fault detected, automatically set to standby and record fault
if fault_detected {
// Determine which zone(s) have faults
let fault_zones: Vec<&str> = [
(self.heating_watchdog.front.fault_detected, "front"),
(self.heating_watchdog.middle.fault_detected, "middle"),
(self.heating_watchdog.back.fault_detected, "back"),
(self.heating_watchdog.nozzle.fault_detected, "nozzle"),
]
.iter()
.filter_map(|(has_fault, zone)| if *has_fault { Some(*zone) } else { None })
.collect();
// In theory fault_detected guarantees at least one zone here, but use a
// defensive default to avoid panicking on an empty list in case the logic
// above is changed in the future.
let first_fault_zone = fault_zones.first().copied().unwrap_or("unknown");
if first_fault_zone == "unknown" {
tracing::error!(
"Heating watchdog reported fault_detected=true but no individual zone was marked as faulty"
);
}
let fault_zone_str = if fault_zones.is_empty() {
"unknown".to_string()
} else {
fault_zones.join(", ")
};
tracing::error!(
"Heating fault detected in zone(s): {} - automatically setting extruder to standby",
fault_zone_str
);
// Set fault state
self.heating_fault_state.fault_zone = Some(first_fault_zone.to_string());
self.heating_fault_state.fault_acknowledged = false;
self.switch_to_standby();
self.emit_state();
}
}
}
+9 -3
View File
@@ -88,7 +88,7 @@ impl MachineNewTrait for ExtruderV3 {
Heating, mitsubishi_cs80::MitsubishiCS80,
screw_speed_controller::ScrewSpeedController,
},
extruder2::{ExtruderV3Mode, api::ExtruderV3Namespace},
extruder2::{ExtruderV3Mode, HeatingWatchdog, api::ExtruderV3Namespace},
};
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
@@ -139,14 +139,14 @@ impl MachineNewTrait for ExtruderV3 {
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t4 = TemperatureInput::new(el3204, EL3204Port::T4);
// For the Relais
// For the Relays
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
// The Extruder's temperature controllers should disable the relays 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(
@@ -241,6 +241,12 @@ impl MachineNewTrait for ExtruderV3 {
screw_speed_controller,
emitted_default_state: false,
last_status_hash: None,
heating_safeguard_enabled: true,
heating_watchdog: HeatingWatchdog::default(),
heating_fault_state: crate::extruder2::api::HeatingFaultState {
fault_zone: None,
fault_acknowledged: false,
},
};
extruder.emit_state();
Ok(extruder)
@@ -10,7 +10,7 @@ use units::thermodynamic_temperature::degree_celsius;
pub struct TemperatureController {
pub pid: PidController,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
relay: DigitalOutput,
pub heating: Heating,
pub target_temp: ThermodynamicTemperature,
window_start: Instant,
@@ -24,7 +24,7 @@ pub struct TemperatureController {
impl TemperatureController {
pub fn disable(&mut self) {
self.relais.set(false);
self.relay.set(false);
self.heating.heating = false;
self.disallow_heating();
}
@@ -36,7 +36,7 @@ impl TemperatureController {
target_temp: ThermodynamicTemperature,
max_temperature: ThermodynamicTemperature,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
relay: DigitalOutput,
heating: Heating,
pwm_duration: Duration,
heating_element_wattage: f64,
@@ -47,7 +47,7 @@ impl TemperatureController {
target_temp,
window_start: Instant::now(),
temperature_sensor,
relais,
relay,
heating,
heating_allowed: false,
pwm_period: pwm_duration,
@@ -86,8 +86,8 @@ impl TemperatureController {
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the relais and return
self.relais.set(false);
// Disable the relays and return
self.relay.set(false);
self.heating.heating = false;
return;
}
@@ -113,7 +113,7 @@ impl TemperatureController {
// Relay is ON if within duty cycle window
let on = elapsed < on_time;
self.relais.set(on);
self.relay.set(on);
self.heating.heating = on;
}
}
+10 -16
View File
@@ -15,7 +15,7 @@ in {
consoleMode = "max"; # Use the highest available resolution
};
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages_6_18;
boot.kernelPackages = pkgs.linuxPackages_6_13;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
@@ -134,6 +134,7 @@ in {
# Enable the X11 windowing system.
services.xserver.enable = true;
#services.xserver.videoDrivers = [ "intel" ];
services.xserver.displayManager.gdm = {
enable = true;
autoSuspend = false;
@@ -185,25 +186,18 @@ in {
powertop.enable = false;
};
# Ensure all power management is disabled
services.logind = {
# This remains for backward compatibility/high-level override
lidSwitch = "ignore";
# Structured settings for logind.conf
settings = {
Login = {
HandlePowerKey = "ignore";
HandleSuspendKey = "ignore";
HandleHibernateKey = "ignore";
HandleLidSwitch = "ignore";
IdleAction = "ignore";
};
};
extraConfig = ''
HandlePowerKey=ignore
HandleSuspendKey=ignore
HandleHibernateKey=ignore
HandleLidSwitch=ignore
IdleAction=ignore
'';
};
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
+13 -2
View File
@@ -3,8 +3,19 @@
home.packages = with pkgs; [ pkgs.qitechPackages.electron ];
xdg.configFile."autostart/de.qitech.control-electron.desktop".source =
"${pkgs.qitechPackages.electron}/share/applications/de.qitech.control-electron.desktop";
# Add autostart entry for the QiTech electron app
xdg.configFile."autostart/de.qitech.control-electron.desktop".text = ''
[Desktop Entry]
Type=Application
Name=QiTech Control
Comment=QiTech Control
Exec=qitech-control-electron
Icon=de.qitech.control-electron
Terminal=false
StartupWMClass=QiTech Control
X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-Phase=Applications
'';
dconf.settings = {
# Set GNOME wallpaper
+2 -1
View File
@@ -10,7 +10,7 @@ buildNpmPackage rec {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
makeCacheWritable = true;
npmDepsHash = "sha256-jW9nogboiuYBzjLNM5rlYsdfp+58OlDf+kxjgNlST70=";
npmDepsHash = "sha256-N2kdS9IKkEMNOPPnRNQ9GvCenR/k88fVyAFzXMJyxs4=";
npmFlags = [ "--no-audit" "--no-fund" ];
installPhase = ''
@@ -44,6 +44,7 @@ buildNpmPackage rec {
--enable-gpu \
--ignore-gpu-blocklist \
--enable-gpu-rasterization \
--disable-software-rasterizer \
"$@"
EOF
chmod +x $out/bin/qitech-control-electron
-3
View File
@@ -30,9 +30,6 @@ docker run --rm \
--workdir /workspace \
nixos/nix:latest \
sh -c "
whoami
ls -lh
cd /workspace
echo 'Enabling flakes and nix-command...'
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' > ~/.config/nix/nix.conf
-868
View File
@@ -1,868 +0,0 @@
{
"name": "control",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"electron": "^38.8.3"
}
},
"node_modules/@electron/get": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
"got": "^11.8.5",
"progress": "^2.0.3",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"global-agent": "^3.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.19.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"license": "MIT",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/electron": {
"version": "38.8.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-38.8.4.tgz",
"integrity": "sha512-vYZ2SH2OhzkCmkbRaijtZLIEJRMnLUL2KQmcAM/7kamf4++3AKSNCBjE1epmQnYFY1M4oKjOUcS3G02ayxR5Bw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
"electron": "cli.js"
},
"engines": {
"node": ">= 12.20.55"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"engines": {
"node": ">=10.0"
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"define-properties": "^1.2.1",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true,
"license": "MIT"
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"semver-compare": "^1.0.0",
"sprintf-js": "^1.1.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"devDependencies": {
"electron": "^38.8.4"
}
}
+2 -1
View File
@@ -33,6 +33,7 @@ use rest::init::start_api_thread;
use serialport::UsbPortInfo;
use smol::{
channel::{Receiver, Sender},
future,
lock::RwLock,
};
use socketioxide::extract::SocketRef;
@@ -412,7 +413,7 @@ fn main() {
app_state.clone(),
));
}
smol::Timer::after(Duration::from_micros(150)).await;
future::yield_now().await;
}
});
}