Compare commits

..
Author SHA1 Message Date
Hans Sebastian Pöhlmann 97c9eee8c2 cargo fmt 2026-02-26 15:37:09 +01:00
Hans Sebastian Pöhlmann 8b7b91907b added directory for minimal machines, moved all test and minimal machines into that dir and updated imports 2026-02-26 15:07:23 +01:00
Achraf Jalleli 35864b4de7 1186 target line static (#1162)
* graph: render historical dashed targets as static overlay

* movement improved

* formatting

* more performance optimizations

* formatting
2026-02-25 16:42:37 +01:00
Robin Krämerandgithub-actions[bot] 621cb1913f 1158 update Linux version, newer electron version (#1161)
* updated electron version

* chore: update npmDepsHash to sha256-jW9nogboiuYBzjLNM5rlYsdfp+58OlDf+kxjgNlST70=

* updated flake.lock

* updated to linux 6.18

* flake updated to 25.11

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-25 16:04:13 +01:00
Christian 3e3e3f1c0b Merge pull request #1159 from qitechgmbh/fix-workflow-permission-issues
Fix workflow permission issues
2026-02-25 13:49:57 +01:00
ChristianandCopilot Autofix powered by AI cdf0374020 Potential fix for code scanning alert no. 11: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-25 12:24:33 +01:00
ChristianandCopilot Autofix powered by AI ec406889f7 Potential fix for code scanning alert no. 12: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-25 12:21:43 +01:00
ChristianandCopilot Autofix powered by AI b731a3bc36 Potential fix for code scanning alert no. 13: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-25 12:20:39 +01:00
Christian 27e15820f7 Add pull request template for better contribution guidelines 2026-02-25 12:00:22 +01:00
155 changed files with 5557 additions and 4241 deletions

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
## 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,6 +8,9 @@ on:
branches:
- master
permissions:
contents: read
defaults:
run:
working-directory: electron
+3
View File
@@ -8,6 +8,9 @@ on:
branches:
- master
permissions:
contents: read
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
+3
View File
@@ -12,6 +12,9 @@ env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.86"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
+5 -22
View File
@@ -71,7 +71,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.0.4",
"@types/react-virtualized": "^9.22.2",
"electron": "^37.3.1",
"electron": "^40.6.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": "37.10.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
"version": "40.6.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-40.6.1.tgz",
"integrity": "sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
"@types/node": "^24.9.0",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -5285,16 +5285,6 @@
"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",
@@ -5330,13 +5320,6 @@
"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": "^37.3.1",
"electron": "^40.6.1",
"electron-playwright-helpers": "^1.7.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
+12 -3
View File
@@ -20,6 +20,7 @@ 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[]) => {
@@ -595,10 +596,18 @@ export function BigGraph({
<div className="flex-1 overflow-hidden rounded-b-3xl pt-4">
<div
ref={containerRef}
className={`h-full w-full overflow-hidden transition-opacity duration-100`}
className={`relative 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>
@@ -0,0 +1,411 @@
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>
);
}
+1 -152
View File
@@ -12,15 +12,6 @@ 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,
@@ -161,8 +152,6 @@ 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"];
@@ -188,35 +177,21 @@ 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 manually in draw hook to
// avoid dash shimmer while the live x-axis slides.
// Historical dashed targets are rendered in an SVG overlay.
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
width: line.width ?? 1,
dash: isHistoricalDashedTarget ? undefined : dash,
show: true,
points: { show: false },
});
visibleLineIndex++;
}
});
@@ -346,119 +321,6 @@ 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: {
@@ -660,25 +522,12 @@ 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();
};
}
@@ -12,9 +12,9 @@ import {
useAnalogInputTestMachineNamespace,
} from "./useAnalogInputTestMachineNamespace";
import { toastError } from "@/components/Toast";
import { MachineIdentificationUnique } from "../types";
import { MachineIdentificationUnique } from "../../types";
import { analogInputTestMachineSerialRoute } from "@/routes/routes";
import { analogInputTestMachine } from "../properties";
import { analogInputTestMachine } from "../../properties";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
@@ -14,7 +14,7 @@ import {
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../client/socketioStore";
} from "../../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
@@ -12,7 +12,7 @@ import { wagoAiTestMachineSerialRoute } from "@/routes/routes";
import { produce } from "immer";
import { useEffect, useMemo, useState } from "react";
import z from "zod";
import { wagoAiTestMachine } from "../properties";
import { wagoAiTestMachine } from "../../properties";
import {
StateEvent,
useWagoAiTestMachineNamespace,
@@ -1,112 +0,0 @@
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import React from "react";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
interface MachineIdentificationUnique {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}
interface Machine {
name: string;
machine_identification_unique: MachineIdentificationUnique;
}
interface MachineSelectorProps {
machines: Machine[];
selectedMachine?: Machine | null;
connectedMachineState?: {
machine_identification_unique?: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
} | null;
is_available: boolean;
};
setConnectedMachine: (machine: MachineIdentificationUnique) => void;
clearConnectedMachine?: () => void;
title?: string;
}
export const MachineSelector: React.FC<MachineSelectorProps> = ({
machines,
selectedMachine,
connectedMachineState,
setConnectedMachine,
clearConnectedMachine,
}) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded border px-4 py-2 text-left">
<Icon name="lu:Settings" className="text-xl" />
<span>
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex min-h-[48px] cursor-pointer items-center gap-2 py-2${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Power" className="text-lg text-green-600" />
) : (
<Icon name="lu:Power" className="text-lg text-gray-400" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:Settings" className="text-lg" />
<span>Clear Selection</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -15,7 +15,7 @@ import { StatusBadge } from "@/control/StatusBadge";
import { useWinder2 } from "./useWinder";
import {
Mode,
OnSpoolLengthTaskCompletedAction,
SpoolAutomaticActionMode,
getGearRatioMultiplier,
} from "./winder2Namespace";
import { TensionArm } from "../TensionArm";
@@ -29,7 +29,6 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { getWinder2TraverseMax } from "./winder2Config";
import { MachineSelector } from "../MachineSelector";
export function Winder2ControlPage() {
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
@@ -39,65 +38,46 @@ export function Winder2ControlPage() {
const {
state,
defaultState,
enableTraverseLaserpointer,
tensionArmAngle,
zeroTensionArmAngle,
spoolRpm,
setMode,
pullerSpeed,
spoolProgress,
setPullerRegulationMode,
setPullerTargetSpeed,
traversePosition,
selectedMachine,
filteredMachines,
isLoading,
isDisabled,
// setters
setMode,
setSpoolDirection,
setSpoolSpeedControlMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setPullerDirection,
setPullerGearRatio,
setPullerSpeedControlMode,
setPullerFixedTargetSpeed,
setPullerAdaptiveBaseSpeed,
setPullerAdaptiveDeviationMax,
setPullerAdaptiveReferenceMachine,
resetSpoolProgress,
setTraverseLimitInner,
setTraverseLimitOuter,
setTraverseStepSize,
setTraversePadding,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setTraverseLaserpointerEnabled,
calibrateTensionArmAngle,
setSpoolLengthTaskTargetLength,
resetSpoolLengthTaskProgress,
setOnSpoolLengthTaskCompletedAction,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
isLoading,
isDisabled,
} = useWinder2();
// Calculate max speed based on gear ratio
const gearRatioMultiplier = getGearRatioMultiplier(
state?.puller_state?.gear_ratio,
);
const maxMotorSpeed = 50; // Maximum motor speed in m/min
const maxPullerSpeed = maxMotorSpeed / gearRatioMultiplier;
const maxMotorSpeed = 75; // Maximum motor speed in m/min
const maxTargetSpeed = maxMotorSpeed / gearRatioMultiplier;
const handleResetProgress = () => {
// Check if the machine is currently in Wind mode
if (state?.mode === "Wind") {
if (state?.mode_state?.mode === "Wind") {
setShowResetConfirmDialog(true);
} else {
resetSpoolLengthTaskProgress();
resetSpoolProgress();
}
};
const confirmResetProgress = () => {
resetSpoolLengthTaskProgress();
resetSpoolProgress();
setShowResetConfirmDialog(false);
};
@@ -151,7 +131,7 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:ArrowLeftToLine"
onClick={gotoTraverseLimitOuter}
disabled={isDisabled || state?.traverse_state?.can_go_out}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Outer Limit
@@ -182,7 +162,7 @@ export function Winder2ControlPage() {
icon="lu:ArrowRightToLine"
onClick={gotoTraverseLimitInner}
disabled={isDisabled}
isLoading={isLoading || state?.traverse_state?.can_go_in}
isLoading={isLoading}
>
Go to Inner Limit
</TouchButton>
@@ -190,12 +170,12 @@ export function Winder2ControlPage() {
</div>
<Label label="Laserpointer">
<SelectionGroupBoolean
value={state?.traverse_state.laserpointer_enabled}
value={state?.traverse_state.laserpointer}
disabled={isLoading || isDisabled}
loading={isLoading}
optionFalse={{ children: "Off", icon: "lu:LightbulbOff" }}
optionTrue={{ children: "On", icon: "lu:Lightbulb" }}
onChange={setTraverseLaserpointerEnabled}
onChange={enableTraverseLaserpointer}
/>
</Label>
<Label label="Home">
@@ -203,7 +183,7 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:House"
onClick={() => gotoTraverseHome()}
disabled={isDisabled || state?.traverse_state?.can_go_home}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Home
@@ -225,20 +205,20 @@ export function Winder2ControlPage() {
<TouchButton
variant="outline"
icon="lu:House"
onClick={calibrateTensionArmAngle}
onClick={zeroTensionArmAngle}
disabled={isDisabled}
isLoading={isLoading}
>
Calibrate
Set Zero Point
</TouchButton>
{!state?.tension_arm_state?.is_calibrated && (
<StatusBadge variant="error">Not Calibrated</StatusBadge>
{!state?.tension_arm_state?.zeroed && (
<StatusBadge variant="error">Not Zeroed</StatusBadge>
)}
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<Mode>
value={state?.mode}
value={state?.mode_state.mode}
disabled={isDisabled}
loading={isLoading}
onChange={setMode}
@@ -267,7 +247,7 @@ export function Winder2ControlPage() {
children: "Wind",
icon: "lu:RefreshCcw",
isActiveClassName: "bg-green-600",
disabled: !state?.can_wind,
disabled: !state?.mode_state?.can_wind,
className: "h-full",
},
}}
@@ -281,98 +261,38 @@ export function Winder2ControlPage() {
timeseries={pullerSpeed}
renderValue={(value) => roundToDecimals(value, 1)}
/>
<Label label="Speed Regulation">
<Label label="Regulation">
<SelectionGroup
value={state?.puller_state?.speed_control_mode}
disabled={isDisabled}
loading={isLoading}
value={state?.puller_state?.regulation}
options={{
Fixed: {
children: "Fixed",
icon: "lu:Crosshair",
Speed: {
children: "Speed",
icon: "lu:Gauge",
},
Adaptive: {
children: "Adaptive",
icon: "lu:Brain",
Diameter: {
children: "Diameter",
icon: "lu:Sun",
disabled: true,
},
}}
onChange={(value) =>
setPullerSpeedControlMode(value)
}
onChange={setPullerRegulationMode}
disabled={isDisabled}
loading={isLoading}
/>
</Label>
<Label label="Target Speed">
<EditValue
value={state?.puller_state?.target_speed}
unit="m/min"
title="Target Speed"
defaultValue={defaultState?.puller_state?.target_speed}
min={0}
max={maxTargetSpeed}
step={0.1}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setPullerTargetSpeed}
/>
</Label>
{state?.puller_state?.speed_control_mode ===
"Fixed" && (
<>
<Label label="Target Speed">
<EditValue
value={state?.puller_state?.fixed_target_speed}
title={"Target Speed"}
unit="m/min"
step={0.1}
min={0}
max={maxPullerSpeed}
defaultValue={
defaultState?.puller_state?.fixed_target_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setPullerFixedTargetSpeed(value)}
/>
</Label>
</>
)}
{state?.puller_state?.speed_control_mode ===
"Adaptive" && (
<>
<Label label="Base Speed">
<EditValue
value={state?.puller_state?.adaptive_base_speed}
title={"Base Speed"}
unit="m/min"
step={0.1}
min={0}
max={maxPullerSpeed}
defaultValue={
defaultState?.puller_state?.adaptive_base_speed
}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) => setPullerAdaptiveBaseSpeed(value)}
/>
</Label>
<Label label="Max Deviation">
<EditValue
value={state?.puller_state?.adaptive_deviation_max}
title={"Max Deviation"}
unit="m/min"
step={10}
min={0.1}
max={maxPullerSpeed}
defaultValue={
defaultState?.puller_state?.adaptive_deviation_max
}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) => setPullerAdaptiveDeviationMax(value)}
/>
</Label>
<Label label="Reference Machine">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.puller_state.adaptive_reference_machine}
setConnectedMachine={(machine) => {
setPullerAdaptiveReferenceMachine(machine);
}}
clearConnectedMachine={() =>
{
if (!selectedMachine) return;
setPullerAdaptiveReferenceMachine(null);
}}
/>
</Label>
</>
)}
</ControlCard>
<ControlCard className="bg-red" title="Spool Autostop">
@@ -385,7 +305,7 @@ export function Winder2ControlPage() {
<Label label="Target Length">
<EditValue
value={state?.spool_length_task_state.target_length}
value={state?.spool_automatic_action_state.spool_required_meters}
unit="m"
title="Expected Meters"
defaultValue={250}
@@ -393,7 +313,7 @@ export function Winder2ControlPage() {
max={10000}
step={10}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={setSpoolLengthTaskTargetLength}
onChange={setSpoolAutomaticRequiredMeters}
/>
</Label>
@@ -407,13 +327,13 @@ export function Winder2ControlPage() {
</TouchButton>
<Label label="After Target Length Reached">
<SelectionGroup<OnSpoolLengthTaskCompletedAction>
<SelectionGroup<SpoolAutomaticActionMode>
value={
state?.spool_length_task_state.on_completed_action
state?.spool_automatic_action_state.spool_automatic_action_mode
}
disabled={isDisabled}
loading={isLoading}
onChange={setOnSpoolLengthTaskCompletedAction}
onChange={setSpoolAutomaticAction}
orientation="vertical"
options={{
Hold: {
@@ -19,6 +19,7 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
targetPullerSpeed,
spoolProgress,
} = useWinder2();
@@ -31,6 +32,8 @@ export function Winder2GraphsPage() {
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSeries={targetPullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
/>
@@ -242,16 +245,30 @@ export function SpoolProgressGraph({
export function PullerSpeedGraph({
syncHook,
newData,
targetSeries,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSeries?: TimeSeries;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const lines: GraphLine[] = [];
if (targetSpeed !== undefined) {
lines.push({
type: "target",
value: targetSpeed,
targetSeries,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
}
const config: GraphConfig = {
title: "Puller Speed",
icon: "lu:Gauge",
@@ -6,7 +6,7 @@ import { PresetsPage } from "@/components/preset/PresetsPage";
import { Preset } from "@/lib/preset/preset";
import {
pullerStateSchema,
spoolStateSchema,
spoolSpeedControllerStateSchema,
} from "./winder2Namespace";
import { z } from "zod";
import {
@@ -26,7 +26,7 @@ const winder2PresetDataSchema = z
})
.partial(),
puller_state: pullerStateSchema.partial(),
spool_speed_controller_state: spoolStateSchema.partial(),
spool_speed_controller_state: spoolSpeedControllerStateSchema.partial(),
})
.partial();
@@ -243,7 +243,7 @@ export function Winder2PresetsPage() {
laserpointer: s?.traverse_state?.laserpointer,
},
puller_state: s?.puller_state ?? {},
spool_speed_controller_state: s?.spool_state ?? {},
spool_speed_controller_state: s?.spool_speed_controller_state ?? {},
});
return (
@@ -22,38 +22,28 @@ export function Winder2SettingPage() {
const {
state,
defaultState,
isLoading,
isDisabled,
// setters
setMode,
setSpoolDirection,
setSpoolSpeedControlMode,
setTraverseStepSize,
setTraversePadding,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseHome,
setPullerForward,
setPullerGearRatio,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolForward,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setPullerDirection,
setPullerGearRatio,
setPullerSpeedControlMode,
setPullerFixedTargetSpeed,
setPullerAdaptiveBaseSpeed,
setPullerAdaptiveDeviationMax,
setPullerAdaptiveReferenceMachine,
setTraverseLimitInner,
setTraverseLimitOuter,
setTraverseStepSize,
setTraversePadding,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setTraverseLaserpointerEnabled,
calibrateTensionArmAngle,
setSpoolLengthTaskTargetLength,
resetSpoolLengthTaskProgress,
setOnSpoolLengthTaskCompletedAction,
isLoading,
isDisabled,
selectedMachine,
filteredMachines,
setConnectedMachine,
disconnectMachine,
} = useWinder2();
const handleXlModeChange = (enabled: boolean) => {
@@ -139,7 +129,7 @@ export function Winder2SettingPage() {
<ControlCard title="Spool">
<Label label="Speed Algorithm">
<SelectionGroup
value={state?.spool_state?.speed_control_mode}
value={state?.spool_speed_controller_state?.regulation_mode}
disabled={isDisabled}
loading={isLoading}
options={{
@@ -153,14 +143,14 @@ export function Winder2SettingPage() {
},
}}
onChange={(value) =>
setSpoolSpeedControlMode(value as "Adaptive" | "MinMax")
setSpoolRegulationMode(value as "Adaptive" | "MinMax")
}
/>
</Label>
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.spool_state?.direction == "Forward"}
value={state?.spool_speed_controller_state?.forward}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
@@ -171,23 +161,23 @@ export function Winder2SettingPage() {
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setSpoolDirection(value ? "Forward" : "Reverse")}
onChange={(value) => setSpoolForward(value)}
/>
</Label>
{state?.spool_state?.speed_control_mode ===
{state?.spool_speed_controller_state?.regulation_mode ===
"MinMax" && (
<>
<Label label="Minimum Speed">
<EditValue
value={state?.spool_state?.minmax_min_speed}
value={state?.spool_speed_controller_state?.minmax_min_speed}
title={"Minimum Speed"}
unit="rpm"
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_state?.minmax_min_speed
defaultState?.spool_speed_controller_state?.minmax_min_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMinSpeed(value)}
@@ -195,14 +185,14 @@ export function Winder2SettingPage() {
</Label>
<Label label="Maximum Speed">
<EditValue
value={state?.spool_state?.minmax_max_speed}
value={state?.spool_speed_controller_state?.minmax_max_speed}
title={"Maximum Speed"}
unit="rpm"
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_state?.minmax_max_speed
defaultState?.spool_speed_controller_state?.minmax_max_speed
}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMaxSpeed(value)}
@@ -211,13 +201,13 @@ export function Winder2SettingPage() {
</>
)}
{state?.spool_state?.speed_control_mode ===
{state?.spool_speed_controller_state?.regulation_mode ===
"Adaptive" && (
<div className="flex flex-row flex-wrap gap-4">
<Label label="Tension Target">
<EditValue
value={
state?.spool_state?.adaptive_tension_target
state?.spool_speed_controller_state?.adaptive_tension_target
}
title={"Tension Target"}
unit={undefined}
@@ -225,7 +215,7 @@ export function Winder2SettingPage() {
min={0}
max={1}
defaultValue={
defaultState?.spool_state
defaultState?.spool_speed_controller_state
?.adaptive_tension_target
}
renderValue={(value) => roundToDecimals(value, 2)}
@@ -235,7 +225,7 @@ export function Winder2SettingPage() {
<Label label="Learning Rate">
<EditValue
value={
state?.spool_state
state?.spool_speed_controller_state
?.adaptive_radius_learning_rate
}
title={"Radius Learning Rate"}
@@ -244,7 +234,7 @@ export function Winder2SettingPage() {
min={0}
max={100}
defaultValue={
defaultState?.spool_state
defaultState?.spool_speed_controller_state
?.adaptive_radius_learning_rate
}
renderValue={(value) => roundToDecimals(value, 2)}
@@ -256,7 +246,7 @@ export function Winder2SettingPage() {
<Label label="Max Speed Multiplier">
<EditValue
value={
state?.spool_state
state?.spool_speed_controller_state
?.adaptive_max_speed_multiplier
}
title={"Max Speed Multiplier"}
@@ -265,7 +255,7 @@ export function Winder2SettingPage() {
min={0.1}
max={10}
defaultValue={
defaultState?.spool_state
defaultState?.spool_speed_controller_state
?.adaptive_max_speed_multiplier
}
renderValue={(value) => roundToDecimals(value, 1)}
@@ -277,7 +267,7 @@ export function Winder2SettingPage() {
<Label label="Acceleration Factor">
<EditValue
value={
state?.spool_state
state?.spool_speed_controller_state
?.adaptive_acceleration_factor
}
title={"Acceleration Factor"}
@@ -286,7 +276,7 @@ export function Winder2SettingPage() {
min={0.01}
max={100}
defaultValue={
defaultState?.spool_state
defaultState?.spool_speed_controller_state
?.adaptive_acceleration_factor
}
renderValue={(value) => roundToDecimals(value, 2)}
@@ -298,7 +288,7 @@ export function Winder2SettingPage() {
<Label label="Deaccel. Urgency">
<EditValue
value={
state?.spool_state
state?.spool_speed_controller_state
?.adaptive_deacceleration_urgency_multiplier
}
title={"Deacceleration Urgency Multiplier"}
@@ -307,7 +297,7 @@ export function Winder2SettingPage() {
min={1}
max={100}
defaultValue={
defaultState?.spool_state
defaultState?.spool_speed_controller_state
?.adaptive_deacceleration_urgency_multiplier
}
renderValue={(value) => roundToDecimals(value, 1)}
@@ -323,7 +313,7 @@ export function Winder2SettingPage() {
<ControlCard title="Puller">
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.puller_state?.direction == "Forward"}
value={state?.puller_state?.forward}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
@@ -334,7 +324,7 @@ export function Winder2SettingPage() {
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setPullerDirection(value ? "Forward" : "Reverse")}
onChange={(value) => setPullerForward(value)}
/>
</Label>
<Label label="Gear Ratio">
@@ -347,23 +337,37 @@ export function Winder2SettingPage() {
children: "1:1",
icon: "lu:Circle",
},
FiveToOne: {
children: "5:1",
OneToFive: {
children: "1:5",
icon: "lu:Cog",
},
TenToOne: {
children: "10:1",
OneToTen: {
children: "1:10",
icon: "lu:Cog",
},
}}
onChange={(value) =>
setPullerGearRatio(
value as "OneToOne" | "FiveToOne" | "TenToOne",
value as "OneToOne" | "OneToFive" | "OneToTen",
)
}
/>
</Label>
</ControlCard>
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_machine_state}
setConnectedMachine={setConnectedMachine}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectMachine(selectedMachine.machine_identification_unique);
}}
/>
</ControlGrid>
</Page>
);
+396 -418
View File
@@ -9,20 +9,18 @@ import { VENDOR_QITECH, winder2 } from "@/machines/properties";
import { winder2SerialRoute } from "@/routes/routes";
import { z } from "zod";
import {
SpoolSpeedControlMode,
SpoolRegulationMode,
StateEvent,
useWinder2Namespace,
modeSchema,
Mode,
spoolSpeedControlModeSchema,
OnSpoolLengthTaskCompletedAction,
spoolLengthTaskCompletedAction,
spoolRegulationModeSchema,
pullerRegulationSchema,
PullerRegulation,
SpoolAutomaticActionMode,
spoolAutomaticActionModeSchema,
gearRatioSchema,
GearRatio,
pullerSpeedControlModeSchema,
directionSchema,
Direction,
PullerSpeedControlMode
} from "./winder2Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
@@ -64,9 +62,10 @@ export function useWinder2() {
defaultState,
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolLengthTaskProgress,
spoolProgress,
} = useWinder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -79,76 +78,25 @@ export function useWinder2() {
}
}, [state]);
// boilerplate junk ends here..
// machine request functions
// Request functions for all operations
const { request: requestTraverseGotoLimitInner } = useMachineMutation(
z.literal("GotoTraverseLimitInner"),
);
const { request: requestTraverseGotoLimitOuter } = useMachineMutation(
z.literal("GotoTraverseLimitOuter"),
);
const { request: requestTraverseGotoHome } = useMachineMutation(
z.literal("GotoTraverseHome"),
);
const { request: requestSetLaserpointer } = useMachineMutation(
z.object({ EnableTraverseLaserpointer: z.boolean() }),
);
const { request: requestModeSet } = useMachineMutation(
z.object({ SetMode: modeSchema }),
);
// spool request functions
const { request: requestSpoolSetDirection } = useMachineMutation(
z.object({ SetSpoolDirection: directionSchema }),
const { request: requestTensionArmZero } = useMachineMutation(
z.literal("ZeroTensionArmAngle"),
);
const { request: requestSpoolSetSpeedControlMode } = useMachineMutation(
z.object({ SetSpoolSpeedControlMode: spoolSpeedControlModeSchema }),
);
const { request: requestSpoolSetMinMaxMinSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMinSpeed: z.number() }),
);
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
);
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
);
const { request: requestSpoolSetAdaptiveRadiusLearningRate } = useMachineMutation(
z.object({ SetSpoolAdaptiveRadiusLearningRate: z.number() }),
);
const { request: requestSpoolSetAdaptiveMaxSpeedMultiplier } = useMachineMutation(
z.object({ SetSpoolAdaptiveMaxSpeedMultiplier: z.number() }),
);
const { request: requestSpoolSetAdaptiveAccelerationFactor } = useMachineMutation(
z.object({ SetSpoolAdaptiveAccelerationFactor: z.number() }),
);
const { request: requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier } = useMachineMutation(
z.object({ SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: z.number() }),
);
// puller request functions
const { request: requestPullerSetDirection } = useMachineMutation(
z.object({ SetPullerDirection: directionSchema }),
);
const { request: requestPullerSetGearRatio } = useMachineMutation(
z.object({ SetPullerGearRatio: gearRatioSchema }),
);
const { request: requestPullerSetSpeedControlMode } = useMachineMutation(
z.object({
SetPullerSpeedControlMode: pullerSpeedControlModeSchema,
}),
);
const { request: requestPullerSetFixedTargetSpeed } = useMachineMutation(
z.object({
SetPullerFixedTargetSpeed: z.number(),
}),
);
const { request: requestPullerSetAdaptiveBaseSpeed } = useMachineMutation(
z.object({
SetPullerAdaptiveBaseSpeed: z.number(),
}),
);
const { request: requestPullerSetAdaptiveDeviationMax } = useMachineMutation(
z.object({
SetPullerAdaptiveDeviationMax: z.number(),
}),
);
const { request: requestPullerSetAdaptiveReferenceMachine } = useMachineMutation(
z.object({
SetPullerAdaptiveReferenceMachine: machineIdentificationUnique.nullable(),
}),
);
// traverse
const { request: requestTraverseSetLimitInner } = useMachineMutation(
z.object({ SetTraverseLimitInner: z.number() }),
);
@@ -161,33 +109,75 @@ export function useWinder2() {
const { request: requestTraverseSetPadding } = useMachineMutation(
z.object({ SetTraversePadding: z.number() }),
);
const { request: requestTraverseGotoLimitInner } = useMachineMutation(
z.literal("GotoTraverseLimitInner"),
const { request: requestPullerSetTargetSpeed } = useMachineMutation(
z.object({ SetPullerTargetSpeed: z.number() }),
);
const { request: requestTraverseGotoLimitOuter } = useMachineMutation(
z.literal("GotoTraverseLimitOuter"),
const { request: requestPullerSetTargetDiameter } = useMachineMutation(
z.object({ SetPullerTargetDiameter: z.number() }),
);
const { request: requestTraverseGotoHome } = useMachineMutation(
z.literal("GotoTraverseHome"),
const { request: requestPullerSetRegulationMode } = useMachineMutation(
z.object({
SetPullerRegulationMode: pullerRegulationSchema,
}),
);
const { request: requestSetLaserpointerEnabled } = useMachineMutation(
z.object({ SetTraverseLaserpointerEnabled: z.boolean() }),
const { request: requestPullerSetForward } = useMachineMutation(
z.object({ SetPullerForward: z.boolean() }),
);
const { request: requestPullerSetGearRatio } = useMachineMutation(
z.object({ SetPullerGearRatio: gearRatioSchema }),
);
const { request: requestSpoolSetRegulationMode } = useMachineMutation(
z.object({ SetSpoolRegulationMode: spoolRegulationModeSchema }),
);
const { request: requestSpoolSetMinMaxMinSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMinSpeed: z.number() }),
);
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
);
const { request: requestSpoolSetForward } = useMachineMutation(
z.object({ SetSpoolForward: z.boolean() }),
);
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
);
const { request: requestSpoolSetAdaptiveRadiusLearningRate } =
useMachineMutation(
z.object({ SetSpoolAdaptiveRadiusLearningRate: z.number() }),
);
const { request: requestSpoolSetAdaptiveMaxSpeedMultiplier } =
useMachineMutation(
z.object({ SetSpoolAdaptiveMaxSpeedMultiplier: z.number() }),
);
const { request: requestSpoolSetAdaptiveAccelerationFactor } =
useMachineMutation(
z.object({ SetSpoolAdaptiveAccelerationFactor: z.number() }),
);
const { request: requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier } =
useMachineMutation(
z.object({ SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: z.number() }),
);
const { request: requestSpoolAutomaticRequiredMeters } = useMachineMutation(
z.object({ SetSpoolAutomaticRequiredMeters: z.number() }),
);
const { request: requestSpoolResetProgress } = useMachineMutation(
z.literal("ResetSpoolProgress"),
);
// tension arm
const { request: requestTensionArmCalibrate } = useMachineMutation(
z.literal("CalibrateTensionArmAngle"),
const { request: requestSpoolAutomaticAction } = useMachineMutation(
z.object({ SetSpoolAutomaticAction: spoolAutomaticActionModeSchema }),
);
// spool length task
const { request: requestSpoolLengthTaskSetTargetLength } = useMachineMutation(
z.object({ SetSpoolLengthTaskTargetLength: z.number() }),
const { request: requestConnectedMachine } = useMachineMutation(
z.object({
SetConnectedMachine: machineIdentificationUnique,
}),
);
const { request: requestSpoolLengthTaskResetProgress } = useMachineMutation(
z.literal("ResetSetSpoolLengthTaskProgress"),
);
const { request: requestSpoolLengthTaskSetOnCompletedAction } = useMachineMutation(
z.object({ SetOnSpoolLengthTaskCompletedAction: spoolLengthTaskCompletedAction }),
const { request: requestDisconnectedMachine } = useMachineMutation(
z.object({
DisconnectMachine: machineIdentificationUnique,
}),
);
// Helper function for optimistic updates using produce
@@ -203,245 +193,19 @@ export function useWinder2() {
};
// Action functions
const setMode = (mode: Mode) => {
const zeroTensionArmAngle = () => {
updateStateOptimistically(
(current) => {
current.data.mode = mode;
current.data.tension_arm_state.zeroed = true;
},
() =>
requestModeSet({
requestTensionArmZero({
machine_identification_unique: machineIdentification,
data: { SetMode: mode },
data: "ZeroTensionArmAngle",
}),
);
};
// spool
const setSpoolDirection = (direction: Direction) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.direction = direction;
},
() =>
requestSpoolSetDirection({
machine_identification_unique: machineIdentification,
data: { SetSpoolDirection: direction },
}),
);
};
const setSpoolSpeedControlMode = (mode: SpoolSpeedControlMode) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.speed_control_mode = mode;
},
() =>
requestSpoolSetSpeedControlMode({
machine_identification_unique: machineIdentification,
data: { SetSpoolSpeedControlMode: mode },
}),
);
};
const setSpoolMinMaxMinSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.minmax_min_speed = speed;
},
() =>
requestSpoolSetMinMaxMinSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMinSpeed: speed },
}),
);
};
const setSpoolMinMaxMaxSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.minmax_max_speed = speed;
},
() =>
requestSpoolSetMinMaxMaxSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMaxSpeed: speed },
}),
);
};
const setSpoolAdaptiveTensionTarget = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_tension_target =
value;
},
() =>
requestSpoolSetAdaptiveTensionTarget({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveTensionTarget: value },
}),
);
};
const setSpoolAdaptiveRadiusLearningRate = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_radius_learning_rate =
value;
},
() =>
requestSpoolSetAdaptiveRadiusLearningRate({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveRadiusLearningRate: value },
}),
);
};
const setSpoolAdaptiveMaxSpeedMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_max_speed_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveMaxSpeedMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveMaxSpeedMultiplier: value },
}),
);
};
const setSpoolAdaptiveAccelerationFactor = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_acceleration_factor =
value;
},
() =>
requestSpoolSetAdaptiveAccelerationFactor({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveAccelerationFactor: value },
}),
);
};
const setSpoolAdaptiveDeaccelerationUrgencyMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_state.adaptive_deacceleration_urgency_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: value },
}),
);
};
// puller
const setPullerDirection = (direction: Direction) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.direction = direction;
},
() =>
requestPullerSetDirection({
machine_identification_unique: machineIdentification,
data: { SetPullerDirection: direction },
}),
);
};
const setPullerGearRatio = (gearRatio: GearRatio) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.gear_ratio = gearRatio;
},
() => {
requestPullerSetGearRatio({
machine_identification_unique: machineIdentification,
data: { SetPullerGearRatio: gearRatio },
});
},
);
};
const setPullerSpeedControlMode = (mode: PullerSpeedControlMode) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.speed_control_mode = mode;
},
() =>
requestPullerSetSpeedControlMode({
machine_identification_unique: machineIdentification,
data: { SetPullerSpeedControlMode: mode },
}),
);
};
const setPullerFixedTargetSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.fixed_target_speed = speed;
},
() =>
requestPullerSetFixedTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerFixedTargetSpeed: speed },
}),
);
};
const setPullerAdaptiveBaseSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.fixed_target_speed = speed;
},
() =>
requestPullerSetAdaptiveBaseSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerAdaptiveBaseSpeed: speed },
}),
);
};
const setPullerAdaptiveDeviationMax = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.fixed_target_speed = speed;
},
() =>
requestPullerSetAdaptiveDeviationMax({
machine_identification_unique: machineIdentification,
data: { SetPullerAdaptiveDeviationMax: speed },
}),
);
};
const setPullerAdaptiveReferenceMachine = (machineUid: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
} | null) => {
updateStateOptimistically(
(current) => {
current.data.puller_state
.adaptive_reference_machine
.machine_identification_unique
= machineUid;
},
() =>
requestPullerSetAdaptiveReferenceMachine({
machine_identification_unique: machineIdentification,
data: { SetPullerAdaptiveReferenceMachine: machineUid },
}),
);
};
// traverse
const setTraverseLimitInner = (limitInner: number) => {
updateStateOptimistically(
(current) => {
@@ -468,32 +232,6 @@ export function useWinder2() {
);
};
const setTraverseStepSize = (stepSize: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.step_size = stepSize;
},
() =>
requestTraverseSetStepSize({
machine_identification_unique: machineIdentification,
data: { SetTraverseStepSize: stepSize },
}),
);
};
const setTraversePadding = (padding: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.padding = padding;
},
() =>
requestTraverseSetPadding({
machine_identification_unique: machineIdentification,
data: { SetTraversePadding: padding },
}),
);
};
const gotoTraverseLimitInner = () => {
updateStateOptimistically(
(current) => {
@@ -533,64 +271,325 @@ export function useWinder2() {
);
};
const setTraverseLaserpointerEnabled = (enabled: boolean) => {
const enableTraverseLaserpointer = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.laserpointer_enabled = enabled;
current.data.traverse_state.laserpointer = enabled;
},
() =>
requestSetLaserpointerEnabled({
requestSetLaserpointer({
machine_identification_unique: machineIdentification,
data: { SetTraverseLaserpointerEnabled: enabled },
data: { EnableTraverseLaserpointer: enabled },
}),
);
};
// tension arm
const calibrateTensionArmAngle = () => {
const setMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.tension_arm_state.is_calibrated = true;
current.data.mode_state.mode = mode;
},
() =>
requestTensionArmCalibrate({
requestModeSet({
machine_identification_unique: machineIdentification,
data: "CalibrateTensionArmAngle",
data: { SetMode: mode },
}),
);
};
// spool length task
const setSpoolLengthTaskTargetLength = (target_length: number) => {
const setTraverseStepSize = (stepSize: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_length_task_state.target_length = target_length;
current.data.traverse_state.step_size = stepSize;
},
() =>
requestSpoolLengthTaskSetTargetLength({
requestTraverseSetStepSize({
machine_identification_unique: machineIdentification,
data: { SetSpoolLengthTaskTargetLength: target_length },
data: { SetTraverseStepSize: stepSize },
}),
);
};
const resetSpoolLengthTaskProgress = () => {
requestSpoolLengthTaskResetProgress({
const setTraversePadding = (padding: number) => {
updateStateOptimistically(
(current) => {
current.data.traverse_state.padding = padding;
},
() =>
requestTraverseSetPadding({
machine_identification_unique: machineIdentification,
data: { SetTraversePadding: padding },
}),
);
};
const setPullerTargetSpeed = (targetSpeed: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.target_speed = targetSpeed;
},
() =>
requestPullerSetTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetSpeed: targetSpeed },
}),
);
};
const setPullerTargetDiameter = (targetDiameter: number) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.target_diameter = targetDiameter;
},
() =>
requestPullerSetTargetDiameter({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetDiameter: targetDiameter },
}),
);
};
const setPullerRegulationMode = (regulationMode: PullerRegulation) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.regulation = regulationMode;
},
() =>
requestPullerSetRegulationMode({
machine_identification_unique: machineIdentification,
data: { SetPullerRegulationMode: regulationMode },
}),
);
};
const setPullerForward = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.forward = forward;
},
() =>
requestPullerSetForward({
machine_identification_unique: machineIdentification,
data: { SetPullerForward: forward },
}),
);
};
const setPullerGearRatio = (gearRatio: GearRatio) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.gear_ratio = gearRatio;
// Reset target speed to 0 to prevent sudden speed changes
current.data.puller_state.target_speed = 0;
},
async () => {
// First set the target speed to 0
await requestPullerSetTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetSpeed: 0 },
});
// Then set the gear ratio
await requestPullerSetGearRatio({
machine_identification_unique: machineIdentification,
data: { SetPullerGearRatio: gearRatio },
});
},
);
};
const setSpoolAutomaticRequiredMeters = (meters: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_automatic_action_state.spool_required_meters =
meters;
},
() =>
requestSpoolAutomaticRequiredMeters({
machine_identification_unique: machineIdentification,
data: { SetSpoolAutomaticRequiredMeters: meters },
}),
);
};
const setSpoolAutomaticAction = (mode: SpoolAutomaticActionMode) => {
updateStateOptimistically(
(current) => {
current.data.spool_automatic_action_state.spool_automatic_action_mode =
mode;
},
() =>
requestSpoolAutomaticAction({
machine_identification_unique: machineIdentification,
data: { SetSpoolAutomaticAction: mode },
}),
);
};
const resetSpoolProgress = () => {
requestSpoolResetProgress({
machine_identification_unique: machineIdentification,
data: "ResetSetSpoolLengthTaskProgress",
data: "ResetSpoolProgress",
});
};
const setOnSpoolLengthTaskCompletedAction = (action: OnSpoolLengthTaskCompletedAction) => {
const setSpoolRegulationMode = (mode: SpoolRegulationMode) => {
updateStateOptimistically(
(current) => {
current.data.spool_length_task_state.on_completed_action =
action;
current.data.spool_speed_controller_state.regulation_mode = mode;
},
() =>
requestSpoolLengthTaskSetOnCompletedAction({
requestSpoolSetRegulationMode({
machine_identification_unique: machineIdentification,
data: { SetOnSpoolLengthTaskCompletedAction: action },
data: { SetSpoolRegulationMode: mode },
}),
);
};
const setSpoolMinMaxMinSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.minmax_min_speed = speed;
},
() =>
requestSpoolSetMinMaxMinSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMinSpeed: speed },
}),
);
};
const setSpoolMinMaxMaxSpeed = (speed: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.minmax_max_speed = speed;
},
() =>
requestSpoolSetMinMaxMaxSpeed({
machine_identification_unique: machineIdentification,
data: { SetSpoolMinMaxMaxSpeed: speed },
}),
);
};
const setSpoolForward = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.forward = forward;
},
() =>
requestSpoolSetForward({
machine_identification_unique: machineIdentification,
data: { SetSpoolForward: forward },
}),
);
};
const setSpoolAdaptiveTensionTarget = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_tension_target =
value;
},
() =>
requestSpoolSetAdaptiveTensionTarget({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveTensionTarget: value },
}),
);
};
const setSpoolAdaptiveRadiusLearningRate = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_radius_learning_rate =
value;
},
() =>
requestSpoolSetAdaptiveRadiusLearningRate({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveRadiusLearningRate: value },
}),
);
};
const setSpoolAdaptiveMaxSpeedMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_max_speed_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveMaxSpeedMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveMaxSpeedMultiplier: value },
}),
);
};
const setSpoolAdaptiveAccelerationFactor = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_acceleration_factor =
value;
},
() =>
requestSpoolSetAdaptiveAccelerationFactor({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveAccelerationFactor: value },
}),
);
};
const setSpoolAdaptiveDeaccelerationUrgencyMultiplier = (value: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.adaptive_deacceleration_urgency_multiplier =
value;
},
() =>
requestSpoolSetAdaptiveDeaccelerationUrgencyMultiplier({
machine_identification_unique: machineIdentification,
data: { SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: value },
}),
);
};
const setConnectedMachine = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_machine_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedMachine({
machine_identification_unique,
data: { SetConnectedMachine: machineIdentificationUnique },
}),
);
};
const disconnectMachine = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_machine_state.machine_identification_unique =
null;
},
() =>
requestDisconnectedMachine({
machine_identification_unique,
data: { DisconnectMachine: machineIdentificationUnique },
}),
);
};
@@ -609,7 +608,7 @@ export function useWinder2() {
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0006, // id of laser machine
0x0008,
),
[machines, machineIdentification],
);
@@ -617,7 +616,7 @@ export function useWinder2() {
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.puller_state.adaptive_reference_machine?.machine_identification_unique
state?.data.connected_machine_state?.machine_identification_unique
?.serial;
return (
@@ -640,65 +639,44 @@ export function useWinder2() {
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress: spoolLengthTaskProgress,
spoolProgress,
// Loading states
isLoading,
isDisabled,
// Action functions
//---------------------------------------------------------
// machine
enableTraverseLaserpointer,
setMode,
// spool
setSpoolDirection,
setSpoolSpeedControlMode,
// spool speed minmax strategy
zeroTensionArmAngle,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
resetSpoolProgress,
setTraverseStepSize,
setTraversePadding,
setPullerTargetSpeed,
setPullerTargetDiameter,
setPullerRegulationMode,
setPullerForward,
setPullerGearRatio,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
// spool speed adaptive strategy
setSpoolForward,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
// puller
setPullerDirection,
setPullerGearRatio,
setPullerSpeedControlMode,
// puller speed fixed strategy
setPullerFixedTargetSpeed,
// puller speed adaptive strategy
setPullerAdaptiveBaseSpeed,
setPullerAdaptiveDeviationMax,
setPullerAdaptiveReferenceMachine,
// traverse
setTraverseLimitInner,
setTraverseLimitOuter,
setTraverseStepSize,
setTraversePadding,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setTraverseLaserpointerEnabled,
// tension arm
calibrateTensionArmAngle,
// spool length task
setSpoolLengthTaskTargetLength,
resetSpoolLengthTaskProgress,
setOnSpoolLengthTaskCompletedAction,
setConnectedMachine,
disconnectMachine,
};
}
@@ -28,14 +28,38 @@ export const liveValuesEventDataSchema = z.object({
puller_speed: z.number(),
spool_rpm: z.number(),
tension_arm_angle: z.number(),
spool_length_task_progress: z.number(),
spool_progress: z.number(),
});
/**
* Direction enum
* Puller regulation type enum
*/
export const directionSchema = z.enum(["Forward", "Reverse"]);
export type Direction = z.infer<typeof directionSchema>;
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
/**
* Gear ratio enum for winding speed
*/
export const gearRatioSchema = z.enum(["OneToOne", "OneToFive", "OneToTen"]);
export type GearRatio = z.infer<typeof gearRatioSchema>;
/**
* Get the multiplier for a gear ratio
*/
export function getGearRatioMultiplier(
gearRatio: GearRatio | undefined,
): number {
switch (gearRatio) {
case "OneToOne":
return 1.0;
case "OneToFive":
return 5.0;
case "OneToTen":
return 10.0;
default:
return 1.0;
}
}
/**
* Machine operation mode enum
@@ -43,6 +67,74 @@ export type Direction = z.infer<typeof directionSchema>;
export const modeSchema = z.enum(["Standby", "Hold", "Pull", "Wind"]);
export type Mode = z.infer<typeof modeSchema>;
/**
* Machine operation mode enum
*/
export const spoolAutomaticActionModeSchema = z.enum([
"NoAction",
"Pull",
"Hold",
]);
export type SpoolAutomaticActionMode = z.infer<
typeof spoolAutomaticActionModeSchema
>;
/**
* Spool speed controller regulation mode enum
*/
export const spoolRegulationModeSchema = z.enum(["Adaptive", "MinMax"]);
export type SpoolRegulationMode = z.infer<typeof spoolRegulationModeSchema>;
export const spoolAutomaticActionStateSchema = z.object({
spool_required_meters: z.number(),
spool_automatic_action_mode: spoolAutomaticActionModeSchema,
});
export type SpoolAutomaticActionState = z.infer<
typeof spoolAutomaticActionStateSchema
>;
/**
* Traverse state schema
*/
export const traverseStateSchema = z.object({
limit_inner: z.number(),
limit_outer: z.number(),
position_in: z.number(),
position_out: z.number(),
is_going_in: z.boolean(),
is_going_out: z.boolean(),
is_homed: z.boolean(),
is_going_home: z.boolean(),
is_traversing: z.boolean(),
laserpointer: z.boolean(),
step_size: z.number(),
padding: z.number(),
can_go_in: z.boolean(),
can_go_out: z.boolean(),
can_go_home: z.boolean(),
});
/**
* Puller state schema
*/
export const pullerStateSchema = z.object({
regulation: pullerRegulationSchema,
target_speed: z.number(),
target_diameter: z.number(),
forward: z.boolean(),
gear_ratio: gearRatioSchema,
});
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
can_wind: z.boolean(),
});
/**
* Connected machine state scheme
*/
@@ -56,147 +148,45 @@ export const machineIdentificationUniqueSchema = z.object({
serial: z.number(),
});
export const machineCrossConnectionStateSchema = z.object({
export const connectedMachineStateSchema = z.object({
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
is_available: z.boolean(),
});
/**
* Gear ratio enum for winding speed
*/
export const gearRatioSchema = z.enum(["OneToOne", "FiveToOne", "TenToOne"]);
export type GearRatio = z.infer<typeof gearRatioSchema>;
/**
* Get the multiplier for a gear ratio
*/
export function getGearRatioMultiplier(
gearRatio: GearRatio | undefined,
): number {
switch (gearRatio) {
case "OneToOne":
return 1.0;
case "FiveToOne":
return 5.0;
case "TenToOne":
return 10.0;
default:
return 1.0;
}
}
/**
* Machine operation mode enum
*/
export const spoolLengthTaskCompletedAction = z.enum(["NoAction", "Pull", "Hold"]);
export type OnSpoolLengthTaskCompletedAction = z.infer<typeof spoolLengthTaskCompletedAction>;
/**
* Spool speed controller regulation mode enum
*/
export const spoolSpeedControlModeSchema = z.enum(["Adaptive", "MinMax"]);
export type SpoolSpeedControlMode = z.infer<typeof spoolSpeedControlModeSchema>;
/**
* Spool speed controller regulation mode enum
*/
export const pullerSpeedControlModeSchema = z.enum(["Fixed", "Adaptive"]);
export type PullerSpeedControlMode = z.infer<typeof pullerSpeedControlModeSchema>;
/**
* Spool speed controller state schema
*/
export const spoolStateSchema = z.object({
direction: directionSchema,
speed_control_mode: spoolSpeedControlModeSchema,
// min max mode
minmax_min_speed: z.number(),
minmax_max_speed: z.number(),
// adaptive mode
adaptive_tension_target: z.number(),
adaptive_radius_learning_rate: z.number(),
adaptive_max_speed_multiplier: z.number(),
adaptive_acceleration_factor: z.number(),
adaptive_deacceleration_urgency_multiplier: z.number(),
});
/**
* Puller state schema
*/
export const pullerStateSchema = z.object({
direction: directionSchema,
gear_ratio: gearRatioSchema,
speed_control_mode: pullerSpeedControlModeSchema,
fixed_target_speed: z.number(),
adaptive_base_speed: z.number(),
adaptive_deviation_max: z.number(),
adaptive_reference_machine: machineCrossConnectionStateSchema,
});
/**
* Traverse state schema
*/
export const traverseStateSchema = z.object({
// config
limit_inner: z.number(),
limit_outer: z.number(),
step_size: z.number(),
padding: z.number(),
// states
is_going_in: z.boolean(),
is_going_out: z.boolean(),
is_homed: z.boolean(),
is_going_home: z.boolean(),
is_traversing: z.boolean(),
// state transitions
can_go_in: z.boolean(),
can_go_out: z.boolean(),
can_go_home: z.boolean(),
// lazeeeeeeeer
laserpointer_enabled: z.boolean(),
});
/**
* Tension arm state schema
*/
export const tensionArmStateSchema = z.object({
is_calibrated: z.boolean(),
zeroed: z.boolean(),
});
/**
* spool length task state schema
* Spool speed controller state schema
*/
export const spoolLengthTaskStateSchema = z.object({
target_length: z.number(),
on_completed_action: spoolLengthTaskCompletedAction,
export const spoolSpeedControllerStateSchema = z.object({
regulation_mode: spoolRegulationModeSchema,
minmax_min_speed: z.number(),
minmax_max_speed: z.number(),
adaptive_tension_target: z.number(),
adaptive_radius_learning_rate: z.number(),
adaptive_max_speed_multiplier: z.number(),
adaptive_acceleration_factor: z.number(),
adaptive_deacceleration_urgency_multiplier: z.number(),
forward: z.boolean(),
});
export type SpoolLengthTaskState = z.infer<typeof spoolLengthTaskStateSchema>;
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
// common
is_default_state: z.boolean(),
// machine
mode: modeSchema,
can_wind: z.boolean(),
// devices
spool_state: spoolStateSchema,
puller_state: pullerStateSchema,
traverse_state: traverseStateSchema,
puller_state: pullerStateSchema,
mode_state: modeStateSchema,
tension_arm_state: tensionArmStateSchema,
// tasks
spool_length_task_state: spoolLengthTaskStateSchema,
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
connected_machine_state: connectedMachineStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -216,11 +206,12 @@ export type Winder2NamespaceStore = {
defaultState: StateEvent | null;
// Time series data for live values
spoolRpm: TimeSeries;
pullerSpeed: TimeSeries;
traversePosition: TimeSeries;
tensionArmAngle: TimeSeries;
spoolLengthTaskProgress: TimeSeries;
pullerSpeed: TimeSeries;
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
targetPullerSpeed: TimeSeries;
};
//Store Factory and Message Handler -> no param, so default values
@@ -251,9 +242,10 @@ export const createWinder2NamespaceStore =
// Time series data for live values
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolLengthTaskProgress: spoolProgress,
spoolProgress,
};
});
/**
@@ -281,21 +273,28 @@ export function winder2MessageHandler(
};
try {
if (eventName === "State")
{
if (eventName === "StateEvent") {
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
const nextTargetPullerSpeed = stateEvent.data.puller_state.target_speed;
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed:
state.targetPullerSpeed.current?.value === nextTargetPullerSpeed
? state.targetPullerSpeed
: addTargetPullerSpeed(state.targetPullerSpeed, {
value: nextTargetPullerSpeed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValues") {
} else if (eventName === "LiveValuesEvent") {
// Parse and validate the live values event
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -305,7 +304,7 @@ export function winder2MessageHandler(
puller_speed,
spool_rpm,
tension_arm_angle,
spool_length_task_progress: spool_progress,
spool_progress,
} = liveValuesEvent.data;
const timestamp = liveValuesEvent.ts;
@@ -328,8 +327,8 @@ export function winder2MessageHandler(
value: spool_progress,
timestamp,
};
newState.spoolLengthTaskProgress = addSpoolProgress(
state.spoolLengthTaskProgress,
newState.spoolProgress = addSpoolProgress(
state.spoolProgress,
timeseriesValue,
);
}
+23 -23
View File
@@ -43,44 +43,44 @@ import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { Mock1ControlPage } from "@/machines/minimal_machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/minimal_machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/minimal_machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/minimal_machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/minimal_machines/mock/mock1/Mock1PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Settings";
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { TestMachinePage } from "@/machines/minimal_machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/minimal_machines/testmachine/TestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
import { WagoAiTestMachineControl } from "@/machines/wagoaitestmachine/WagoAiTestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/minimal_machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/minimal_machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/minimal_machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/minimal_machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/minimal_machines/wagoaitestmachine/WagoAiTestMachinePage";
import { WagoAiTestMachineControl } from "@/machines/minimal_machines/wagoaitestmachine/WagoAiTestMachineControlPage";
import { DigitalInputTestMachinePage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachinePage";
import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
import { DigitalInputTestMachinePage } from "@/machines/minimal_machines/digitalinputtestmachine/DigitalInputTestMachinePage";
import { DigitalInputTestMachineControlPage } from "@/machines/minimal_machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/motor_test_machine/TestMotorControlPage";
import { IP20TestMachinePage } from "@/machines/minimal_machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/minimal_machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/minimal_machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/minimal_machines/motor_test_machine/TestMotorControlPage";
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
import { WagoPower1Page } from "@/machines/wago_power/wago_power1/WagoPower1Page";
import { WagoPower1ControlPage } from "@/machines/wago_power/wago_power1/WagoPower1ControlPage";
import { WagoDoTestMachinePage } from "@/machines/wagodotestmachine/WagoDoTestMachinePage";
import { WagoDoTestMachineControlPage } from "@/machines/wagodotestmachine/WagoDoTestMachineControlPage";
import { Wago8chDioTestMachinePage } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachinePage";
import { Wago8chDioTestMachineControlRoute } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachineControlPage";
import { WagoDoTestMachinePage } from "@/machines/minimal_machines/wagodotestmachine/WagoDoTestMachinePage";
import { WagoDoTestMachineControlPage } from "@/machines/minimal_machines/wagodotestmachine/WagoDoTestMachineControlPage";
import { Wago8chDioTestMachinePage } from "@/machines/minimal_machines/wago8chdiotestmachine/wago8chDioTestMachinePage";
import { Wago8chDioTestMachineControlRoute } from "@/machines/minimal_machines/wago8chdiotestmachine/wago8chDioTestMachineControlPage";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
Generated
+111 -12
View File
@@ -1,6 +1,21 @@
{
"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=",
@@ -15,7 +30,7 @@
"type": "github"
}
},
"crane_2": {
"crane_3": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
@@ -102,12 +117,51 @@
"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=",
@@ -122,9 +176,10 @@
"type": "github"
}
},
"home-manager_2": {
"home-manager_3": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
]
@@ -143,9 +198,10 @@
"type": "github"
}
},
"home-manager_3": {
"home-manager_4": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -167,11 +223,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1750969886,
"narHash": "sha256-zW/OFnotiz/ndPFdebpo3X0CrbVNf22n4DjN2vxlb58=",
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a676066377a2fe7457369dd37c31fd2263b662f4",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
@@ -207,6 +263,31 @@
],
"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=",
@@ -221,15 +302,16 @@
"type": "github"
}
},
"qitech-control_2": {
"qitech-control_3": {
"inputs": {
"flake-utils": "flake-utils_3",
"home-manager": "home-manager_3",
"flake-utils": "flake-utils_4",
"home-manager": "home-manager_4",
"nixpkgs": [
"qitech-control",
"qitech-control",
"nixpkgs"
],
"qitech-control": "qitech-control_3",
"qitech-control": "qitech-control_4",
"rust-overlay": "rust-overlay_2"
},
"locked": {
@@ -246,10 +328,11 @@
"type": "github"
}
},
"qitech-control_3": {
"qitech-control_4": {
"inputs": {
"flake-utils": "flake-utils_4",
"flake-utils": "flake-utils_5",
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -300,6 +383,7 @@
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"qitech-control",
"qitech-control",
"qitech-control",
"nixpkgs"
@@ -378,6 +462,21 @@
"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.05";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
# Crane for Rust builds with dependency caching
crane = { url = "github:ipetkov/crane"; };
-1
View File
@@ -61,7 +61,6 @@ impl MachineAct for AquaPathV1 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-1
View File
@@ -49,7 +49,6 @@ impl MachineAct for BufferV1 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-1
View File
@@ -77,7 +77,6 @@ impl MachineAct for ExtruderV2 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-1
View File
@@ -51,7 +51,6 @@ impl MachineAct for ExtruderV2 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-1
View File
@@ -80,7 +80,6 @@ impl MachineAct for ExtruderV3 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-1
View File
@@ -48,7 +48,6 @@ impl MachineAct for ExtruderV2 {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
-139
View File
@@ -1,139 +0,0 @@
use units::angle::revolution;
use units::f64::*;
use super::{Clamp, ClampedValue};
pub fn clamp_revolution_uom(value: Angle, min: Angle, max: Angle) -> ClampedValue<Angle>
{
let value = value.get::<revolution>();
let min = min.get::<revolution>();
let max = max.get::<revolution>();
// clamp
let clamped_value = clamp_revolution(value, min, max);
// convert back to uom
ClampedValue {
value: Angle::new::<revolution>(clamped_value.value),
clamp: clamped_value.clamp
}
}
pub fn clamp_revolution(value: f64, min: f64, max: f64) -> ClampedValue<f64>
{
// normalize value from 0..1
let value = wrap_revolution(value);
let min = wrap_revolution(min);
let max = wrap_revolution(max);
// check if in acceptable range
if revolution_in_range(value, min, max) {
return ClampedValue::new(value, Clamp::None);
}
// calculates the distance between min and max
let (clamp_to_min_min, clamp_to_min_max, clamp_to_max_min, clamp_to_max_max) =
clamping_ranges(min, max);
// check if in min clamping range
if revolution_in_range(value, clamp_to_min_min, clamp_to_min_max) {
return ClampedValue::new(min, Clamp::Min);
}
// check if in max clamping range
if revolution_in_range(value, clamp_to_max_min, clamp_to_max_max) {
return ClampedValue::new(max, Clamp::Max);
}
// at this point our input value should be either retured (cause in spec) or clamped to min or max
// so this point should never be reached
// in case it does we just clamp to min
ClampedValue::new(min, Clamp::Min)
//TODO: consider using unreachable!() if truly unreachable
}
pub fn scale_revolution_to_range(value: f64, min: f64, max: f64) -> f64
{
// we calculate the distance between min and max
let distance = revolution_distance(min, max);
// we scale the value to the distance
(value - min) / distance
}
fn clamping_ranges(min: f64, max: f64) -> (f64, f64, f64, f64)
{
// normalize min and max
let min = wrap_revolution(min);
let max = wrap_revolution(max);
// calculates the distance between min and max (distance A in the test)
let in_spec_distance = revolution_distance(min, max);
// calculate distance B and clamping distance as per the test comment
let out_spec_distance = 1.0 - in_spec_distance;
let clamping_distance = out_spec_distance / 2.0;
let clamp_to_min_min = wrap_revolution(min - clamping_distance);
let clamp_to_min_max = min;
let clamp_to_max_min = max;
let clamp_to_max_max = wrap_revolution(max + clamping_distance);
(
clamp_to_min_min,
clamp_to_min_max,
clamp_to_max_min,
clamp_to_max_max,
)
}
fn revolution_distance(min: f64, max: f64) -> f64
{
// Normalize the values to ensure they're in the [0, 1) range
let normalized_min = wrap_revolution(min);
let normalized_max = wrap_revolution(max);
// Check if the range crosses zero
if normalized_min > normalized_max {
// For ranges that cross zero (e.g., min = 0.9, max = 0.1)
// The distance is (1 - min) + max
1.0 - normalized_min + normalized_max
} else {
// For normal ranges (e.g., min = 0.1, max = 0.3)
// The distance is simply max - min
normalized_max - normalized_min
}
}
fn wrap_revolution(value: f64) -> f64
{
debug_assert!(value.is_finite(), "value must be finite");
match value % 1.0
{
0.0 if value >= 1.0 => 1.0,
n if n < 0.0 => n + 1.0,
n => n,
}
}
fn revolution_in_range(value: f64, min: f64, max: f64) -> bool
{
debug_assert!(value.is_finite(), "value must be finite");
debug_assert!(min.is_finite(), "min must be finite");
debug_assert!(max.is_finite(), "max must be finite");
debug_assert!(value >= 0.0 && value < 1.0, "value out of [0,1)");
debug_assert!(min >= 0.0 && min < 1.0, "min out of [0,1)");
debug_assert!(max >= 0.0 && max < 1.0, "max out of [0,1)");
let wraps = min > max;
if wraps {
// Range wraps across boundary (e.g., 0.9 → 0.1)
min <= value || value <= max
} else {
// Standard interval
min <= value && value <= max
}
}
-29
View File
@@ -1,29 +0,0 @@
// keep for backwards reference
mod clamp_revolution_old;
mod clamp_revolution;
pub use clamp_revolution::clamp_revolution;
pub use clamp_revolution::clamp_revolution_uom;
pub use clamp_revolution::scale_revolution_to_range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Clamp
{
None,
Min,
Max,
}
pub struct ClampedValue<T>
{
pub value: T,
pub clamp: Clamp,
}
impl<T> ClampedValue<T>
{
pub fn new(value: T, clamp: Clamp) -> Self
{
Self { value, clamp }
}
}
+6 -34
View File
@@ -1,5 +1,4 @@
use super::LaserMachine;
use crate::MachinesLiveValues;
use crate::MachineAct;
use crate::MachineMessage;
use crate::MachineValues;
@@ -35,26 +34,6 @@ impl MachineAct for LaserMachine {
self.emit_state();
}
// send live values to all connected machines every frame
if now.duration_since(self.last_machine_send) > Duration::from_millis(1) {
let values = MachinesLiveValues::Laser(self.get_live_values());
for machine in self.connected_machines.iter() {
const CHANNEL_MESSAGE_CAP: usize = 5;
// ensure we don' flood the channel. Cap it at 5
if machine.connection.len() <= CHANNEL_MESSAGE_CAP {
use MachineMessage::ReceiveLiveValues;
_ = machine
.connection
.try_send(ReceiveLiveValues(values.clone()));
}
}
// reset timer
self.last_machine_send = now;
}
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
@@ -81,18 +60,12 @@ impl MachineAct for LaserMachine {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(machine_connection) => {
if self.connected_machines.len() >= Self::MAX_CONNECTIONS {
tracing::debug!("Not adding machine connection. Max capacity reached!");
return;
}
self.connected_machines.push(machine_connection);
}
MachineMessage::DisconnectMachine(machine_connection) => {
self.connected_machines
.retain(|machine| machine.ident != machine_connection.ident);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connect to any Machine so do nothing*/
{}
MachineMessage::RequestValues(sender) => {
sender
.send_blocking(MachineValues {
@@ -106,7 +79,6 @@ impl MachineAct for LaserMachine {
()
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
+1 -8
View File
@@ -3,7 +3,7 @@ use crate::{
MACHINE_LASER_V1, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
use crate::{Machine, MachineConnection, MachineMessage};
use crate::{Machine, MachineMessage};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::{
@@ -53,11 +53,6 @@ pub struct LaserMachine {
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
did_change_state: bool,
/// last time we sent live values to connected machines
last_machine_send: Instant,
connected_machines: Vec<MachineConnection>,
}
impl Machine for LaserMachine {
@@ -102,8 +97,6 @@ impl LaserMachine {
machine: MACHINE_LASER_V1,
};
pub const MAX_CONNECTIONS: usize = 2;
pub fn get_live_values(&self) -> LiveValuesEvent {
let diameter = self.diameter.get::<millimeter>();
let x_diameter = self.x_diameter.map(|x| x.get::<millimeter>());
-2
View File
@@ -55,8 +55,6 @@ impl MachineNewTrait for LaserMachine {
higher_tolerance: Length::new::<millimeter>(0.05),
in_tolerance: true,
did_change_state: true,
last_machine_send: Instant::now(),
connected_machines: Vec::new(),
};
Ok(laser_machine)
+8 -39
View File
@@ -17,33 +17,18 @@ use std::any::Any;
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Instant;
pub mod analog_input_test_machine;
pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod digital_input_test_machine;
pub mod extruder1;
pub mod extruder2;
pub mod ip20_test_machine;
pub mod laser;
mod live_values;
pub mod machine_identification;
pub mod mock;
pub mod motor_test_machine;
pub mod minimal_machines;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod test_machine_stepper;
pub mod wago_8ch_dio_test_machine;
pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
pub mod wago_power;
pub mod winder2;
pub mod types;
pub mod helpers;
pub use live_values::MachinesLiveValues;
// pub use live_values::
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
@@ -81,7 +66,6 @@ pub struct CrossConnection {
pub enum AsyncThreadMessage {
NoMsg,
ConnectOneWayRequest(CrossConnection),
ConnectTwoWayRequest(CrossConnection),
DisconnectMachines(CrossConnection),
}
@@ -314,7 +298,6 @@ pub enum MachineMessage {
ConnectToMachine(MachineConnection),
DisconnectMachine(MachineConnection),
RequestValues(Sender<MachineValues>),
ReceiveLiveValues(MachinesLiveValues),
}
pub trait MachineApi {
@@ -490,10 +473,6 @@ pub trait MachineWithChannel: Send + Debug + Sync {
fn get_live_values(&self) -> Option<Self::LiveValues> {
None
}
fn on_receive_live_values(&mut self,live_values: MachinesLiveValues) { _ = live_values; }
fn on_connect_to_machine(&mut self,connection: MachineConnection) { _ = connection; }
fn on_disconnect_machine(&mut self,connection: MachineConnection) { _ = connection; }
}
impl<C> MachineApi for C
@@ -523,15 +502,9 @@ impl<C> MachineAct for C
where
C: MachineWithChannel,
{
fn act(&mut self, now: Instant)
{
for _ in 0..5 // Limit to 5 requests max
{
match self.get_machine_channel_mut().api_receiver.try_recv()
{
Ok(msg) => self.act_machine_message(msg),
Err(_) => break,
}
fn act(&mut self, now: Instant) {
while let Ok(msg) = self.get_machine_channel_mut().api_receiver.try_recv() {
self.act_machine_message(msg);
}
if let Err(e) = self.update(now) {
@@ -553,11 +526,11 @@ where
MachineMessage::HttpApiJsonRequest(value) => {
let _ = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(connection) => {
self.on_connect_to_machine(connection);
MachineMessage::ConnectToMachine(_machine_connection) => {
todo!();
}
MachineMessage::DisconnectMachine(connection) => {
self.on_disconnect_machine(connection);
MachineMessage::DisconnectMachine(_machine_connection) => {
todo!();
}
MachineMessage::RequestValues(sender) => {
sender
@@ -570,10 +543,6 @@ where
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(live_values) =>
{
self.on_receive_live_values(live_values);
}
}
}
}
-6
View File
@@ -1,6 +0,0 @@
use crate::laser::api::LiveValuesEvent as LaserLiveValues;
#[derive(Debug, Clone)]
pub enum MachinesLiveValues {
Laser(LaserLiveValues),
}
@@ -1,7 +1,8 @@
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::{
MachineAct, MachineMessage, MachineValues, analog_input_test_machine::AnalogInputTestMachine,
MachineAct, MachineMessage, MachineValues,
minimal_machines::analog_input_test_machine::AnalogInputTestMachine,
};
impl MachineAct for AnalogInputTestMachine {
@@ -27,7 +28,6 @@ impl MachineAct for AnalogInputTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
@@ -8,7 +8,7 @@ use control_core::socketio::{
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, analog_input_test_machine::AnalogInputTestMachine};
use crate::{MachineApi, minimal_machines::analog_input_test_machine::AnalogInputTestMachine};
#[derive(Debug, Clone)]
pub struct AnalogInputTestMachineNamespace {
@@ -4,11 +4,9 @@ use control_core::socketio::{event::Event, namespace::NamespaceCacheingLogic};
use ethercat_hal::io::analog_input::AnalogInput;
use smol::channel::{Receiver, Sender};
use self::api::{AnalogInputTestMachineEvents, AnalogInputTestMachineNamespace, MeasurementEvent};
use crate::{
ANALOG_INPUT_TEST_MACHINE, AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH,
analog_input_test_machine::api::{
AnalogInputTestMachineEvents, AnalogInputTestMachineNamespace, MeasurementEvent,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
@@ -7,10 +7,10 @@ use ethercat_hal::{
};
use smol::{block_on, channel::unbounded};
use super::{AnalogInputTestMachine, api::AnalogInputTestMachineNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
analog_input_test_machine::{AnalogInputTestMachine, api::AnalogInputTestMachineNamespace},
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
impl MachineNewTrait for AnalogInputTestMachine {
@@ -41,7 +41,6 @@ impl MachineAct for DigitalInputTestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
@@ -8,7 +8,7 @@ use control_core::socketio::{
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, digital_input_test_machine::DigitalInputTestMachine};
use crate::{MachineApi, minimal_machines::digital_input_test_machine::DigitalInputTestMachine};
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
@@ -4,11 +4,9 @@ use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_input::DigitalInput;
use smol::channel::{Receiver, Sender};
use self::api::{DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent};
use crate::{
AsyncThreadMessage, DIGITAL_INPUT_TEST_MACHINE, Machine, MachineMessage, VENDOR_QITECH,
digital_input_test_machine::api::{
DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
@@ -11,10 +11,10 @@ use ethercat_hal::{
};
use smol::{block_on, lock::RwLock};
use super::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
digital_input_test_machine::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace},
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
impl MachineNewTrait for DigitalInputTestMachine {
@@ -53,7 +53,6 @@ impl MachineAct for IP20TestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
@@ -1,5 +1,7 @@
use crate::ip20_test_machine::api::{IP20TestMachineEvents, LiveValuesEvent, StateEvent};
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::minimal_machines::ip20_test_machine::api::{
IP20TestMachineEvents, LiveValuesEvent, StateEvent,
};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_input::DigitalInput;
@@ -9,7 +11,7 @@ use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::ip20_test_machine::api::IP20TestMachineNamespace;
use crate::minimal_machines::ip20_test_machine::api::IP20TestMachineNamespace;
use crate::{IP20_TEST_MACHINE, VENDOR_QITECH};
#[derive(Debug)]
@@ -1,5 +1,5 @@
use crate::ip20_test_machine::IP20TestMachine;
use crate::ip20_test_machine::api::IP20TestMachineNamespace;
use crate::minimal_machines::ip20_test_machine::IP20TestMachine;
use crate::minimal_machines::ip20_test_machine::api::IP20TestMachineNamespace;
use smol::block_on;
use std::time::Instant;
@@ -68,7 +68,6 @@ impl MachineAct for MockMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
+10
View File
@@ -0,0 +1,10 @@
pub mod analog_input_test_machine;
pub mod digital_input_test_machine;
pub mod ip20_test_machine;
pub mod mock;
pub mod motor_test_machine;
pub mod test_machine;
pub mod test_machine_stepper;
pub mod wago_8ch_dio_test_machine;
pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
@@ -41,7 +41,6 @@ impl MachineAct for TestMachine {
.expect("Failed to send values");
sender.close();
}
MachineMessage::ReceiveLiveValues(_) => {}
}
}
}
@@ -1,5 +1,5 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine::api::{StateEvent, TestMachineEvents};
use crate::minimal_machines::test_machine::api::{StateEvent, TestMachineEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_output::DigitalOutput;
@@ -8,7 +8,7 @@ use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine::api::TestMachineNamespace;
use crate::minimal_machines::test_machine::api::TestMachineNamespace;
use crate::{TEST_MACHINE, VENDOR_QITECH};
#[derive(Debug)]
@@ -1,5 +1,5 @@
use crate::test_machine::TestMachine;
use crate::test_machine::api::TestMachineNamespace;
use crate::minimal_machines::test_machine::TestMachine;
use crate::minimal_machines::test_machine::api::TestMachineNamespace;
use smol::block_on;
use std::time::Instant;
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more